mirror of
https://github.com/vitorpamplona/amethyst.git
synced 2026-07-22 15:52:22 +00:00
Compare commits
35 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6ad00c112a | ||
|
|
0010a19c24 | ||
|
|
d78c7a91cb | ||
|
|
4a2dced02f | ||
|
|
2d1a63be57 | ||
|
|
9e207d0f24 | ||
|
|
efad94ddfd | ||
|
|
d49fde755e | ||
|
|
0224af18c0 | ||
|
|
53a4dfb702 | ||
|
|
4e4e798133 | ||
|
|
e8bd4a12ac | ||
|
|
dffed6e78f | ||
|
|
46a4f360f7 | ||
|
|
64a5071345 | ||
|
|
1fba93b4f0 | ||
|
|
caa67b03a8 | ||
|
|
5f1b2f9eb8 | ||
|
|
2968cb12aa | ||
|
|
8261b38de9 | ||
|
|
2c4489a0d0 | ||
|
|
52dcb73fd7 | ||
|
|
91a2f8ecfa | ||
|
|
6905e1b47f | ||
|
|
640b0d3174 | ||
|
|
a86cd8772f | ||
|
|
83be195a43 | ||
|
|
14bf73e2f1 | ||
|
|
efb7b06e4e | ||
|
|
8b9f37d678 | ||
|
|
e83f16f5c5 | ||
|
|
e648970b17 | ||
|
|
44badf8c86 | ||
|
|
516ff17eed | ||
|
|
3b1533eb82 |
@@ -41,7 +41,7 @@ height="80">](https://github.com/vitorpamplona/amethyst/releases)
|
||||
- [x] URI Support (NIP-21)
|
||||
- [x] Created_at Limits (NIP-22)
|
||||
- [x] Event Deletion (NIP-09)
|
||||
- [x] Identity Verification (NIP-05)
|
||||
- [x] Nostr Address (NIP-05)
|
||||
- [x] Long-form Content (NIP-23)
|
||||
- [x] Parameterized Replaceable Events (NIP-33)
|
||||
- [x] Online Relay Search (NIP-50)
|
||||
|
||||
@@ -13,8 +13,8 @@ android {
|
||||
applicationId "com.vitorpamplona.amethyst"
|
||||
minSdk 26
|
||||
targetSdk 33
|
||||
versionCode 250
|
||||
versionName "0.68.2"
|
||||
versionCode 253
|
||||
versionName "0.69.0"
|
||||
|
||||
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
||||
vectorDrawables {
|
||||
@@ -139,9 +139,9 @@ dependencies {
|
||||
implementation 'androidx.security:security-crypto-ktx:1.1.0-alpha06'
|
||||
|
||||
// view videos
|
||||
implementation "com.google.android.exoplayer:exoplayer:${exoplayer_version}"
|
||||
implementation "androidx.media3:media3-exoplayer:${exoplayer_version}"
|
||||
// important for proxy / tor
|
||||
implementation "com.google.android.exoplayer:extension-okhttp:${exoplayer_version}"
|
||||
implementation "androidx.media3:media3-datasource-okhttp:${exoplayer_version}"
|
||||
|
||||
// Load images from the web.
|
||||
implementation "io.coil-kt:coil-compose:$coil_version"
|
||||
@@ -175,6 +175,8 @@ dependencies {
|
||||
|
||||
// Language picker and Theme chooser
|
||||
implementation 'androidx.appcompat:appcompat:1.6.1'
|
||||
implementation 'androidx.media3:media3-exoplayer-hls:1.1.0'
|
||||
implementation 'androidx.media3:media3-ui:1.1.0'
|
||||
|
||||
// Local model for language identification
|
||||
playImplementation 'com.google.mlkit:language-id:17.0.4'
|
||||
|
||||
@@ -79,6 +79,7 @@ object ServiceManager {
|
||||
NostrAccountDataSource.start()
|
||||
NostrChatroomListDataSource.start()
|
||||
NostrDiscoveryDataSource.start()
|
||||
NostrVideoDataSource.start()
|
||||
|
||||
// More Info Data Sources
|
||||
NostrSingleEventDataSource.start()
|
||||
|
||||
@@ -1,14 +1,15 @@
|
||||
package com.vitorpamplona.amethyst
|
||||
|
||||
import android.content.Context
|
||||
import com.google.android.exoplayer2.database.StandaloneDatabaseProvider
|
||||
import com.google.android.exoplayer2.ext.okhttp.OkHttpDataSource
|
||||
import com.google.android.exoplayer2.upstream.cache.CacheDataSource
|
||||
import com.google.android.exoplayer2.upstream.cache.LeastRecentlyUsedCacheEvictor
|
||||
import com.google.android.exoplayer2.upstream.cache.SimpleCache
|
||||
import androidx.media3.common.util.UnstableApi
|
||||
import androidx.media3.database.StandaloneDatabaseProvider
|
||||
import androidx.media3.datasource.cache.CacheDataSource
|
||||
import androidx.media3.datasource.cache.LeastRecentlyUsedCacheEvictor
|
||||
import androidx.media3.datasource.cache.SimpleCache
|
||||
import androidx.media3.datasource.okhttp.OkHttpDataSource
|
||||
import com.vitorpamplona.amethyst.service.HttpClient
|
||||
|
||||
object VideoCache {
|
||||
@UnstableApi object VideoCache {
|
||||
|
||||
var exoPlayerCacheSize: Long = 90 * 1024 * 1024 // 90MB
|
||||
|
||||
|
||||
@@ -6,6 +6,8 @@ import androidx.compose.runtime.Immutable
|
||||
import androidx.compose.runtime.Stable
|
||||
import androidx.core.os.ConfigurationCompat
|
||||
import androidx.lifecycle.LiveData
|
||||
import androidx.lifecycle.distinctUntilChanged
|
||||
import androidx.lifecycle.map
|
||||
import com.vitorpamplona.amethyst.OptOutFromFilters
|
||||
import com.vitorpamplona.amethyst.service.FileHeader
|
||||
import com.vitorpamplona.amethyst.service.NostrLnZapPaymentResponseDataSource
|
||||
@@ -18,6 +20,10 @@ import com.vitorpamplona.amethyst.service.relays.RelayPool
|
||||
import com.vitorpamplona.amethyst.ui.actions.ServersAvailable
|
||||
import com.vitorpamplona.amethyst.ui.components.BundledUpdate
|
||||
import com.vitorpamplona.amethyst.ui.note.Nip47URI
|
||||
import com.vitorpamplona.amethyst.ui.note.combineWith
|
||||
import kotlinx.collections.immutable.ImmutableSet
|
||||
import kotlinx.collections.immutable.persistentSetOf
|
||||
import kotlinx.collections.immutable.toImmutableSet
|
||||
import kotlinx.coroutines.DelicateCoroutinesApi
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.GlobalScope
|
||||
@@ -49,9 +55,11 @@ val KIND3_FOLLOWS = " All Follows "
|
||||
@Stable
|
||||
class Account(
|
||||
val loggedIn: Persona,
|
||||
var followingChannels: Set<String> = DefaultChannels,
|
||||
var followingCommunities: Set<String> = setOf(),
|
||||
var hiddenUsers: Set<String> = setOf(),
|
||||
|
||||
var followingChannels: Set<String> = DefaultChannels, // deprecated
|
||||
var followingCommunities: Set<String> = setOf(), // deprecated
|
||||
var hiddenUsers: Set<String> = setOf(), // deprecated
|
||||
|
||||
var localRelays: Set<RelaySetupInfo> = Constants.defaultRelays.toSet(),
|
||||
var dontTranslateFrom: Set<String> = getLanguagesSpokenByUser(),
|
||||
var languagePreferences: Map<String, String> = mapOf(),
|
||||
@@ -76,7 +84,7 @@ class Account(
|
||||
var lastReadPerRoute: Map<String, Long> = mapOf<String, Long>(),
|
||||
var settings: Settings = Settings()
|
||||
) {
|
||||
var transientHiddenUsers: Set<String> = setOf()
|
||||
var transientHiddenUsers: ImmutableSet<String> = persistentSetOf()
|
||||
|
||||
// Observers line up here.
|
||||
val live: AccountLiveData = AccountLiveData(this)
|
||||
@@ -84,7 +92,24 @@ class Account(
|
||||
val liveLastRead: AccountLiveData = AccountLiveData(this)
|
||||
val saveable: AccountLiveData = AccountLiveData(this)
|
||||
|
||||
@Immutable
|
||||
data class LiveHiddenUsers(
|
||||
val hiddenUsers: ImmutableSet<String>,
|
||||
val spammers: ImmutableSet<String>,
|
||||
val showSensitiveContent: Boolean?
|
||||
)
|
||||
|
||||
val liveHiddenUsers: LiveData<LiveHiddenUsers> = live.combineWith(getBlockListNote().live().metadata) { localLive, liveMuteListEvent ->
|
||||
val liveBlockedUsers = (liveMuteListEvent?.note?.event as? PeopleListEvent)?.publicAndPrivateUsers(loggedIn.privKey)
|
||||
LiveHiddenUsers(
|
||||
hiddenUsers = liveBlockedUsers ?: persistentSetOf(),
|
||||
spammers = localLive?.account?.transientHiddenUsers ?: persistentSetOf(),
|
||||
showSensitiveContent = showSensitiveContent
|
||||
)
|
||||
}.distinctUntilChanged()
|
||||
|
||||
var userProfileCache: User? = null
|
||||
|
||||
fun updateAutomaticallyStartPlayback(
|
||||
automaticallyStartPlayback: Boolean?
|
||||
) {
|
||||
@@ -114,7 +139,7 @@ class Account(
|
||||
filterSpamFromStrangers = filterSpam
|
||||
OptOutFromFilters.start(warnAboutPostsWithReports, filterSpamFromStrangers)
|
||||
if (!filterSpamFromStrangers) {
|
||||
transientHiddenUsers = setOf()
|
||||
transientHiddenUsers = persistentSetOf()
|
||||
}
|
||||
live.invalidateData()
|
||||
saveable.invalidateData()
|
||||
@@ -128,14 +153,6 @@ class Account(
|
||||
}
|
||||
}
|
||||
|
||||
fun followingChannels(): List<Channel> {
|
||||
return followingChannels.mapNotNull { LocalCache.checkGetOrCreateChannel(it) }
|
||||
}
|
||||
|
||||
fun followingCommunities(): List<AddressableNote> {
|
||||
return followingCommunities.mapNotNull { LocalCache.checkGetOrCreateAddressableNote(it) }
|
||||
}
|
||||
|
||||
fun isWriteable(): Boolean {
|
||||
return loggedIn.privKey != null
|
||||
}
|
||||
@@ -144,21 +161,25 @@ class Account(
|
||||
if (!isWriteable()) return
|
||||
|
||||
val contactList = userProfile().latestContactList
|
||||
val follows = contactList?.follows() ?: emptyList()
|
||||
val followsTags = contactList?.unverifiedFollowTagSet() ?: emptyList()
|
||||
|
||||
if (contactList != null && follows.isNotEmpty()) {
|
||||
val event = ContactListEvent.create(
|
||||
follows,
|
||||
followsTags,
|
||||
relays,
|
||||
loggedIn.privKey!!
|
||||
if (contactList != null && contactList.tags.isNotEmpty()) {
|
||||
val event = ContactListEvent.updateRelayList(
|
||||
earlierVersion = contactList,
|
||||
relayUse = relays,
|
||||
privateKey = loggedIn.privKey!!
|
||||
)
|
||||
|
||||
Client.send(event)
|
||||
LocalCache.consume(event)
|
||||
} else {
|
||||
val event = ContactListEvent.create(listOf(), listOf(), relays, loggedIn.privKey!!)
|
||||
val event = ContactListEvent.createFromScratch(
|
||||
followUsers = listOf(),
|
||||
followTags = listOf(),
|
||||
followCommunities = listOf(),
|
||||
followEvents = DefaultChannels.toList(),
|
||||
relayUse = relays,
|
||||
privateKey = loggedIn.privKey!!
|
||||
)
|
||||
|
||||
// Keep this local to avoid erasing a good contact list.
|
||||
// Client.send(event)
|
||||
@@ -398,27 +419,90 @@ class Account(
|
||||
}
|
||||
}
|
||||
|
||||
private fun migrateCommunitiesAndChannelsIfNeeded(latestContactList: ContactListEvent?): ContactListEvent? {
|
||||
if (latestContactList == null) return latestContactList
|
||||
|
||||
var returningContactList: ContactListEvent = latestContactList
|
||||
|
||||
if (followingCommunities.isNotEmpty()) {
|
||||
followingCommunities.forEach {
|
||||
ATag.parse(it, null)?.let {
|
||||
returningContactList = ContactListEvent.followAddressableEvent(returningContactList, it, loggedIn.privKey!!)
|
||||
}
|
||||
}
|
||||
followingCommunities = emptySet()
|
||||
}
|
||||
|
||||
if (followingChannels.isNotEmpty()) {
|
||||
followingChannels.forEach {
|
||||
returningContactList = ContactListEvent.followEvent(returningContactList, it, loggedIn.privKey!!)
|
||||
}
|
||||
followingChannels = emptySet()
|
||||
}
|
||||
|
||||
return returningContactList
|
||||
}
|
||||
|
||||
fun follow(user: User) {
|
||||
if (!isWriteable()) return
|
||||
|
||||
val contactList = userProfile().latestContactList
|
||||
val followingUsers = contactList?.follows() ?: emptyList()
|
||||
val followingTags = contactList?.unverifiedFollowTagSet() ?: emptyList()
|
||||
val contactList = migrateCommunitiesAndChannelsIfNeeded(userProfile().latestContactList)
|
||||
|
||||
val event = if (contactList != null) {
|
||||
ContactListEvent.create(
|
||||
followingUsers.plus(Contact(user.pubkeyHex, null)),
|
||||
followingTags,
|
||||
contactList.relays(),
|
||||
loggedIn.privKey!!
|
||||
ContactListEvent.followUser(contactList, user.pubkeyHex, loggedIn.privKey!!)
|
||||
} else {
|
||||
ContactListEvent.createFromScratch(
|
||||
followUsers = listOf(Contact(user.pubkeyHex, null)),
|
||||
followTags = emptyList(),
|
||||
followCommunities = emptyList(),
|
||||
followEvents = DefaultChannels.toList(),
|
||||
relayUse = Constants.defaultRelays.associate { it.url to ContactListEvent.ReadWrite(it.read, it.write) },
|
||||
privateKey = loggedIn.privKey!!
|
||||
)
|
||||
}
|
||||
|
||||
Client.send(event)
|
||||
LocalCache.consume(event)
|
||||
}
|
||||
|
||||
fun follow(channel: Channel) {
|
||||
if (!isWriteable()) return
|
||||
|
||||
val contactList = migrateCommunitiesAndChannelsIfNeeded(userProfile().latestContactList)
|
||||
|
||||
val event = if (contactList != null) {
|
||||
ContactListEvent.followEvent(contactList, channel.idHex, loggedIn.privKey!!)
|
||||
} else {
|
||||
ContactListEvent.createFromScratch(
|
||||
followUsers = emptyList(),
|
||||
followTags = emptyList(),
|
||||
followCommunities = emptyList(),
|
||||
followEvents = DefaultChannels.toList().plus(channel.idHex),
|
||||
relayUse = Constants.defaultRelays.associate { it.url to ContactListEvent.ReadWrite(it.read, it.write) },
|
||||
privateKey = loggedIn.privKey!!
|
||||
)
|
||||
}
|
||||
|
||||
Client.send(event)
|
||||
LocalCache.consume(event)
|
||||
}
|
||||
|
||||
fun follow(community: AddressableNote) {
|
||||
if (!isWriteable()) return
|
||||
|
||||
val contactList = migrateCommunitiesAndChannelsIfNeeded(userProfile().latestContactList)
|
||||
|
||||
val event = if (contactList != null) {
|
||||
ContactListEvent.followAddressableEvent(contactList, community.address, loggedIn.privKey!!)
|
||||
} else {
|
||||
val relays = Constants.defaultRelays.associate { it.url to ContactListEvent.ReadWrite(it.read, it.write) }
|
||||
ContactListEvent.create(
|
||||
listOf(Contact(user.pubkeyHex, null)),
|
||||
followingTags,
|
||||
relays,
|
||||
loggedIn.privKey!!
|
||||
ContactListEvent.createFromScratch(
|
||||
followUsers = emptyList(),
|
||||
followTags = emptyList(),
|
||||
followCommunities = listOf(community.address),
|
||||
followEvents = DefaultChannels.toList(),
|
||||
relayUse = relays,
|
||||
privateKey = loggedIn.privKey!!
|
||||
)
|
||||
}
|
||||
|
||||
@@ -429,24 +513,22 @@ class Account(
|
||||
fun follow(tag: String) {
|
||||
if (!isWriteable()) return
|
||||
|
||||
val contactList = userProfile().latestContactList
|
||||
val followingUsers = contactList?.follows() ?: emptyList()
|
||||
val followingTags = contactList?.unverifiedFollowTagSet() ?: emptyList()
|
||||
val contactList = migrateCommunitiesAndChannelsIfNeeded(userProfile().latestContactList)
|
||||
|
||||
val event = if (contactList != null) {
|
||||
ContactListEvent.create(
|
||||
followingUsers,
|
||||
followingTags.plus(tag),
|
||||
contactList.relays(),
|
||||
ContactListEvent.followHashtag(
|
||||
contactList,
|
||||
tag,
|
||||
loggedIn.privKey!!
|
||||
)
|
||||
} else {
|
||||
val relays = Constants.defaultRelays.associate { it.url to ContactListEvent.ReadWrite(it.read, it.write) }
|
||||
ContactListEvent.create(
|
||||
followingUsers,
|
||||
followingTags.plus(tag),
|
||||
relays,
|
||||
loggedIn.privKey!!
|
||||
ContactListEvent.createFromScratch(
|
||||
followUsers = emptyList(),
|
||||
followTags = listOf(tag),
|
||||
followCommunities = emptyList(),
|
||||
followEvents = DefaultChannels.toList(),
|
||||
relayUse = Constants.defaultRelays.associate { it.url to ContactListEvent.ReadWrite(it.read, it.write) },
|
||||
privateKey = loggedIn.privKey!!
|
||||
)
|
||||
}
|
||||
|
||||
@@ -457,15 +539,12 @@ class Account(
|
||||
fun unfollow(user: User) {
|
||||
if (!isWriteable()) return
|
||||
|
||||
val contactList = userProfile().latestContactList
|
||||
val followingUsers = contactList?.follows() ?: emptyList()
|
||||
val followingTags = contactList?.unverifiedFollowTagSet() ?: emptyList()
|
||||
val contactList = migrateCommunitiesAndChannelsIfNeeded(userProfile().latestContactList)
|
||||
|
||||
if (contactList != null && (followingUsers.isNotEmpty() || followingTags.isNotEmpty())) {
|
||||
val event = ContactListEvent.create(
|
||||
followingUsers.filter { it.pubKeyHex != user.pubkeyHex },
|
||||
followingTags,
|
||||
contactList.relays(),
|
||||
if (contactList != null && contactList.tags.isNotEmpty()) {
|
||||
val event = ContactListEvent.unfollowUser(
|
||||
contactList,
|
||||
user.pubkeyHex,
|
||||
loggedIn.privKey!!
|
||||
)
|
||||
|
||||
@@ -477,15 +556,46 @@ class Account(
|
||||
fun unfollow(tag: String) {
|
||||
if (!isWriteable()) return
|
||||
|
||||
val contactList = userProfile().latestContactList
|
||||
val followingUsers = contactList?.follows() ?: emptyList()
|
||||
val followingTags = contactList?.unverifiedFollowTagSet() ?: emptyList()
|
||||
val contactList = migrateCommunitiesAndChannelsIfNeeded(userProfile().latestContactList)
|
||||
|
||||
if (contactList != null && (followingUsers.isNotEmpty() || followingTags.isNotEmpty())) {
|
||||
val event = ContactListEvent.create(
|
||||
followingUsers,
|
||||
followingTags.filter { !it.equals(tag, ignoreCase = true) },
|
||||
contactList.relays(),
|
||||
if (contactList != null && contactList.tags.isNotEmpty()) {
|
||||
val event = ContactListEvent.unfollowHashtag(
|
||||
contactList,
|
||||
tag,
|
||||
loggedIn.privKey!!
|
||||
)
|
||||
|
||||
Client.send(event)
|
||||
LocalCache.consume(event)
|
||||
}
|
||||
}
|
||||
|
||||
fun unfollow(channel: Channel) {
|
||||
if (!isWriteable()) return
|
||||
|
||||
val contactList = migrateCommunitiesAndChannelsIfNeeded(userProfile().latestContactList)
|
||||
|
||||
if (contactList != null && contactList.tags.isNotEmpty()) {
|
||||
val event = ContactListEvent.unfollowEvent(
|
||||
contactList,
|
||||
channel.idHex,
|
||||
loggedIn.privKey!!
|
||||
)
|
||||
|
||||
Client.send(event)
|
||||
LocalCache.consume(event)
|
||||
}
|
||||
}
|
||||
|
||||
fun unfollow(community: AddressableNote) {
|
||||
if (!isWriteable()) return
|
||||
|
||||
val contactList = migrateCommunitiesAndChannelsIfNeeded(userProfile().latestContactList)
|
||||
|
||||
if (contactList != null && contactList.tags.isNotEmpty()) {
|
||||
val event = ContactListEvent.unfollowAddressableEvent(
|
||||
contactList,
|
||||
community.address,
|
||||
loggedIn.privKey!!
|
||||
)
|
||||
|
||||
@@ -708,7 +818,9 @@ class Account(
|
||||
Client.send(event)
|
||||
LocalCache.consume(event)
|
||||
|
||||
joinChannel(event.id)
|
||||
LocalCache.getChannelIfExists(event.id)?.let {
|
||||
follow(it)
|
||||
}
|
||||
}
|
||||
|
||||
fun removeEmojiPack(usersEmojiList: Note, emojiList: Note) {
|
||||
@@ -932,43 +1044,70 @@ class Account(
|
||||
}
|
||||
}
|
||||
|
||||
fun joinChannel(idHex: String) {
|
||||
followingChannels = followingChannels + idHex
|
||||
live.invalidateData()
|
||||
|
||||
saveable.invalidateData()
|
||||
fun getBlockListNote(): AddressableNote {
|
||||
val aTag = ATag(PeopleListEvent.kind, userProfile().pubkeyHex, PeopleListEvent.blockList, null)
|
||||
return LocalCache.getOrCreateAddressableNote(aTag)
|
||||
}
|
||||
|
||||
fun leaveChannel(idHex: String) {
|
||||
followingChannels = followingChannels - idHex
|
||||
live.invalidateData()
|
||||
|
||||
saveable.invalidateData()
|
||||
fun getBlockList(): PeopleListEvent? {
|
||||
return getBlockListNote().event as? PeopleListEvent
|
||||
}
|
||||
|
||||
fun joinCommunity(idHex: String) {
|
||||
followingCommunities = followingCommunities + idHex
|
||||
live.invalidateData()
|
||||
private fun migrateHiddenUsersIfNeeded(latestList: PeopleListEvent?): PeopleListEvent? {
|
||||
if (latestList == null) return latestList
|
||||
|
||||
saveable.invalidateData()
|
||||
}
|
||||
var returningList: PeopleListEvent = latestList
|
||||
|
||||
fun leaveCommunity(idHex: String) {
|
||||
followingCommunities = followingCommunities - idHex
|
||||
live.invalidateData()
|
||||
if (hiddenUsers.isNotEmpty()) {
|
||||
returningList = PeopleListEvent.addUsers(returningList, hiddenUsers.toList(), true, loggedIn.privKey!!)
|
||||
hiddenUsers = emptySet()
|
||||
}
|
||||
|
||||
saveable.invalidateData()
|
||||
return returningList
|
||||
}
|
||||
|
||||
fun hideUser(pubkeyHex: String) {
|
||||
hiddenUsers = hiddenUsers + pubkeyHex
|
||||
val blockList = migrateHiddenUsersIfNeeded(getBlockList())
|
||||
|
||||
val event = if (blockList != null) {
|
||||
PeopleListEvent.addUser(
|
||||
earlierVersion = blockList,
|
||||
pubKeyHex = pubkeyHex,
|
||||
isPrivate = true,
|
||||
privateKey = loggedIn.privKey!!
|
||||
)
|
||||
} else {
|
||||
PeopleListEvent.createListWithUser(
|
||||
name = PeopleListEvent.blockList,
|
||||
pubKeyHex = pubkeyHex,
|
||||
isPrivate = true,
|
||||
privateKey = loggedIn.privKey!!
|
||||
)
|
||||
}
|
||||
|
||||
Client.send(event)
|
||||
LocalCache.consume(event)
|
||||
|
||||
live.invalidateData()
|
||||
saveable.invalidateData()
|
||||
}
|
||||
|
||||
fun showUser(pubkeyHex: String) {
|
||||
hiddenUsers = hiddenUsers - pubkeyHex
|
||||
transientHiddenUsers = transientHiddenUsers - pubkeyHex
|
||||
val blockList = migrateHiddenUsersIfNeeded(getBlockList())
|
||||
|
||||
if (blockList != null) {
|
||||
val event = PeopleListEvent.removeUser(
|
||||
earlierVersion = blockList,
|
||||
pubKeyHex = pubkeyHex,
|
||||
isPrivate = true,
|
||||
privateKey = loggedIn.privKey!!
|
||||
)
|
||||
|
||||
Client.send(event)
|
||||
LocalCache.consume(event)
|
||||
}
|
||||
|
||||
transientHiddenUsers = (transientHiddenUsers - pubkeyHex).toImmutableSet()
|
||||
live.invalidateData()
|
||||
saveable.invalidateData()
|
||||
}
|
||||
@@ -1055,7 +1194,53 @@ class Account(
|
||||
if (listName == GLOBAL_FOLLOWS) return null
|
||||
if (listName == KIND3_FOLLOWS) return userProfile().cachedFollowingTagSet()
|
||||
|
||||
return emptySet()
|
||||
val privKey = loggedIn.privKey
|
||||
|
||||
return if (listName != null) {
|
||||
val aTag = ATag(PeopleListEvent.kind, userProfile().pubkeyHex, listName, null).toTag()
|
||||
val list = LocalCache.addressables[aTag]
|
||||
if (list != null) {
|
||||
val publicAddresses = list.event?.hashtags() ?: emptySet()
|
||||
val privateAddresses = privKey?.let {
|
||||
(list.event as? GeneralListEvent)?.privateHashtags(it)
|
||||
} ?: emptySet()
|
||||
|
||||
(publicAddresses + privateAddresses).toSet()
|
||||
} else {
|
||||
emptySet()
|
||||
}
|
||||
} else {
|
||||
emptySet()
|
||||
}
|
||||
}
|
||||
|
||||
fun selectedCommunitiesFollowList(listName: String?): Set<String>? {
|
||||
if (listName == GLOBAL_FOLLOWS) return null
|
||||
if (listName == KIND3_FOLLOWS) return userProfile().cachedFollowingCommunitiesSet()
|
||||
|
||||
val privKey = loggedIn.privKey
|
||||
|
||||
return if (listName != null) {
|
||||
val aTag = ATag(PeopleListEvent.kind, userProfile().pubkeyHex, listName, null).toTag()
|
||||
val list = LocalCache.addressables[aTag]
|
||||
if (list != null) {
|
||||
val publicAddresses = list.event?.taggedAddresses()?.map { it.toTag() } ?: emptySet()
|
||||
val privateAddresses = privKey?.let {
|
||||
(list.event as? GeneralListEvent)?.privateTaggedAddresses(it)?.map { it.toTag() }
|
||||
} ?: emptySet()
|
||||
|
||||
(publicAddresses + privateAddresses).toSet()
|
||||
} else {
|
||||
emptySet()
|
||||
}
|
||||
} else {
|
||||
emptySet()
|
||||
}
|
||||
}
|
||||
|
||||
fun selectedChatsFollowList(): Set<String> {
|
||||
val contactList = userProfile().latestContactList
|
||||
return contactList?.taggedEvents()?.toSet() ?: DefaultChannels
|
||||
}
|
||||
|
||||
fun sendChangeChannel(name: String, about: String, picture: String, channel: Channel) {
|
||||
@@ -1076,7 +1261,7 @@ class Account(
|
||||
Client.send(event)
|
||||
LocalCache.consume(event)
|
||||
|
||||
joinChannel(event.id)
|
||||
follow(channel)
|
||||
}
|
||||
|
||||
fun decryptContent(note: Note): String? {
|
||||
@@ -1224,7 +1409,11 @@ class Account(
|
||||
}
|
||||
|
||||
fun isHidden(user: User) = isHidden(user.pubkeyHex)
|
||||
fun isHidden(userHex: String) = userHex in hiddenUsers || userHex in transientHiddenUsers
|
||||
fun isHidden(userHex: String): Boolean {
|
||||
val blockList = getBlockList()
|
||||
|
||||
return (blockList?.publicAndPrivateUsers(loggedIn.privKey)?.contains(userHex) ?: false) || userHex in transientHiddenUsers
|
||||
}
|
||||
|
||||
fun followingKeySet(): Set<HexKey> {
|
||||
return userProfile().cachedFollowingKeySet()
|
||||
@@ -1343,7 +1532,7 @@ class Account(
|
||||
it.cache.spamMessages.snapshot().values.forEach {
|
||||
if (it.pubkeyHex !in transientHiddenUsers && it.duplicatedMessages.size >= 5) {
|
||||
if (it.pubkeyHex != userProfile().pubkeyHex && it.pubkeyHex !in followingKeySet()) {
|
||||
transientHiddenUsers = transientHiddenUsers + it.pubkeyHex
|
||||
transientHiddenUsers = (transientHiddenUsers + it.pubkeyHex).toImmutableSet()
|
||||
live.invalidateData()
|
||||
}
|
||||
}
|
||||
@@ -1356,6 +1545,7 @@ class Account(
|
||||
Log.d("Init", "Account")
|
||||
backupContactList?.let {
|
||||
println("Loading saved contacts ${it.toJson()}")
|
||||
|
||||
if (userProfile().latestContactList == null) {
|
||||
LocalCache.consume(it)
|
||||
}
|
||||
|
||||
@@ -33,7 +33,7 @@ object LocalCache {
|
||||
ConcurrentHashMap<HexKey, Pair<Note?, (LnZapPaymentResponseEvent) -> Unit>>(10)
|
||||
|
||||
fun checkGetOrCreateUser(key: String): User? {
|
||||
checkNotInMainThread()
|
||||
// checkNotInMainThread()
|
||||
|
||||
if (isValidHex(key)) {
|
||||
return getOrCreateUser(key)
|
||||
@@ -143,7 +143,7 @@ object LocalCache {
|
||||
}
|
||||
|
||||
fun getOrCreateAddressableNoteInternal(key: ATag): AddressableNote {
|
||||
checkNotInMainThread()
|
||||
// checkNotInMainThread()
|
||||
|
||||
// we can't use naddr here because naddr might include relay info and
|
||||
// the preferred relay should not be part of the index.
|
||||
@@ -176,6 +176,16 @@ object LocalCache {
|
||||
}
|
||||
}
|
||||
|
||||
fun consume(event: ContactListEvent) {
|
||||
val user = getOrCreateUser(event.pubKey)
|
||||
|
||||
// avoids processing empty contact lists.
|
||||
if (event.createdAt > (user.latestContactList?.createdAt ?: 0) && !event.tags.isEmpty()) {
|
||||
user.updateContactList(event)
|
||||
// Log.d("CL", "AAA ${user.toBestDisplayName()} ${follows.size}")
|
||||
}
|
||||
}
|
||||
|
||||
fun consume(event: BookmarkListEvent) {
|
||||
val user = getOrCreateUser(event.pubKey)
|
||||
if (user.latestBookmarkList == null || event.createdAt > user.latestBookmarkList!!.createdAt) {
|
||||
@@ -238,9 +248,6 @@ object LocalCache {
|
||||
|
||||
// Log.d("TN", "New Note (${notes.size},${users.size}) ${note.author?.toBestDisplayName()} ${note.event?.content()?.split("\n")?.take(100)} ${formattedDateTime(event.createdAt)}")
|
||||
|
||||
// Prepares user's profile view.
|
||||
author.addNote(note)
|
||||
|
||||
// Counts the replies
|
||||
replyTo.forEach {
|
||||
it.addReply(note)
|
||||
@@ -279,8 +286,6 @@ object LocalCache {
|
||||
if (event.createdAt > (note.createdAt() ?: 0)) {
|
||||
note.loadEvent(event, author, replyTo)
|
||||
|
||||
author.addNote(note)
|
||||
|
||||
refreshObservers(note)
|
||||
}
|
||||
}
|
||||
@@ -310,9 +315,6 @@ object LocalCache {
|
||||
|
||||
// Log.d("TN", "New Note (${notes.size},${users.size}) ${note.author?.toBestDisplayName()} ${note.event?.content()?.split("\n")?.take(100)} ${formattedDateTime(event.createdAt)}")
|
||||
|
||||
// Prepares user's profile view.
|
||||
author.addNote(note)
|
||||
|
||||
// Counts the replies
|
||||
replyTo.forEach {
|
||||
it.addReply(note)
|
||||
@@ -518,7 +520,7 @@ object LocalCache {
|
||||
if (event.createdAt > (note.createdAt() ?: 0)) {
|
||||
note.loadEvent(event, author, replyTo)
|
||||
|
||||
author.updateAcceptedBadges(note)
|
||||
refreshObservers(note)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -543,6 +545,26 @@ object LocalCache {
|
||||
refreshObservers(note)
|
||||
}
|
||||
|
||||
private fun comsume(event: NNSEvent) {
|
||||
val version = getOrCreateNote(event.id)
|
||||
val note = getOrCreateAddressableNote(event.address())
|
||||
val author = getOrCreateUser(event.pubKey)
|
||||
|
||||
if (version.event == null) {
|
||||
version.loadEvent(event, author, emptyList())
|
||||
version.moveAllReferencesTo(note)
|
||||
}
|
||||
|
||||
// Already processed this event.
|
||||
if (note.event?.id() == event.id()) return
|
||||
|
||||
if (event.createdAt > (note.createdAt() ?: 0)) {
|
||||
note.loadEvent(event, author, emptyList())
|
||||
|
||||
refreshObservers(note)
|
||||
}
|
||||
}
|
||||
|
||||
fun consume(event: AppDefinitionEvent) {
|
||||
val version = getOrCreateNote(event.id)
|
||||
val note = getOrCreateAddressableNote(event.address())
|
||||
@@ -554,7 +576,7 @@ object LocalCache {
|
||||
}
|
||||
|
||||
// Already processed this event.
|
||||
if (note.event != null) return
|
||||
if (note.event?.id() == event.id()) return
|
||||
|
||||
if (event.createdAt > (note.createdAt() ?: 0)) {
|
||||
note.loadEvent(event, author, emptyList())
|
||||
@@ -588,16 +610,6 @@ object LocalCache {
|
||||
// // Log.d("RR", event.toJson())
|
||||
}
|
||||
|
||||
fun consume(event: ContactListEvent) {
|
||||
val user = getOrCreateUser(event.pubKey)
|
||||
|
||||
// avoids processing empty contact lists.
|
||||
if (event.createdAt > (user.latestContactList?.createdAt ?: 0) && !event.tags.isEmpty()) {
|
||||
user.updateContactList(event)
|
||||
// Log.d("CL", "AAA ${user.toBestDisplayName()} ${follows.size}")
|
||||
}
|
||||
}
|
||||
|
||||
fun consume(event: PrivateDmEvent, relay: Relay?): Note {
|
||||
val note = getOrCreateNote(event.id)
|
||||
val author = getOrCreateUser(event.pubKey)
|
||||
@@ -634,8 +646,6 @@ object LocalCache {
|
||||
event.deleteEvents().mapNotNull { notes[it] }.forEach { deleteNote ->
|
||||
// must be the same author
|
||||
if (deleteNote.author?.pubkeyHex == event.pubKey) {
|
||||
deleteNote.author?.removeNote(deleteNote)
|
||||
|
||||
// reverts the add
|
||||
val mentions = deleteNote.event?.tags()?.filter { it.firstOrNull() == "p" }
|
||||
?.mapNotNull { it.getOrNull(1) }?.mapNotNull { checkGetOrCreateUser(it) }
|
||||
@@ -697,9 +707,6 @@ object LocalCache {
|
||||
|
||||
note.loadEvent(event, author, repliesTo)
|
||||
|
||||
// Prepares user's profile view.
|
||||
author.addNote(note)
|
||||
|
||||
// Counts the replies
|
||||
repliesTo.forEach {
|
||||
it.addBoost(note)
|
||||
@@ -722,9 +729,6 @@ object LocalCache {
|
||||
|
||||
note.loadEvent(event, author, repliesTo)
|
||||
|
||||
// Prepares user's profile view.
|
||||
author.addNote(note)
|
||||
|
||||
// Counts the replies
|
||||
repliesTo.forEach {
|
||||
it.addBoost(note)
|
||||
@@ -750,9 +754,6 @@ object LocalCache {
|
||||
|
||||
note.loadEvent(event, author, eventsApproved)
|
||||
|
||||
// Prepares user's profile view.
|
||||
author.addNote(note)
|
||||
|
||||
// Counts the replies
|
||||
repliesTo.forEach {
|
||||
it.addBoost(note)
|
||||
@@ -1072,9 +1073,6 @@ object LocalCache {
|
||||
|
||||
note.loadEvent(event, author, emptyList())
|
||||
|
||||
// Adds to user profile
|
||||
author.addNote(note)
|
||||
|
||||
refreshObservers(note)
|
||||
}
|
||||
|
||||
@@ -1245,24 +1243,26 @@ object LocalCache {
|
||||
}
|
||||
}
|
||||
|
||||
println("PRUNE: ${toBeRemoved.size} messages removed from ${it.value.toBestDisplayName()}. ${it.value.notes.size} kept")
|
||||
if (toBeRemoved.size > 100 || it.value.notes.size > 100) {
|
||||
println("PRUNE: ${toBeRemoved.size} messages removed from ${it.value.toBestDisplayName()}. ${it.value.notes.size} kept")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun pruneHiddenMessages(account: Account) {
|
||||
checkNotInMainThread()
|
||||
|
||||
val toBeRemoved = account.hiddenUsers.map {
|
||||
(users[it]?.notes ?: emptySet())
|
||||
val toBeRemoved = account.hiddenUsers.map { userHex ->
|
||||
(
|
||||
notes.values.filter {
|
||||
it.event?.pubKey() == userHex
|
||||
} + addressables.values.filter {
|
||||
it.event?.pubKey() == userHex
|
||||
}
|
||||
).toSet()
|
||||
}.flatten()
|
||||
|
||||
account.hiddenUsers.forEach {
|
||||
users[it]?.clearNotes()
|
||||
}
|
||||
|
||||
toBeRemoved.forEach {
|
||||
it.author?.removeNote(it)
|
||||
|
||||
// Counts the replies
|
||||
it.replyTo?.forEach { masterNote ->
|
||||
masterNote.removeReply(it)
|
||||
@@ -1355,6 +1355,7 @@ object LocalCache {
|
||||
is LnZapPaymentResponseEvent -> consume(event)
|
||||
is LongTextNoteEvent -> consume(event, relay)
|
||||
is MetadataEvent -> consume(event)
|
||||
is NNSEvent -> comsume(event)
|
||||
is PrivateDmEvent -> consume(event, relay)
|
||||
is PinListEvent -> consume(event)
|
||||
is PeopleListEvent -> consume(event)
|
||||
|
||||
@@ -212,26 +212,58 @@ open class Note(val idHex: String) {
|
||||
}
|
||||
|
||||
@Synchronized
|
||||
private fun innerAddZap(zapRequest: Note, zap: Note?): Boolean {
|
||||
if (zapRequest !in zaps.keys) {
|
||||
zaps = zaps + Pair(zapRequest, zap)
|
||||
return true
|
||||
} else if (zaps[zapRequest] == null) {
|
||||
zaps = zaps + Pair(zapRequest, zap)
|
||||
return true
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
fun addZap(zapRequest: Note, zap: Note?) {
|
||||
checkNotInMainThread()
|
||||
if (zapRequest !in zaps.keys) {
|
||||
zaps = zaps + Pair(zapRequest, zap)
|
||||
liveSet?.zaps?.invalidateData()
|
||||
} else if (zapRequest in zaps.keys && zaps[zapRequest] == null) {
|
||||
zaps = zaps + Pair(zapRequest, zap)
|
||||
liveSet?.zaps?.invalidateData()
|
||||
val inserted = innerAddZap(zapRequest, zap)
|
||||
if (inserted) {
|
||||
liveSet?.zaps?.invalidateData()
|
||||
}
|
||||
} else if (zaps[zapRequest] == null) {
|
||||
val inserted = innerAddZap(zapRequest, zap)
|
||||
if (inserted) {
|
||||
liveSet?.zaps?.invalidateData()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Synchronized
|
||||
private fun innerAddZapPayment(zapPaymentRequest: Note, zapPayment: Note?): Boolean {
|
||||
if (zapPaymentRequest !in zapPayments.keys) {
|
||||
zapPayments = zapPayments + Pair(zapPaymentRequest, zapPayment)
|
||||
return true
|
||||
} else if (zapPayments[zapPaymentRequest] == null) {
|
||||
zapPayments = zapPayments + Pair(zapPaymentRequest, zapPayment)
|
||||
return true
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
fun addZapPayment(zapPaymentRequest: Note, zapPayment: Note?) {
|
||||
checkNotInMainThread()
|
||||
if (zapPaymentRequest !in zapPayments.keys) {
|
||||
zapPayments = zapPayments + Pair(zapPaymentRequest, zapPayment)
|
||||
liveSet?.zaps?.invalidateData()
|
||||
} else if (zapPaymentRequest in zapPayments.keys && zapPayments[zapPaymentRequest] == null) {
|
||||
zapPayments = zapPayments + Pair(zapPaymentRequest, zapPayment)
|
||||
liveSet?.zaps?.invalidateData()
|
||||
val inserted = innerAddZapPayment(zapPaymentRequest, zapPayment)
|
||||
if (inserted) {
|
||||
liveSet?.zaps?.invalidateData()
|
||||
}
|
||||
} else if (zapPayments[zapPaymentRequest] == null) {
|
||||
val inserted = innerAddZapPayment(zapPaymentRequest, zapPayment)
|
||||
if (inserted) {
|
||||
liveSet?.zaps?.invalidateData()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -531,6 +563,15 @@ open class Note(val idHex: String) {
|
||||
liveSet = null
|
||||
}
|
||||
}
|
||||
|
||||
fun isHiddenFor(accountChoices: Account.LiveHiddenUsers): Boolean {
|
||||
if (event == null) return false
|
||||
|
||||
val isSensitive = event?.isSensitive() ?: false
|
||||
return accountChoices.hiddenUsers.contains(author?.pubkeyHex) ||
|
||||
accountChoices.spammers.contains(author?.pubkeyHex) ||
|
||||
(isSensitive && accountChoices.showSensitiveContent == false)
|
||||
}
|
||||
}
|
||||
|
||||
class NoteLiveSet(u: Note) {
|
||||
|
||||
@@ -31,10 +31,6 @@ class User(val pubkeyHex: String) {
|
||||
|
||||
var latestContactList: ContactListEvent? = null
|
||||
var latestBookmarkList: BookmarkListEvent? = null
|
||||
var latestAcceptedBadges: AddressableNote? = null
|
||||
|
||||
var notes = setOf<Note>()
|
||||
private set
|
||||
|
||||
var reports = mapOf<User, Set<Note>>()
|
||||
private set
|
||||
@@ -106,21 +102,6 @@ class User(val pubkeyHex: String) {
|
||||
liveSet?.relays?.invalidateData()
|
||||
}
|
||||
|
||||
fun addNote(note: Note) {
|
||||
if (note !in notes) {
|
||||
notes = notes + note
|
||||
// No need for Listener yet
|
||||
}
|
||||
}
|
||||
|
||||
fun removeNote(note: Note) {
|
||||
notes = notes - note
|
||||
}
|
||||
|
||||
fun clearNotes() {
|
||||
notes = setOf<Note>()
|
||||
}
|
||||
|
||||
fun addReport(note: Note) {
|
||||
val author = note.author ?: return
|
||||
|
||||
@@ -144,13 +125,6 @@ class User(val pubkeyHex: String) {
|
||||
}
|
||||
}
|
||||
|
||||
fun updateAcceptedBadges(note: AddressableNote) {
|
||||
if (latestAcceptedBadges?.idHex != note.idHex) {
|
||||
latestAcceptedBadges = note
|
||||
liveSet?.badges?.invalidateData()
|
||||
}
|
||||
}
|
||||
|
||||
fun addZap(zapRequest: Note, zap: Note?) {
|
||||
if (zapRequest !in zaps.keys) {
|
||||
zaps = zaps + Pair(zapRequest, zap)
|
||||
@@ -301,6 +275,10 @@ class User(val pubkeyHex: String) {
|
||||
return latestContactList?.verifiedFollowTagSet ?: emptySet()
|
||||
}
|
||||
|
||||
fun cachedFollowingCommunitiesSet(): Set<HexKey> {
|
||||
return latestContactList?.verifiedFollowCommunitySet ?: emptySet()
|
||||
}
|
||||
|
||||
fun cachedFollowCount(): Int? {
|
||||
return latestContactList?.verifiedFollowKeySet?.size
|
||||
}
|
||||
@@ -351,7 +329,6 @@ class UserLiveSet(u: User) {
|
||||
val relayInfo: UserLiveData = UserLiveData(u)
|
||||
val metadata: UserLiveData = UserLiveData(u)
|
||||
val zaps: UserLiveData = UserLiveData(u)
|
||||
val badges: UserLiveData = UserLiveData(u)
|
||||
val bookmarks: UserLiveData = UserLiveData(u)
|
||||
|
||||
fun isInUse(): Boolean {
|
||||
@@ -363,7 +340,6 @@ class UserLiveSet(u: User) {
|
||||
relayInfo.hasObservers() ||
|
||||
metadata.hasObservers() ||
|
||||
zaps.hasObservers() ||
|
||||
badges.hasObservers() ||
|
||||
bookmarks.hasObservers()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -44,17 +44,23 @@ object NostrChatroomListDataSource : NostrDataSource("MailBoxFeed") {
|
||||
)
|
||||
)
|
||||
|
||||
fun createMyChannelsFilter() = TypedFilter(
|
||||
types = COMMON_FEED_TYPES, // Metadata comes from any relay
|
||||
filter = JsonFilter(
|
||||
kinds = listOf(ChannelCreateEvent.kind),
|
||||
ids = account.followingChannels.toList(),
|
||||
since = latestEOSEs.users[account.userProfile()]?.followList?.get(chatRoomList)?.relayList
|
||||
)
|
||||
)
|
||||
fun createMyChannelsFilter(): TypedFilter {
|
||||
val followingEvents = account.selectedChatsFollowList()
|
||||
|
||||
fun createLastChannelInfoFilter(): List<TypedFilter> {
|
||||
return account.followingChannels.map {
|
||||
return TypedFilter(
|
||||
types = COMMON_FEED_TYPES, // Metadata comes from any relay
|
||||
filter = JsonFilter(
|
||||
kinds = listOf(ChannelCreateEvent.kind),
|
||||
ids = followingEvents.toList(),
|
||||
since = latestEOSEs.users[account.userProfile()]?.followList?.get(chatRoomList)?.relayList
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
fun createLastChannelInfoFilter(): List<TypedFilter>? {
|
||||
val followingEvents = account.selectedChatsFollowList()
|
||||
|
||||
return followingEvents.map {
|
||||
TypedFilter(
|
||||
types = COMMON_FEED_TYPES, // Metadata comes from any relay
|
||||
filter = JsonFilter(
|
||||
@@ -66,8 +72,10 @@ object NostrChatroomListDataSource : NostrDataSource("MailBoxFeed") {
|
||||
}
|
||||
}
|
||||
|
||||
fun createLastMessageOfEachChannelFilter(): List<TypedFilter> {
|
||||
return account.followingChannels.map {
|
||||
fun createLastMessageOfEachChannelFilter(): List<TypedFilter>? {
|
||||
val followingEvents = account.selectedChatsFollowList()
|
||||
|
||||
return followingEvents.map {
|
||||
TypedFilter(
|
||||
types = setOf(FeedType.PUBLIC_CHATS),
|
||||
filter = JsonFilter(
|
||||
|
||||
@@ -17,13 +17,12 @@ object NostrDiscoveryDataSource : NostrDataSource("DiscoveryFeed") {
|
||||
lateinit var account: Account
|
||||
|
||||
val latestEOSEs = EOSEAccount()
|
||||
val dataSource = "Discovery"
|
||||
|
||||
fun createLiveStreamFilter(): TypedFilter {
|
||||
val follows = account.selectedUsersFollowList(account.defaultDiscoveryFollowList)
|
||||
|
||||
val followKeys = follows?.map {
|
||||
it.substring(0, 6)
|
||||
it.substring(0, 8)
|
||||
}
|
||||
|
||||
return TypedFilter(
|
||||
@@ -32,7 +31,7 @@ object NostrDiscoveryDataSource : NostrDataSource("DiscoveryFeed") {
|
||||
authors = followKeys,
|
||||
kinds = listOf(LiveActivitiesChatMessageEvent.kind, LiveActivitiesEvent.kind),
|
||||
limit = 300,
|
||||
since = latestEOSEs.users[account.userProfile()]?.followList?.get(dataSource)?.relayList
|
||||
since = latestEOSEs.users[account.userProfile()]?.followList?.get(account.defaultDiscoveryFollowList)?.relayList
|
||||
)
|
||||
)
|
||||
}
|
||||
@@ -41,7 +40,7 @@ object NostrDiscoveryDataSource : NostrDataSource("DiscoveryFeed") {
|
||||
val follows = account.selectedUsersFollowList(account.defaultDiscoveryFollowList)
|
||||
|
||||
val followKeys = follows?.map {
|
||||
it.substring(0, 6)
|
||||
it.substring(0, 8)
|
||||
}
|
||||
|
||||
return TypedFilter(
|
||||
@@ -50,7 +49,7 @@ object NostrDiscoveryDataSource : NostrDataSource("DiscoveryFeed") {
|
||||
authors = followKeys,
|
||||
kinds = listOf(ChannelCreateEvent.kind, ChannelMetadataEvent.kind, ChannelMessageEvent.kind),
|
||||
limit = 300,
|
||||
since = latestEOSEs.users[account.userProfile()]?.followList?.get(dataSource)?.relayList
|
||||
since = latestEOSEs.users[account.userProfile()]?.followList?.get(account.defaultDiscoveryFollowList)?.relayList
|
||||
)
|
||||
)
|
||||
}
|
||||
@@ -59,7 +58,7 @@ object NostrDiscoveryDataSource : NostrDataSource("DiscoveryFeed") {
|
||||
val follows = account.selectedUsersFollowList(account.defaultDiscoveryFollowList)
|
||||
|
||||
val followKeys = follows?.map {
|
||||
it.substring(0, 6)
|
||||
it.substring(0, 8)
|
||||
}
|
||||
|
||||
return TypedFilter(
|
||||
@@ -68,7 +67,7 @@ object NostrDiscoveryDataSource : NostrDataSource("DiscoveryFeed") {
|
||||
authors = followKeys,
|
||||
kinds = listOf(CommunityDefinitionEvent.kind, CommunityPostApprovalEvent.kind),
|
||||
limit = 300,
|
||||
since = latestEOSEs.users[account.userProfile()]?.followList?.get(dataSource)?.relayList
|
||||
since = latestEOSEs.users[account.userProfile()]?.followList?.get(account.defaultDiscoveryFollowList)?.relayList
|
||||
)
|
||||
)
|
||||
}
|
||||
@@ -88,7 +87,7 @@ object NostrDiscoveryDataSource : NostrDataSource("DiscoveryFeed") {
|
||||
}.flatten()
|
||||
),
|
||||
limit = 300,
|
||||
since = latestEOSEs.users[account.userProfile()]?.followList?.get(dataSource)?.relayList
|
||||
since = latestEOSEs.users[account.userProfile()]?.followList?.get(account.defaultDiscoveryFollowList)?.relayList
|
||||
)
|
||||
)
|
||||
}
|
||||
@@ -108,7 +107,7 @@ object NostrDiscoveryDataSource : NostrDataSource("DiscoveryFeed") {
|
||||
}.flatten()
|
||||
),
|
||||
limit = 300,
|
||||
since = latestEOSEs.users[account.userProfile()]?.followList?.get(dataSource)?.relayList
|
||||
since = latestEOSEs.users[account.userProfile()]?.followList?.get(account.defaultDiscoveryFollowList)?.relayList
|
||||
)
|
||||
)
|
||||
}
|
||||
@@ -128,12 +127,14 @@ object NostrDiscoveryDataSource : NostrDataSource("DiscoveryFeed") {
|
||||
}.flatten()
|
||||
),
|
||||
limit = 300,
|
||||
since = latestEOSEs.users[account.userProfile()]?.followList?.get(dataSource)?.relayList
|
||||
since = latestEOSEs.users[account.userProfile()]?.followList?.get(account.defaultDiscoveryFollowList)?.relayList
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
val discoveryFeedChannel = requestNewChannel()
|
||||
val discoveryFeedChannel = requestNewChannel() { time, relayUrl ->
|
||||
latestEOSEs.addOrUpdate(account.userProfile(), account.defaultDiscoveryFollowList, relayUrl, time)
|
||||
}
|
||||
|
||||
override fun updateChannelFilters() {
|
||||
discoveryFeedChannel.typedFilters = listOfNotNull(
|
||||
|
||||
@@ -4,6 +4,7 @@ import com.vitorpamplona.amethyst.model.Account
|
||||
import com.vitorpamplona.amethyst.model.UserState
|
||||
import com.vitorpamplona.amethyst.service.model.AudioTrackEvent
|
||||
import com.vitorpamplona.amethyst.service.model.ClassifiedsEvent
|
||||
import com.vitorpamplona.amethyst.service.model.CommunityPostApprovalEvent
|
||||
import com.vitorpamplona.amethyst.service.model.GenericRepostEvent
|
||||
import com.vitorpamplona.amethyst.service.model.HighlightEvent
|
||||
import com.vitorpamplona.amethyst.service.model.LiveActivitiesChatMessageEvent
|
||||
@@ -103,11 +104,37 @@ object NostrHomeDataSource : NostrDataSource("HomeFeed") {
|
||||
)
|
||||
}
|
||||
|
||||
fun createFollowCommunitiesFilter(): TypedFilter? {
|
||||
val communitiesToLoad = account.selectedCommunitiesFollowList(account.defaultHomeFollowList) ?: emptySet()
|
||||
|
||||
if (communitiesToLoad.isEmpty()) return null
|
||||
|
||||
return TypedFilter(
|
||||
types = setOf(FeedType.FOLLOWS),
|
||||
filter = JsonFilter(
|
||||
kinds = listOf(
|
||||
TextNoteEvent.kind,
|
||||
LongTextNoteEvent.kind,
|
||||
ClassifiedsEvent.kind,
|
||||
HighlightEvent.kind,
|
||||
AudioTrackEvent.kind,
|
||||
PinListEvent.kind,
|
||||
CommunityPostApprovalEvent.kind
|
||||
),
|
||||
tags = mapOf(
|
||||
"a" to communitiesToLoad.toList()
|
||||
),
|
||||
limit = 100,
|
||||
since = latestEOSEs.users[account.userProfile()]?.followList?.get(account.defaultHomeFollowList)?.relayList
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
val followAccountChannel = requestNewChannel { time, relayUrl ->
|
||||
latestEOSEs.addOrUpdate(account.userProfile(), account.defaultHomeFollowList, relayUrl, time)
|
||||
}
|
||||
|
||||
override fun updateChannelFilters() {
|
||||
followAccountChannel.typedFilters = listOfNotNull(createFollowAccountsFilter(), createFollowTagsFilter()).ifEmpty { null }
|
||||
followAccountChannel.typedFilters = listOfNotNull(createFollowAccountsFilter(), createFollowCommunitiesFilter(), createFollowTagsFilter()).ifEmpty { null }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ package com.vitorpamplona.amethyst.service
|
||||
import com.vitorpamplona.amethyst.model.Account
|
||||
import com.vitorpamplona.amethyst.service.model.FileHeaderEvent
|
||||
import com.vitorpamplona.amethyst.service.model.FileStorageHeaderEvent
|
||||
import com.vitorpamplona.amethyst.service.relays.EOSEAccount
|
||||
import com.vitorpamplona.amethyst.service.relays.FeedType
|
||||
import com.vitorpamplona.amethyst.service.relays.JsonFilter
|
||||
import com.vitorpamplona.amethyst.service.relays.TypedFilter
|
||||
@@ -10,6 +11,8 @@ import com.vitorpamplona.amethyst.service.relays.TypedFilter
|
||||
object NostrVideoDataSource : NostrDataSource("VideoFeed") {
|
||||
lateinit var account: Account
|
||||
|
||||
val latestEOSEs = EOSEAccount()
|
||||
|
||||
fun createContextualFilter(): TypedFilter? {
|
||||
val follows = account.selectedUsersFollowList(account.defaultStoriesFollowList)
|
||||
|
||||
@@ -22,7 +25,8 @@ object NostrVideoDataSource : NostrDataSource("VideoFeed") {
|
||||
filter = JsonFilter(
|
||||
authors = followKeys,
|
||||
kinds = listOf(FileHeaderEvent.kind, FileStorageHeaderEvent.kind),
|
||||
limit = 200
|
||||
limit = 200,
|
||||
since = latestEOSEs.users[account.userProfile()]?.followList?.get(account.defaultStoriesFollowList)?.relayList
|
||||
)
|
||||
)
|
||||
}
|
||||
@@ -41,12 +45,15 @@ object NostrVideoDataSource : NostrDataSource("VideoFeed") {
|
||||
listOf(it, it.lowercase(), it.uppercase(), it.capitalize())
|
||||
}.flatten()
|
||||
),
|
||||
limit = 100
|
||||
limit = 100,
|
||||
since = latestEOSEs.users[account.userProfile()]?.followList?.get(account.defaultStoriesFollowList)?.relayList
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
val videoFeedChannel = requestNewChannel()
|
||||
val videoFeedChannel = requestNewChannel() { time, relayUrl ->
|
||||
latestEOSEs.addOrUpdate(account.userProfile(), account.defaultStoriesFollowList, relayUrl, time)
|
||||
}
|
||||
|
||||
override fun updateChannelFilters() {
|
||||
videoFeedChannel.typedFilters = listOfNotNull(createContextualFilter(), createFollowTagsFilter()).ifEmpty { null }
|
||||
|
||||
@@ -11,7 +11,12 @@ object ConnectivityStatus {
|
||||
val isOnWifi: MutableState<Boolean> = onWifi
|
||||
|
||||
fun updateConnectivityStatus(isOnMobileData: Boolean, isOnWifi: Boolean) {
|
||||
onMobileData.value = isOnMobileData
|
||||
onWifi.value = isOnWifi
|
||||
if (onMobileData.value != isOnMobileData) {
|
||||
onMobileData.value = isOnMobileData
|
||||
}
|
||||
|
||||
if (onWifi.value != isOnWifi) {
|
||||
onWifi.value = isOnWifi
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -25,5 +25,6 @@ class BadgeProfilesEvent(
|
||||
|
||||
companion object {
|
||||
const val kind = 30008
|
||||
const val standardDTAg = "profile_badges"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -39,6 +39,10 @@ class ContactListEvent(
|
||||
unverifiedFollowTagSet().map { it.lowercase() }.toSet()
|
||||
}
|
||||
|
||||
val verifiedFollowCommunitySet: Set<String> by lazy {
|
||||
unverifiedFollowAddressSet().toSet()
|
||||
}
|
||||
|
||||
val verifiedFollowKeySetAndMe: Set<HexKey> by lazy {
|
||||
verifiedFollowKeySet + pubKey
|
||||
}
|
||||
@@ -46,6 +50,8 @@ class ContactListEvent(
|
||||
fun unverifiedFollowKeySet() = tags.filter { it[0] == "p" }.mapNotNull { it.getOrNull(1) }
|
||||
fun unverifiedFollowTagSet() = tags.filter { it[0] == "t" }.mapNotNull { it.getOrNull(1) }
|
||||
|
||||
fun unverifiedFollowAddressSet() = tags.filter { it[0] == "a" }.mapNotNull { it.getOrNull(1) }
|
||||
|
||||
fun follows() = tags.filter { it[0] == "p" }.mapNotNull {
|
||||
try {
|
||||
Contact(decodePublicKey(it[1]).toHexKey(), it.getOrNull(2))
|
||||
@@ -73,14 +79,22 @@ class ContactListEvent(
|
||||
companion object {
|
||||
const val kind = 3
|
||||
|
||||
fun create(follows: List<Contact>, followTags: List<String>, relayUse: Map<String, ReadWrite>?, privateKey: ByteArray, createdAt: Long = TimeUtils.now()): ContactListEvent {
|
||||
fun createFromScratch(
|
||||
followUsers: List<Contact>,
|
||||
followTags: List<String>,
|
||||
followCommunities: List<ATag>,
|
||||
followEvents: List<String>,
|
||||
relayUse: Map<String, ReadWrite>?,
|
||||
privateKey: ByteArray,
|
||||
createdAt: Long = TimeUtils.now()
|
||||
): ContactListEvent {
|
||||
val content = if (relayUse != null) {
|
||||
gson.toJson(relayUse)
|
||||
} else {
|
||||
""
|
||||
}
|
||||
val pubKey = Utils.pubkeyCreate(privateKey).toHexKey()
|
||||
val tags = follows.map {
|
||||
|
||||
val tags = followUsers.map {
|
||||
if (it.relayUri != null) {
|
||||
listOf("p", it.pubKeyHex, it.relayUri)
|
||||
} else {
|
||||
@@ -88,8 +102,129 @@ class ContactListEvent(
|
||||
}
|
||||
} + followTags.map {
|
||||
listOf("t", it)
|
||||
} + followEvents.map {
|
||||
listOf("e", it)
|
||||
} + followCommunities.map {
|
||||
if (it.relay != null) {
|
||||
listOf("a", it.toTag(), it.relay)
|
||||
} else {
|
||||
listOf("a", it.toTag())
|
||||
}
|
||||
}
|
||||
|
||||
return create(
|
||||
content = content,
|
||||
tags = tags,
|
||||
privateKey = privateKey,
|
||||
createdAt = createdAt
|
||||
)
|
||||
}
|
||||
|
||||
fun followUser(earlierVersion: ContactListEvent, pubKeyHex: String, privateKey: ByteArray, createdAt: Long = TimeUtils.now()): ContactListEvent {
|
||||
if (earlierVersion.isTaggedUser(pubKeyHex)) return earlierVersion
|
||||
|
||||
return create(
|
||||
content = earlierVersion.content,
|
||||
tags = earlierVersion.tags.plus(element = listOf("p", pubKeyHex)),
|
||||
privateKey = privateKey,
|
||||
createdAt = createdAt
|
||||
)
|
||||
}
|
||||
|
||||
fun unfollowUser(earlierVersion: ContactListEvent, pubKeyHex: String, privateKey: ByteArray, createdAt: Long = TimeUtils.now()): ContactListEvent {
|
||||
if (!earlierVersion.isTaggedUser(pubKeyHex)) return earlierVersion
|
||||
|
||||
return create(
|
||||
content = earlierVersion.content,
|
||||
tags = earlierVersion.tags.filter { it.size > 1 && it[1] != pubKeyHex },
|
||||
privateKey = privateKey,
|
||||
createdAt = createdAt
|
||||
)
|
||||
}
|
||||
|
||||
fun followHashtag(earlierVersion: ContactListEvent, hashtag: String, privateKey: ByteArray, createdAt: Long = TimeUtils.now()): ContactListEvent {
|
||||
if (earlierVersion.isTaggedHash(hashtag)) return earlierVersion
|
||||
|
||||
return create(
|
||||
content = earlierVersion.content,
|
||||
tags = earlierVersion.tags.plus(element = listOf("t", hashtag)),
|
||||
privateKey = privateKey,
|
||||
createdAt = createdAt
|
||||
)
|
||||
}
|
||||
|
||||
fun unfollowHashtag(earlierVersion: ContactListEvent, hashtag: String, privateKey: ByteArray, createdAt: Long = TimeUtils.now()): ContactListEvent {
|
||||
if (!earlierVersion.isTaggedHash(hashtag)) return earlierVersion
|
||||
|
||||
return create(
|
||||
content = earlierVersion.content,
|
||||
tags = earlierVersion.tags.filter { it.size > 1 && it[1] != hashtag },
|
||||
privateKey = privateKey,
|
||||
createdAt = createdAt
|
||||
)
|
||||
}
|
||||
|
||||
fun followEvent(earlierVersion: ContactListEvent, idHex: String, privateKey: ByteArray, createdAt: Long = TimeUtils.now()): ContactListEvent {
|
||||
if (earlierVersion.isTaggedEvent(idHex)) return earlierVersion
|
||||
|
||||
return create(
|
||||
content = earlierVersion.content,
|
||||
tags = earlierVersion.tags.plus(element = listOf("e", idHex)),
|
||||
privateKey = privateKey,
|
||||
createdAt = createdAt
|
||||
)
|
||||
}
|
||||
|
||||
fun unfollowEvent(earlierVersion: ContactListEvent, idHex: String, privateKey: ByteArray, createdAt: Long = TimeUtils.now()): ContactListEvent {
|
||||
if (!earlierVersion.isTaggedEvent(idHex)) return earlierVersion
|
||||
|
||||
return create(
|
||||
content = earlierVersion.content,
|
||||
tags = earlierVersion.tags.filter { it.size > 1 && it[1] != idHex },
|
||||
privateKey = privateKey,
|
||||
createdAt = createdAt
|
||||
)
|
||||
}
|
||||
|
||||
fun followAddressableEvent(earlierVersion: ContactListEvent, aTag: ATag, privateKey: ByteArray, createdAt: Long = TimeUtils.now()): ContactListEvent {
|
||||
if (earlierVersion.isTaggedAddressableNote(aTag.toTag())) return earlierVersion
|
||||
|
||||
return create(
|
||||
content = earlierVersion.content,
|
||||
tags = earlierVersion.tags.plus(element = listOfNotNull("a", aTag.toTag(), aTag.relay)),
|
||||
privateKey = privateKey,
|
||||
createdAt = createdAt
|
||||
)
|
||||
}
|
||||
|
||||
fun unfollowAddressableEvent(earlierVersion: ContactListEvent, aTag: ATag, privateKey: ByteArray, createdAt: Long = TimeUtils.now()): ContactListEvent {
|
||||
if (!earlierVersion.isTaggedAddressableNote(aTag.toTag())) return earlierVersion
|
||||
|
||||
return create(
|
||||
content = earlierVersion.content,
|
||||
tags = earlierVersion.tags.filter { it.size > 1 && it[1] != aTag.toTag() },
|
||||
privateKey = privateKey,
|
||||
createdAt = createdAt
|
||||
)
|
||||
}
|
||||
|
||||
fun updateRelayList(earlierVersion: ContactListEvent, relayUse: Map<String, ReadWrite>?, privateKey: ByteArray, createdAt: Long = TimeUtils.now()): ContactListEvent {
|
||||
val content = if (relayUse != null) {
|
||||
gson.toJson(relayUse)
|
||||
} else {
|
||||
""
|
||||
}
|
||||
|
||||
return create(
|
||||
content = content,
|
||||
tags = earlierVersion.tags,
|
||||
privateKey = privateKey,
|
||||
createdAt = createdAt
|
||||
)
|
||||
}
|
||||
|
||||
fun create(content: String, tags: List<List<String>>, privateKey: ByteArray, createdAt: Long = TimeUtils.now()): ContactListEvent {
|
||||
val pubKey = Utils.pubkeyCreate(privateKey).toHexKey()
|
||||
val id = generateId(pubKey, createdAt, kind, tags, content)
|
||||
val sig = Utils.sign(id, privateKey)
|
||||
return ContactListEvent(id.toHexKey(), pubKey, createdAt, tags, content, sig.toHexKey())
|
||||
|
||||
@@ -44,10 +44,10 @@ open class Event(
|
||||
|
||||
fun hasAnyTaggedUser() = tags.any { it.size > 1 && it[0] == "p" }
|
||||
|
||||
fun taggedUsers() = tags.filter { it.size > 1 && it[0] == "p" }.map { it[1] }
|
||||
fun taggedEvents() = tags.filter { it.size > 1 && it[0] == "e" }.map { it[1] }
|
||||
override fun taggedUsers() = tags.filter { it.size > 1 && it[0] == "p" }.map { it[1] }
|
||||
override fun taggedEvents() = tags.filter { it.size > 1 && it[0] == "e" }.map { it[1] }
|
||||
|
||||
fun taggedUrls() = tags.filter { it.size > 1 && it[0] == "r" }.map { it[1] }
|
||||
override fun taggedUrls() = tags.filter { it.size > 1 && it[0] == "r" }.map { it[1] }
|
||||
|
||||
override fun taggedEmojis() = tags.filter { it.size > 2 && it[0] == "emoji" }.map { EmojiUrl(it[1], it[2]) }
|
||||
|
||||
@@ -63,7 +63,7 @@ open class Event(
|
||||
|
||||
override fun zapAddress() = tags.firstOrNull { it.size > 1 && it[0] == "zap" }?.get(1)
|
||||
|
||||
fun taggedAddresses() = tags.filter { it.size > 1 && it[0] == "a" }.mapNotNull {
|
||||
override fun taggedAddresses() = tags.filter { it.size > 1 && it[0] == "a" }.mapNotNull {
|
||||
val aTagValue = it[1]
|
||||
val relay = it.getOrNull(2)
|
||||
|
||||
@@ -76,8 +76,12 @@ open class Event(
|
||||
|
||||
override fun isTaggedUser(idHex: String) = tags.any { it.size > 1 && it[0] == "p" && it[1] == idHex }
|
||||
|
||||
override fun isTaggedEvent(idHex: String) = tags.any { it.size > 1 && it[0] == "e" && it[1] == idHex }
|
||||
|
||||
override fun isTaggedAddressableNote(idHex: String) = tags.any { it.size > 1 && it[0] == "a" && it[1] == idHex }
|
||||
|
||||
override fun isTaggedAddressableNotes(idHexes: Set<String>) = tags.any { it.size > 1 && it[0] == "a" && it[1] in idHexes }
|
||||
|
||||
override fun isTaggedHash(hashtag: String) = tags.any { it.size > 1 && it[0] == "t" && it[1].equals(hashtag, true) }
|
||||
override fun isTaggedHashes(hashtags: Set<String>) = tags.any { it.size > 1 && it[0] == "t" && it[1].lowercase() in hashtags }
|
||||
override fun firstIsTaggedHashes(hashtags: Set<String>) = tags.firstOrNull { it.size > 1 && it[0] == "t" && it[1].lowercase() in hashtags }?.getOrNull(1)
|
||||
@@ -288,6 +292,7 @@ open class Event(
|
||||
LnZapRequestEvent.kind -> LnZapRequestEvent(id, pubKey, createdAt, tags, content, sig)
|
||||
LongTextNoteEvent.kind -> LongTextNoteEvent(id, pubKey, createdAt, tags, content, sig)
|
||||
MetadataEvent.kind -> MetadataEvent(id, pubKey, createdAt, tags, content, sig)
|
||||
NNSEvent.kind -> NNSEvent(id, pubKey, createdAt, tags, content, sig)
|
||||
PeopleListEvent.kind -> PeopleListEvent(id, pubKey, createdAt, tags, content, sig)
|
||||
PinListEvent.kind -> PinListEvent(id, pubKey, createdAt, tags, content, sig)
|
||||
PollNoteEvent.kind -> PollNoteEvent(id, pubKey, createdAt, tags, content, sig)
|
||||
|
||||
@@ -27,7 +27,11 @@ interface EventInterface {
|
||||
fun hasValidSignature(): Boolean
|
||||
|
||||
fun isTaggedUser(idHex: String): Boolean
|
||||
|
||||
fun isTaggedEvent(idHex: String): Boolean
|
||||
|
||||
fun isTaggedAddressableNote(idHex: String): Boolean
|
||||
fun isTaggedAddressableNotes(idHexes: Set<String>): Boolean
|
||||
|
||||
fun isTaggedHash(hashtag: String): Boolean
|
||||
fun isTaggedHashes(hashtags: Set<String>): Boolean
|
||||
@@ -46,6 +50,11 @@ interface EventInterface {
|
||||
fun isSensitive(): Boolean
|
||||
fun zapraiserAmount(): Long?
|
||||
|
||||
fun taggedAddresses(): List<ATag>
|
||||
fun taggedUsers(): List<HexKey>
|
||||
fun taggedEvents(): List<HexKey>
|
||||
fun taggedUrls(): List<String>
|
||||
|
||||
fun taggedEmojis(): List<EmojiUrl>
|
||||
fun matchTag1With(text: String): Boolean
|
||||
}
|
||||
|
||||
@@ -54,7 +54,13 @@ abstract class GeneralListEvent(
|
||||
return privateTagsCache
|
||||
}
|
||||
|
||||
fun privateTagsOrEmpty(privKey: ByteArray): List<List<String>> {
|
||||
return privateTags(privKey) ?: emptyList()
|
||||
}
|
||||
|
||||
fun privateTaggedUsers(privKey: ByteArray) = privateTags(privKey)?.filter { it.size > 1 && it[0] == "p" }?.map { it[1] }
|
||||
|
||||
fun privateHashtags(privKey: ByteArray) = privateTags(privKey)?.filter { it.size > 1 && it[0] == "t" }?.map { it[1] }
|
||||
fun privateTaggedEvents(privKey: ByteArray) = privateTags(privKey)?.filter { it.size > 1 && it[0] == "e" }?.map { it[1] }
|
||||
fun privateTaggedAddresses(privKey: ByteArray) = privateTags(privKey)?.filter { it.firstOrNull() == "a" }?.mapNotNull {
|
||||
val aTagValue = it.getOrNull(1)
|
||||
@@ -90,5 +96,16 @@ abstract class GeneralListEvent(
|
||||
pubKey
|
||||
)
|
||||
}
|
||||
|
||||
fun encryptTags(
|
||||
privateTags: List<List<String>>? = null,
|
||||
privateKey: ByteArray
|
||||
): String {
|
||||
return Utils.encrypt(
|
||||
msg = gson.toJson(privateTags),
|
||||
privateKey = privateKey,
|
||||
pubKey = Utils.pubkeyCreate(privateKey)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -20,6 +20,8 @@ class HighlightEvent(
|
||||
fun author() = taggedUsers().firstOrNull()
|
||||
fun quote() = content
|
||||
|
||||
fun inPost() = taggedAddresses().firstOrNull()
|
||||
|
||||
companion object {
|
||||
const val kind = 9802
|
||||
|
||||
|
||||
@@ -14,8 +14,6 @@ interface LnZapEventInterface : EventInterface {
|
||||
|
||||
fun zappedRequestAuthor(): String?
|
||||
|
||||
fun taggedAddresses(): List<ATag>
|
||||
|
||||
fun amount(): BigDecimal?
|
||||
|
||||
fun containedPost(): Event?
|
||||
|
||||
@@ -147,8 +147,8 @@ class MetadataEvent(
|
||||
UserMetadata::class.java
|
||||
)
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
Log.w("MT", "Content Parse Error ${e.localizedMessage} $content")
|
||||
// e.printStackTrace()
|
||||
Log.w("MT", "Content Parse Error: ${e.localizedMessage} $content")
|
||||
null
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,40 @@
|
||||
package com.vitorpamplona.amethyst.service.model
|
||||
|
||||
import androidx.compose.runtime.Immutable
|
||||
import com.vitorpamplona.amethyst.model.HexKey
|
||||
import com.vitorpamplona.amethyst.model.TimeUtils
|
||||
import com.vitorpamplona.amethyst.model.toHexKey
|
||||
import nostr.postr.Utils
|
||||
|
||||
@Immutable
|
||||
class NNSEvent(
|
||||
id: HexKey,
|
||||
pubKey: HexKey,
|
||||
createdAt: Long,
|
||||
tags: List<List<String>>,
|
||||
content: String,
|
||||
sig: HexKey
|
||||
) : Event(id, pubKey, createdAt, kind, tags, content, sig), AddressableEvent {
|
||||
|
||||
override fun dTag() = tags.firstOrNull { it.size > 1 && it[0] == "d" }?.get(1) ?: ""
|
||||
override fun address() = ATag(kind, pubKey, dTag(), null)
|
||||
|
||||
fun ip4() = tags.firstOrNull { it.size > 1 && it[0] == "ip4" }?.get(1)
|
||||
fun ip6() = tags.firstOrNull { it.size > 1 && it[0] == "ip6" }?.get(1)
|
||||
fun version() = tags.firstOrNull { it.size > 1 && it[0] == "version" }?.get(1)
|
||||
|
||||
companion object {
|
||||
const val kind = 30053
|
||||
|
||||
fun create(
|
||||
privateKey: ByteArray,
|
||||
createdAt: Long = TimeUtils.now()
|
||||
): NNSEvent {
|
||||
val tags = mutableListOf<List<String>>()
|
||||
val pubKey = Utils.pubkeyCreate(privateKey).toHexKey()
|
||||
val id = generateId(pubKey, createdAt, kind, tags, "")
|
||||
val sig = Utils.sign(id, privateKey)
|
||||
return NNSEvent(id.toHexKey(), pubKey, createdAt, tags, "", sig.toHexKey())
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -4,6 +4,9 @@ import androidx.compose.runtime.Immutable
|
||||
import com.vitorpamplona.amethyst.model.HexKey
|
||||
import com.vitorpamplona.amethyst.model.TimeUtils
|
||||
import com.vitorpamplona.amethyst.model.toHexKey
|
||||
import kotlinx.collections.immutable.ImmutableSet
|
||||
import kotlinx.collections.immutable.persistentSetOf
|
||||
import kotlinx.collections.immutable.toImmutableSet
|
||||
import nostr.postr.Utils
|
||||
|
||||
@Immutable
|
||||
@@ -15,42 +18,133 @@ class PeopleListEvent(
|
||||
content: String,
|
||||
sig: HexKey
|
||||
) : GeneralListEvent(id, pubKey, createdAt, kind, tags, content, sig) {
|
||||
var publicAndPrivateUserCache: ImmutableSet<HexKey>? = null
|
||||
|
||||
fun publicAndPrivateUsers(privateKey: ByteArray?): ImmutableSet<HexKey> {
|
||||
publicAndPrivateUserCache?.let {
|
||||
return it
|
||||
}
|
||||
|
||||
val privateUserList = privateKey?.let {
|
||||
privateTagsOrEmpty(privKey = it).filter { it.size > 1 && it[0] == "p" }.map { it[1] }.toSet()
|
||||
} ?: emptySet()
|
||||
val publicUserList = tags.filter { it.size > 1 && it[0] == "p" }.map { it[1] }.toSet()
|
||||
|
||||
publicAndPrivateUserCache = (privateUserList + publicUserList).toImmutableSet()
|
||||
|
||||
return publicAndPrivateUserCache ?: persistentSetOf()
|
||||
}
|
||||
|
||||
fun isTaggedUser(idHex: String, isPrivate: Boolean, privateKey: ByteArray): Boolean {
|
||||
return if (isPrivate) {
|
||||
privateTagsOrEmpty(privKey = privateKey).any { it.size > 1 && it[0] == "p" && it[1] == idHex }
|
||||
} else {
|
||||
isTaggedUser(idHex)
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
const val kind = 30000
|
||||
const val blockList = "mute"
|
||||
|
||||
fun create(
|
||||
name: String = "",
|
||||
|
||||
events: List<String>? = null,
|
||||
users: List<String>? = null,
|
||||
addresses: List<ATag>? = null,
|
||||
|
||||
privEvents: List<String>? = null,
|
||||
privUsers: List<String>? = null,
|
||||
privAddresses: List<ATag>? = null,
|
||||
|
||||
privateKey: ByteArray,
|
||||
createdAt: Long = TimeUtils.now()
|
||||
): PeopleListEvent {
|
||||
val pubKey = Utils.pubkeyCreate(privateKey)
|
||||
val content = createPrivateTags(privEvents, privUsers, privAddresses, privateKey, pubKey)
|
||||
|
||||
val tags = mutableListOf<List<String>>()
|
||||
tags.add(listOf("d", name))
|
||||
|
||||
events?.forEach {
|
||||
tags.add(listOf("e", it))
|
||||
}
|
||||
users?.forEach {
|
||||
tags.add(listOf("p", it))
|
||||
}
|
||||
addresses?.forEach {
|
||||
tags.add(listOf("a", it.toTag()))
|
||||
fun createListWithUser(name: String, pubKeyHex: String, isPrivate: Boolean, privateKey: ByteArray, createdAt: Long = TimeUtils.now()): PeopleListEvent {
|
||||
return if (isPrivate) {
|
||||
create(
|
||||
content = encryptTags(listOf(listOf("p", pubKeyHex)), privateKey),
|
||||
tags = listOf(listOf("d", name)),
|
||||
privateKey = privateKey,
|
||||
createdAt = createdAt
|
||||
)
|
||||
} else {
|
||||
create(
|
||||
content = "",
|
||||
tags = listOf(listOf("d", name), listOf("p", pubKeyHex)),
|
||||
privateKey = privateKey,
|
||||
createdAt = createdAt
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
val id = generateId(pubKey.toHexKey(), createdAt, kind, tags, content)
|
||||
fun addUsers(earlierVersion: PeopleListEvent, listPubKeyHex: List<String>, isPrivate: Boolean, privateKey: ByteArray, createdAt: Long = TimeUtils.now()): PeopleListEvent {
|
||||
return if (isPrivate) {
|
||||
create(
|
||||
content = encryptTags(
|
||||
privateTags = earlierVersion.privateTagsOrEmpty(privKey = privateKey).plus(
|
||||
listPubKeyHex.map {
|
||||
listOf("p", it)
|
||||
}
|
||||
),
|
||||
privateKey = privateKey
|
||||
),
|
||||
tags = earlierVersion.tags,
|
||||
privateKey = privateKey,
|
||||
createdAt = createdAt
|
||||
)
|
||||
} else {
|
||||
create(
|
||||
content = earlierVersion.content,
|
||||
tags = earlierVersion.tags.plus(
|
||||
listPubKeyHex.map {
|
||||
listOf("p", it)
|
||||
}
|
||||
),
|
||||
privateKey = privateKey,
|
||||
createdAt = createdAt
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fun addUser(earlierVersion: PeopleListEvent, pubKeyHex: String, isPrivate: Boolean, privateKey: ByteArray, createdAt: Long = TimeUtils.now()): PeopleListEvent {
|
||||
if (earlierVersion.isTaggedUser(pubKeyHex, isPrivate, privateKey)) return earlierVersion
|
||||
|
||||
return if (isPrivate) {
|
||||
create(
|
||||
content = encryptTags(
|
||||
privateTags = earlierVersion.privateTagsOrEmpty(privKey = privateKey).plus(element = listOf("p", pubKeyHex)),
|
||||
privateKey = privateKey
|
||||
),
|
||||
tags = earlierVersion.tags,
|
||||
privateKey = privateKey,
|
||||
createdAt = createdAt
|
||||
)
|
||||
} else {
|
||||
create(
|
||||
content = earlierVersion.content,
|
||||
tags = earlierVersion.tags.plus(element = listOf("p", pubKeyHex)),
|
||||
privateKey = privateKey,
|
||||
createdAt = createdAt
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fun removeUser(earlierVersion: PeopleListEvent, pubKeyHex: String, isPrivate: Boolean, privateKey: ByteArray, createdAt: Long = TimeUtils.now()): PeopleListEvent {
|
||||
if (!earlierVersion.isTaggedUser(pubKeyHex, isPrivate, privateKey)) return earlierVersion
|
||||
|
||||
return if (isPrivate) {
|
||||
create(
|
||||
content = encryptTags(
|
||||
privateTags = earlierVersion.privateTagsOrEmpty(privKey = privateKey).filter { it.size > 1 && it[1] != pubKeyHex },
|
||||
privateKey = privateKey
|
||||
),
|
||||
tags = earlierVersion.tags.filter { it.size > 1 && it[1] != pubKeyHex },
|
||||
privateKey = privateKey,
|
||||
createdAt = createdAt
|
||||
)
|
||||
} else {
|
||||
create(
|
||||
content = earlierVersion.content,
|
||||
tags = earlierVersion.tags.filter { it.size > 1 && it[1] != pubKeyHex },
|
||||
privateKey = privateKey,
|
||||
createdAt = createdAt
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fun create(content: String, tags: List<List<String>>, privateKey: ByteArray, createdAt: Long = TimeUtils.now()): PeopleListEvent {
|
||||
val pubKey = Utils.pubkeyCreate(privateKey).toHexKey()
|
||||
val id = generateId(pubKey, createdAt, kind, tags, content)
|
||||
val sig = Utils.sign(id, privateKey)
|
||||
return PeopleListEvent(id.toHexKey(), pubKey.toHexKey(), createdAt, tags, content, sig.toHexKey())
|
||||
return PeopleListEvent(id.toHexKey(), pubKey, createdAt, tags, content, sig.toHexKey())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -139,14 +139,18 @@ class MainActivity : AppCompatActivity() {
|
||||
networkCapabilities: NetworkCapabilities
|
||||
) {
|
||||
super.onCapabilitiesChanged(network, networkCapabilities)
|
||||
val hasMobileData = networkCapabilities.hasTransport(NetworkCapabilities.TRANSPORT_CELLULAR)
|
||||
val hasWifi = networkCapabilities.hasTransport(NetworkCapabilities.TRANSPORT_WIFI)
|
||||
Log.d("NETWORKCALLBACK", "onCapabilitiesChanged: hasMobileData $hasMobileData")
|
||||
Log.d("NETWORKCALLBACK", "onCapabilitiesChanged: hasWifi $hasWifi")
|
||||
ConnectivityStatus.updateConnectivityStatus(
|
||||
networkCapabilities.hasTransport(NetworkCapabilities.TRANSPORT_CELLULAR),
|
||||
networkCapabilities.hasTransport(NetworkCapabilities.TRANSPORT_WIFI)
|
||||
)
|
||||
|
||||
GlobalScope.launch(Dispatchers.IO) {
|
||||
val hasMobileData =
|
||||
networkCapabilities.hasTransport(NetworkCapabilities.TRANSPORT_CELLULAR)
|
||||
val hasWifi = networkCapabilities.hasTransport(NetworkCapabilities.TRANSPORT_WIFI)
|
||||
Log.d("NETWORKCALLBACK", "onCapabilitiesChanged: hasMobileData $hasMobileData")
|
||||
Log.d("NETWORKCALLBACK", "onCapabilitiesChanged: hasWifi $hasWifi")
|
||||
ConnectivityStatus.updateConnectivityStatus(
|
||||
networkCapabilities.hasTransport(NetworkCapabilities.TRANSPORT_CELLULAR),
|
||||
networkCapabilities.hasTransport(NetworkCapabilities.TRANSPORT_WIFI)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// lost network connection
|
||||
|
||||
@@ -129,7 +129,8 @@ fun isNIP94Server(selectedServer: ServersAvailable?): Boolean {
|
||||
return selectedServer == ServersAvailable.NOSTRIMG_NIP_94 ||
|
||||
// selectedServer == ServersAvailable.IMGUR_NIP_94 ||
|
||||
selectedServer == ServersAvailable.NOSTR_BUILD_NIP_94 ||
|
||||
selectedServer == ServersAvailable.NOSTRFILES_DEV_NIP_94
|
||||
selectedServer == ServersAvailable.NOSTRFILES_DEV_NIP_94 ||
|
||||
selectedServer == ServersAvailable.NOSTRCHECK_ME_NIP_94
|
||||
}
|
||||
|
||||
@Composable
|
||||
@@ -139,6 +140,7 @@ fun ImageVideoPost(postViewModel: NewMediaModel, accountViewModel: AccountViewMo
|
||||
Triple(ServersAvailable.NOSTRIMG_NIP_94, stringResource(id = R.string.upload_server_nostrimg_nip94), stringResource(id = R.string.upload_server_nostrimg_nip94_explainer)),
|
||||
Triple(ServersAvailable.NOSTR_BUILD_NIP_94, stringResource(id = R.string.upload_server_nostrbuild_nip94), stringResource(id = R.string.upload_server_nostrbuild_nip94_explainer)),
|
||||
Triple(ServersAvailable.NOSTRFILES_DEV_NIP_94, stringResource(id = R.string.upload_server_nostrfilesdev_nip94), stringResource(id = R.string.upload_server_nostrfilesdev_nip94_explainer)),
|
||||
Triple(ServersAvailable.NOSTRCHECK_ME_NIP_94, stringResource(id = R.string.upload_server_nostrcheckme_nip94), stringResource(id = R.string.upload_server_nostrcheckme_nip94_explainer)),
|
||||
Triple(ServersAvailable.NIP95, stringResource(id = R.string.upload_server_relays_nip95), stringResource(id = R.string.upload_server_relays_nip95_explainer))
|
||||
)
|
||||
|
||||
|
||||
@@ -0,0 +1,157 @@
|
||||
package com.vitorpamplona.amethyst.ui.buttons
|
||||
|
||||
import android.Manifest
|
||||
import android.net.Uri
|
||||
import android.os.Build
|
||||
import androidx.compose.animation.core.animateFloatAsState
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.PaddingValues
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.shape.CircleShape
|
||||
import androidx.compose.material.ButtonDefaults
|
||||
import androidx.compose.material.CircularProgressIndicator
|
||||
import androidx.compose.material.Icon
|
||||
import androidx.compose.material.MaterialTheme
|
||||
import androidx.compose.material.OutlinedButton
|
||||
import androidx.compose.material.ProgressIndicatorDefaults
|
||||
import androidx.compose.material.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.rememberCoroutineScope
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import androidx.lifecycle.viewmodel.compose.viewModel
|
||||
import com.google.accompanist.permissions.ExperimentalPermissionsApi
|
||||
import com.google.accompanist.permissions.isGranted
|
||||
import com.google.accompanist.permissions.rememberPermissionState
|
||||
import com.vitorpamplona.amethyst.R
|
||||
import com.vitorpamplona.amethyst.ui.actions.GallerySelect
|
||||
import com.vitorpamplona.amethyst.ui.actions.NewMediaModel
|
||||
import com.vitorpamplona.amethyst.ui.actions.NewMediaView
|
||||
import com.vitorpamplona.amethyst.ui.navigation.Route
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
|
||||
@OptIn(ExperimentalPermissionsApi::class)
|
||||
@Composable
|
||||
fun NewImageButton(accountViewModel: AccountViewModel, nav: (String) -> Unit) {
|
||||
var wantsToPost by remember {
|
||||
mutableStateOf(false)
|
||||
}
|
||||
|
||||
var pickedURI by remember {
|
||||
mutableStateOf<Uri?>(null)
|
||||
}
|
||||
|
||||
val scope = rememberCoroutineScope()
|
||||
|
||||
val postViewModel: NewMediaModel = viewModel()
|
||||
postViewModel.onceUploaded {
|
||||
scope.launch(Dispatchers.Default) {
|
||||
// awaits an refresh on the list
|
||||
delay(250)
|
||||
withContext(Dispatchers.Main) {
|
||||
val route = Route.Video.route.replace("{scrollToTop}", "true")
|
||||
nav(route)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (wantsToPost) {
|
||||
val cameraPermissionState =
|
||||
rememberPermissionState(
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|
||||
Manifest.permission.READ_MEDIA_IMAGES
|
||||
} else {
|
||||
Manifest.permission.READ_EXTERNAL_STORAGE
|
||||
}
|
||||
)
|
||||
|
||||
if (cameraPermissionState.status.isGranted) {
|
||||
var showGallerySelect by remember { mutableStateOf(false) }
|
||||
if (showGallerySelect) {
|
||||
GallerySelect(
|
||||
onImageUri = { uri ->
|
||||
wantsToPost = false
|
||||
showGallerySelect = false
|
||||
pickedURI = uri
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
showGallerySelect = true
|
||||
} else {
|
||||
LaunchedEffect(key1 = accountViewModel) {
|
||||
cameraPermissionState.launchPermissionRequest()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pickedURI?.let {
|
||||
NewMediaView(
|
||||
uri = it,
|
||||
onClose = { pickedURI = null },
|
||||
postViewModel = postViewModel,
|
||||
accountViewModel = accountViewModel,
|
||||
nav = nav
|
||||
)
|
||||
}
|
||||
|
||||
if (postViewModel.isUploadingImage) {
|
||||
ShowProgress(postViewModel)
|
||||
} else {
|
||||
OutlinedButton(
|
||||
onClick = { wantsToPost = true },
|
||||
modifier = Modifier.size(55.dp),
|
||||
shape = CircleShape,
|
||||
colors = ButtonDefaults.outlinedButtonColors(backgroundColor = MaterialTheme.colors.primary),
|
||||
contentPadding = PaddingValues(0.dp)
|
||||
) {
|
||||
Icon(
|
||||
painter = painterResource(R.drawable.ic_compose),
|
||||
null,
|
||||
modifier = Modifier.size(26.dp),
|
||||
tint = Color.White
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun ShowProgress(postViewModel: NewMediaModel) {
|
||||
Box(Modifier.size(55.dp), contentAlignment = Alignment.Center) {
|
||||
CircularProgressIndicator(
|
||||
progress = animateFloatAsState(
|
||||
targetValue = postViewModel.uploadingPercentage.value,
|
||||
animationSpec = ProgressIndicatorDefaults.ProgressAnimationSpec
|
||||
).value,
|
||||
modifier = Modifier
|
||||
.size(55.dp)
|
||||
.clip(CircleShape)
|
||||
.background(MaterialTheme.colors.background),
|
||||
strokeWidth = 5.dp
|
||||
)
|
||||
postViewModel.uploadingDescription.value?.let {
|
||||
Text(
|
||||
it,
|
||||
color = MaterialTheme.colors.onSurface,
|
||||
fontSize = 10.sp,
|
||||
textAlign = TextAlign.Center
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -583,14 +583,22 @@ fun ClickableInLineIconRenderer(
|
||||
style: SpanStyle,
|
||||
onClick: (Int) -> Unit
|
||||
) {
|
||||
val placeholderSize = remember(style) {
|
||||
if (style.fontSize == TextUnit.Unspecified) {
|
||||
22.sp
|
||||
} else {
|
||||
style.fontSize.times(1.1f)
|
||||
}
|
||||
}
|
||||
|
||||
val inlineContent = wordsInOrder.mapIndexedNotNull { idx, value ->
|
||||
if (value is ImageUrlType) {
|
||||
Pair(
|
||||
"inlineContent$idx",
|
||||
InlineTextContent(
|
||||
Placeholder(
|
||||
width = 17.sp,
|
||||
height = 17.sp,
|
||||
width = placeholderSize,
|
||||
height = placeholderSize,
|
||||
placeholderVerticalAlign = PlaceholderVerticalAlign.Center
|
||||
)
|
||||
) {
|
||||
@@ -651,14 +659,22 @@ fun InLineIconRenderer(
|
||||
overflow: TextOverflow = TextOverflow.Clip,
|
||||
modifier: Modifier = Modifier
|
||||
) {
|
||||
val placeholderSize = remember(fontSize) {
|
||||
if (fontSize == TextUnit.Unspecified) {
|
||||
22.sp
|
||||
} else {
|
||||
fontSize.times(1.1f)
|
||||
}
|
||||
}
|
||||
|
||||
val inlineContent = wordsInOrder.mapIndexedNotNull { idx, value ->
|
||||
if (value is ImageUrlType) {
|
||||
Pair(
|
||||
"inlineContent$idx",
|
||||
InlineTextContent(
|
||||
Placeholder(
|
||||
width = 20.sp,
|
||||
height = 20.sp,
|
||||
width = placeholderSize,
|
||||
height = placeholderSize,
|
||||
placeholderVerticalAlign = PlaceholderVerticalAlign.Center
|
||||
)
|
||||
) {
|
||||
|
||||
@@ -47,18 +47,18 @@ import androidx.compose.ui.unit.isFinite
|
||||
import androidx.compose.ui.viewinterop.AndroidView
|
||||
import androidx.lifecycle.Lifecycle
|
||||
import androidx.lifecycle.LifecycleEventObserver
|
||||
import androidx.media3.common.C
|
||||
import androidx.media3.common.MediaItem
|
||||
import androidx.media3.common.Player
|
||||
import androidx.media3.datasource.DataSource
|
||||
import androidx.media3.datasource.okhttp.OkHttpDataSource
|
||||
import androidx.media3.exoplayer.ExoPlayer
|
||||
import androidx.media3.exoplayer.hls.HlsMediaSource
|
||||
import androidx.media3.exoplayer.source.ProgressiveMediaSource
|
||||
import androidx.media3.ui.AspectRatioFrameLayout
|
||||
import androidx.media3.ui.PlayerView
|
||||
import coil.imageLoader
|
||||
import coil.request.ImageRequest
|
||||
import com.google.android.exoplayer2.C
|
||||
import com.google.android.exoplayer2.ExoPlayer
|
||||
import com.google.android.exoplayer2.MediaItem
|
||||
import com.google.android.exoplayer2.Player
|
||||
import com.google.android.exoplayer2.ext.okhttp.OkHttpDataSource
|
||||
import com.google.android.exoplayer2.source.ProgressiveMediaSource
|
||||
import com.google.android.exoplayer2.source.hls.HlsMediaSource
|
||||
import com.google.android.exoplayer2.ui.AspectRatioFrameLayout
|
||||
import com.google.android.exoplayer2.ui.StyledPlayerView
|
||||
import com.google.android.exoplayer2.upstream.DataSource
|
||||
import com.vitorpamplona.amethyst.VideoCache
|
||||
import com.vitorpamplona.amethyst.service.HttpClient
|
||||
import com.vitorpamplona.amethyst.service.connectivitystatus.ConnectivityStatus
|
||||
@@ -175,6 +175,7 @@ fun VideoView(
|
||||
}
|
||||
|
||||
@Composable
|
||||
@androidx.annotation.OptIn(androidx.media3.common.util.UnstableApi::class)
|
||||
fun VideoView1(
|
||||
videoUri: String,
|
||||
description: String? = null,
|
||||
@@ -263,6 +264,7 @@ data class VideoThumb(
|
||||
)
|
||||
|
||||
@Composable
|
||||
@androidx.annotation.OptIn(androidx.media3.common.util.UnstableApi::class)
|
||||
private fun RenderVideoPlayer(
|
||||
playerData: VideoPlayer,
|
||||
thumbData: VideoThumb?,
|
||||
@@ -290,7 +292,7 @@ private fun RenderVideoPlayer(
|
||||
}
|
||||
},
|
||||
factory = {
|
||||
StyledPlayerView(context).apply {
|
||||
PlayerView(context).apply {
|
||||
player = playerData.exoPlayer
|
||||
layoutParams = FrameLayout.LayoutParams(
|
||||
ViewGroup.LayoutParams.MATCH_PARENT,
|
||||
|
||||
@@ -32,7 +32,9 @@ class ChatroomListKnownFeedFilter(val account: Account) : AdditiveFeedFilter<Not
|
||||
?.lastOrNull { it.event != null }
|
||||
}
|
||||
|
||||
val publicChannels = account.followingChannels().map { it ->
|
||||
val publicChannels = account.selectedChatsFollowList().mapNotNull {
|
||||
LocalCache.getChannelIfExists(it)
|
||||
}.mapNotNull { it ->
|
||||
it.notes.values
|
||||
.filter { account.isAcceptable(it) && it.event != null }
|
||||
.sortedWith(compareBy({ it.createdAt() }, { it.idHex }))
|
||||
@@ -40,7 +42,6 @@ class ChatroomListKnownFeedFilter(val account: Account) : AdditiveFeedFilter<Not
|
||||
}
|
||||
|
||||
return (privateMessages + publicChannels)
|
||||
.filterNotNull()
|
||||
.sortedWith(compareBy({ it.createdAt() }, { it.idHex }))
|
||||
.reversed()
|
||||
}
|
||||
@@ -106,7 +107,7 @@ class ChatroomListKnownFeedFilter(val account: Account) : AdditiveFeedFilter<Not
|
||||
}
|
||||
|
||||
private fun filterRelevantPublicMessages(newItems: Set<Note>, account: Account): MutableMap<String, Note> {
|
||||
val followingChannels = account.followingChannels
|
||||
val followingChannels = account.userProfile().latestContactList?.taggedEvents()?.toSet() ?: emptySet()
|
||||
val newRelevantPublicMessages = mutableMapOf<String, Note>()
|
||||
newItems.filter { it.event is ChannelMessageEvent }.forEach { newNote ->
|
||||
newNote.channelHex()?.let { channelHex ->
|
||||
|
||||
@@ -13,6 +13,10 @@ open class DiscoverChatFeedFilter(val account: Account) : AdditiveFeedFilter<Not
|
||||
return account.userProfile().pubkeyHex + "-" + account.defaultDiscoveryFollowList
|
||||
}
|
||||
|
||||
override fun showHiddenKey(): Boolean {
|
||||
return account.defaultDiscoveryFollowList == PeopleListEvent.blockList
|
||||
}
|
||||
|
||||
override fun feed(): List<Note> {
|
||||
val allChannelNotes = LocalCache.channels.values.mapNotNull { LocalCache.getNoteIfExists(it.idHex) }
|
||||
|
||||
@@ -28,6 +32,7 @@ open class DiscoverChatFeedFilter(val account: Account) : AdditiveFeedFilter<Not
|
||||
protected open fun innerApplyFilter(collection: Collection<Note>): Set<Note> {
|
||||
val now = TimeUtils.now()
|
||||
val isGlobal = account.defaultDiscoveryFollowList == GLOBAL_FOLLOWS
|
||||
val isHiddenList = showHiddenKey()
|
||||
|
||||
val followingKeySet = account.selectedUsersFollowList(account.defaultDiscoveryFollowList) ?: emptySet()
|
||||
val followingTagSet = account.selectedTagsFollowList(account.defaultDiscoveryFollowList) ?: emptySet()
|
||||
@@ -36,15 +41,15 @@ open class DiscoverChatFeedFilter(val account: Account) : AdditiveFeedFilter<Not
|
||||
val anyOtherChannelEvent = collection
|
||||
.asSequence()
|
||||
.filter { it.event is IsInPublicChatChannel }
|
||||
.mapNotNull { (it.event as? ChannelMessageEvent)?.channel() }
|
||||
.mapNotNull { (it.event as? IsInPublicChatChannel)?.channel() }
|
||||
.mapNotNull { LocalCache.checkGetOrCreateNote(it) }
|
||||
.toSet()
|
||||
|
||||
val activities = (createEvents + anyOtherChannelEvent)
|
||||
.asSequence()
|
||||
.filter { it.event is ChannelCreateEvent }
|
||||
// .filter { it.event is ChannelCreateEvent } // Event heads might not be loaded yet.
|
||||
.filter { isGlobal || it.author?.pubkeyHex in followingKeySet || it.event?.isTaggedHashes(followingTagSet) == true }
|
||||
.filter { it.author?.let { !account.isHidden(it.pubkeyHex) } ?: true }
|
||||
.filter { isHiddenList || it.author?.let { !account.isHidden(it.pubkeyHex) } ?: true }
|
||||
.filter { (it.createdAt() ?: 0) <= now }
|
||||
.toSet()
|
||||
|
||||
|
||||
@@ -13,6 +13,10 @@ open class DiscoverCommunityFeedFilter(val account: Account) : AdditiveFeedFilte
|
||||
return account.userProfile().pubkeyHex + "-" + account.defaultDiscoveryFollowList
|
||||
}
|
||||
|
||||
override fun showHiddenKey(): Boolean {
|
||||
return account.defaultDiscoveryFollowList == PeopleListEvent.blockList
|
||||
}
|
||||
|
||||
override fun feed(): List<Note> {
|
||||
val allNotes = LocalCache.addressables.values
|
||||
|
||||
@@ -28,15 +32,24 @@ open class DiscoverCommunityFeedFilter(val account: Account) : AdditiveFeedFilte
|
||||
protected open fun innerApplyFilter(collection: Collection<Note>): Set<Note> {
|
||||
val now = TimeUtils.now()
|
||||
val isGlobal = account.defaultDiscoveryFollowList == GLOBAL_FOLLOWS
|
||||
val isHiddenList = showHiddenKey()
|
||||
|
||||
val followingKeySet = account.selectedUsersFollowList(account.defaultDiscoveryFollowList) ?: emptySet()
|
||||
val followingTagSet = account.selectedTagsFollowList(account.defaultDiscoveryFollowList) ?: emptySet()
|
||||
|
||||
val activities = collection
|
||||
val createEvents = collection.filter { it.event is CommunityDefinitionEvent }
|
||||
val anyOtherCommunityEvent = collection
|
||||
.asSequence()
|
||||
.filter { it.event is CommunityPostApprovalEvent }
|
||||
.mapNotNull { (it.event as? CommunityPostApprovalEvent)?.communities() }.flatten()
|
||||
.map { LocalCache.getOrCreateAddressableNote(it) }
|
||||
.toSet()
|
||||
|
||||
val activities = (createEvents + anyOtherCommunityEvent)
|
||||
.asSequence()
|
||||
.filter { it.event is CommunityDefinitionEvent }
|
||||
.filter { isGlobal || it.author?.pubkeyHex in followingKeySet || it.event?.isTaggedHashes(followingTagSet) == true }
|
||||
.filter { it.author?.let { !account.isHidden(it.pubkeyHex) } ?: true }
|
||||
.filter { isHiddenList || it.author?.let { !account.isHidden(it.pubkeyHex) } ?: true }
|
||||
.filter { (it.createdAt() ?: 0) <= now }
|
||||
.toSet()
|
||||
|
||||
|
||||
@@ -16,6 +16,10 @@ open class DiscoverLiveFeedFilter(val account: Account) : AdditiveFeedFilter<Not
|
||||
return account.userProfile().pubkeyHex + "-" + account.defaultDiscoveryFollowList
|
||||
}
|
||||
|
||||
override fun showHiddenKey(): Boolean {
|
||||
return account.defaultDiscoveryFollowList == PeopleListEvent.blockList
|
||||
}
|
||||
|
||||
override fun feed(): List<Note> {
|
||||
val allChannelNotes =
|
||||
LocalCache.channels.values.mapNotNull { LocalCache.getNoteIfExists(it.idHex) }
|
||||
@@ -33,6 +37,7 @@ open class DiscoverLiveFeedFilter(val account: Account) : AdditiveFeedFilter<Not
|
||||
protected open fun innerApplyFilter(collection: Collection<Note>): Set<Note> {
|
||||
val now = TimeUtils.now()
|
||||
val isGlobal = account.defaultDiscoveryFollowList == GLOBAL_FOLLOWS
|
||||
val isHiddenList = showHiddenKey()
|
||||
|
||||
val followingKeySet =
|
||||
account.selectedUsersFollowList(account.defaultDiscoveryFollowList) ?: emptySet()
|
||||
@@ -47,7 +52,7 @@ open class DiscoverLiveFeedFilter(val account: Account) : AdditiveFeedFilter<Not
|
||||
followingTagSet
|
||||
) == true
|
||||
}
|
||||
.filter { it.author?.let { !account.isHidden(it.pubkeyHex) } ?: true }
|
||||
.filter { isHiddenList || it.author?.let { !account.isHidden(it.pubkeyHex) } ?: true }
|
||||
.filter { (it.createdAt() ?: 0) <= now }
|
||||
.toSet()
|
||||
|
||||
|
||||
@@ -24,6 +24,7 @@ abstract class FeedFilter<T> {
|
||||
* Returns a string that serves as the key to invalidate the list if it changes.
|
||||
*/
|
||||
abstract fun feedKey(): String
|
||||
open fun showHiddenKey(): Boolean = false
|
||||
|
||||
abstract fun feed(): List<T>
|
||||
}
|
||||
|
||||
@@ -9,8 +9,28 @@ class HiddenAccountsFeedFilter(val account: Account) : FeedFilter<User>() {
|
||||
return account.userProfile().pubkeyHex
|
||||
}
|
||||
|
||||
override fun showHiddenKey(): Boolean {
|
||||
return true
|
||||
}
|
||||
|
||||
override fun feed(): List<User> {
|
||||
return (account.hiddenUsers + account.transientHiddenUsers)
|
||||
.map { LocalCache.getOrCreateUser(it) }
|
||||
return account.getBlockList()
|
||||
?.publicAndPrivateUsers(account.loggedIn.privKey)
|
||||
?.map { LocalCache.getOrCreateUser(it) }
|
||||
?: emptyList()
|
||||
}
|
||||
}
|
||||
|
||||
class SpammerAccountsFeedFilter(val account: Account) : FeedFilter<User>() {
|
||||
override fun feedKey(): String {
|
||||
return account.userProfile().pubkeyHex
|
||||
}
|
||||
|
||||
override fun showHiddenKey(): Boolean {
|
||||
return true
|
||||
}
|
||||
|
||||
override fun feed(): List<User> {
|
||||
return (account.transientHiddenUsers).map { LocalCache.getOrCreateUser(it) }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,6 +7,7 @@ import com.vitorpamplona.amethyst.model.Note
|
||||
import com.vitorpamplona.amethyst.model.TimeUtils
|
||||
import com.vitorpamplona.amethyst.service.model.ChannelMessageEvent
|
||||
import com.vitorpamplona.amethyst.service.model.LiveActivitiesChatMessageEvent
|
||||
import com.vitorpamplona.amethyst.service.model.PeopleListEvent
|
||||
import com.vitorpamplona.amethyst.service.model.PollNoteEvent
|
||||
import com.vitorpamplona.amethyst.service.model.TextNoteEvent
|
||||
|
||||
@@ -16,6 +17,10 @@ class HomeConversationsFeedFilter(val account: Account) : AdditiveFeedFilter<Not
|
||||
return account.userProfile().pubkeyHex + "-" + account.defaultHomeFollowList
|
||||
}
|
||||
|
||||
override fun showHiddenKey(): Boolean {
|
||||
return account.defaultHomeFollowList == PeopleListEvent.blockList
|
||||
}
|
||||
|
||||
override fun feed(): List<Note> {
|
||||
return sort(innerApplyFilter(LocalCache.notes.values))
|
||||
}
|
||||
@@ -26,6 +31,7 @@ class HomeConversationsFeedFilter(val account: Account) : AdditiveFeedFilter<Not
|
||||
|
||||
private fun innerApplyFilter(collection: Collection<Note>): Set<Note> {
|
||||
val isGlobal = account.defaultHomeFollowList == GLOBAL_FOLLOWS
|
||||
val isHiddenList = showHiddenKey()
|
||||
|
||||
val followingKeySet = account.selectedUsersFollowList(account.defaultHomeFollowList) ?: emptySet()
|
||||
val followingTagSet = account.selectedTagsFollowList(account.defaultHomeFollowList) ?: emptySet()
|
||||
@@ -38,7 +44,7 @@ class HomeConversationsFeedFilter(val account: Account) : AdditiveFeedFilter<Not
|
||||
(it.event is TextNoteEvent || it.event is PollNoteEvent || it.event is ChannelMessageEvent || it.event is LiveActivitiesChatMessageEvent) &&
|
||||
(isGlobal || it.author?.pubkeyHex in followingKeySet || it.event?.isTaggedHashes(followingTagSet) ?: false) &&
|
||||
// && account.isAcceptable(it) // This filter follows only. No need to check if acceptable
|
||||
it.author?.let { !account.isHidden(it) } ?: true &&
|
||||
(isHiddenList || it.author?.let { !account.isHidden(it) } ?: true) &&
|
||||
((it.event?.createdAt() ?: 0) < now) &&
|
||||
!it.isNewThread()
|
||||
}
|
||||
|
||||
@@ -10,6 +10,7 @@ import com.vitorpamplona.amethyst.service.model.ClassifiedsEvent
|
||||
import com.vitorpamplona.amethyst.service.model.GenericRepostEvent
|
||||
import com.vitorpamplona.amethyst.service.model.HighlightEvent
|
||||
import com.vitorpamplona.amethyst.service.model.LongTextNoteEvent
|
||||
import com.vitorpamplona.amethyst.service.model.PeopleListEvent
|
||||
import com.vitorpamplona.amethyst.service.model.PollNoteEvent
|
||||
import com.vitorpamplona.amethyst.service.model.RepostEvent
|
||||
import com.vitorpamplona.amethyst.service.model.TextNoteEvent
|
||||
@@ -19,6 +20,10 @@ class HomeNewThreadFeedFilter(val account: Account) : AdditiveFeedFilter<Note>()
|
||||
return account.userProfile().pubkeyHex + "-" + account.defaultHomeFollowList
|
||||
}
|
||||
|
||||
override fun showHiddenKey(): Boolean {
|
||||
return account.defaultHomeFollowList == PeopleListEvent.blockList
|
||||
}
|
||||
|
||||
override fun feed(): List<Note> {
|
||||
val notes = innerApplyFilter(LocalCache.notes.values)
|
||||
val longFormNotes = innerApplyFilter(LocalCache.addressables.values)
|
||||
@@ -32,9 +37,11 @@ class HomeNewThreadFeedFilter(val account: Account) : AdditiveFeedFilter<Note>()
|
||||
|
||||
private fun innerApplyFilter(collection: Collection<Note>): Set<Note> {
|
||||
val isGlobal = account.defaultHomeFollowList == GLOBAL_FOLLOWS
|
||||
val isHiddenList = showHiddenKey()
|
||||
|
||||
val followingKeySet = account.selectedUsersFollowList(account.defaultHomeFollowList) ?: emptySet()
|
||||
val followingTagSet = account.selectedTagsFollowList(account.defaultHomeFollowList) ?: emptySet()
|
||||
val followingCommunities = account.selectedCommunitiesFollowList(account.defaultHomeFollowList) ?: emptySet()
|
||||
|
||||
val oneMinuteInTheFuture = TimeUtils.now() + (1 * 60) // one minute in the future.
|
||||
val oneHr = 60 * 60
|
||||
@@ -44,9 +51,9 @@ class HomeNewThreadFeedFilter(val account: Account) : AdditiveFeedFilter<Note>()
|
||||
.filter { it ->
|
||||
val noteEvent = it.event
|
||||
(noteEvent is TextNoteEvent || noteEvent is ClassifiedsEvent || noteEvent is RepostEvent || noteEvent is GenericRepostEvent || noteEvent is LongTextNoteEvent || noteEvent is PollNoteEvent || noteEvent is HighlightEvent || noteEvent is AudioTrackEvent) &&
|
||||
(isGlobal || it.author?.pubkeyHex in followingKeySet || noteEvent.isTaggedHashes(followingTagSet)) &&
|
||||
(isGlobal || it.author?.pubkeyHex in followingKeySet || noteEvent.isTaggedHashes(followingTagSet) || noteEvent.isTaggedAddressableNotes(followingCommunities)) &&
|
||||
// && account.isAcceptable(it) // This filter follows only. No need to check if acceptable
|
||||
it.author?.let { !account.isHidden(it.pubkeyHex) } ?: true &&
|
||||
(isHiddenList || it.author?.let { !account.isHidden(it.pubkeyHex) } ?: true) &&
|
||||
((it.event?.createdAt() ?: 0) < oneMinuteInTheFuture) &&
|
||||
it.isNewThread() &&
|
||||
(
|
||||
|
||||
@@ -12,6 +12,10 @@ class NotificationFeedFilter(val account: Account) : AdditiveFeedFilter<Note>()
|
||||
return account.userProfile().pubkeyHex + "-" + account.defaultNotificationFollowList
|
||||
}
|
||||
|
||||
override fun showHiddenKey(): Boolean {
|
||||
return account.defaultNotificationFollowList == PeopleListEvent.blockList
|
||||
}
|
||||
|
||||
override fun feed(): List<Note> {
|
||||
return sort(innerApplyFilter(LocalCache.notes.values))
|
||||
}
|
||||
@@ -22,6 +26,7 @@ class NotificationFeedFilter(val account: Account) : AdditiveFeedFilter<Note>()
|
||||
|
||||
private fun innerApplyFilter(collection: Collection<Note>): Set<Note> {
|
||||
val isGlobal = account.defaultNotificationFollowList == GLOBAL_FOLLOWS
|
||||
val isHiddenList = showHiddenKey()
|
||||
|
||||
val followingKeySet = account.selectedUsersFollowList(account.defaultNotificationFollowList) ?: emptySet()
|
||||
|
||||
@@ -37,7 +42,7 @@ class NotificationFeedFilter(val account: Account) : AdditiveFeedFilter<Note>()
|
||||
it.author !== loggedInUser &&
|
||||
(isGlobal || it.author?.pubkeyHex in followingKeySet) &&
|
||||
it.event?.isTaggedUser(loggedInUserHex) ?: false &&
|
||||
(it.author == null || !account.isHidden(it.author!!.pubkeyHex)) &&
|
||||
(isHiddenList || it.author == null || !account.isHidden(it.author!!.pubkeyHex)) &&
|
||||
tagsAnEventByUser(it, loggedInUserHex)
|
||||
}.toSet()
|
||||
}
|
||||
|
||||
@@ -5,21 +5,37 @@ import com.vitorpamplona.amethyst.model.Note
|
||||
import com.vitorpamplona.amethyst.model.User
|
||||
import com.vitorpamplona.amethyst.service.model.AppRecommendationEvent
|
||||
|
||||
class UserProfileAppRecommendationsFeedFilter(val user: User) : FeedFilter<Note>() {
|
||||
class UserProfileAppRecommendationsFeedFilter(val user: User) : AdditiveFeedFilter<Note>() {
|
||||
override fun feedKey(): String {
|
||||
return user.pubkeyHex
|
||||
}
|
||||
|
||||
override fun feed(): List<Note> {
|
||||
val recommendations = LocalCache.addressables.values.filter {
|
||||
(it.event as? AppRecommendationEvent)?.pubKey == user.pubkeyHex
|
||||
}.mapNotNull {
|
||||
(it.event as? AppRecommendationEvent)?.recommendations()
|
||||
}.flatten()
|
||||
.map {
|
||||
return sort(innerApplyFilter(LocalCache.addressables.values))
|
||||
}
|
||||
|
||||
override fun applyFilter(collection: Set<Note>): Set<Note> {
|
||||
return innerApplyFilter(collection)
|
||||
}
|
||||
|
||||
private fun innerApplyFilter(collection: Collection<Note>): Set<Note> {
|
||||
val recommendations = collection.asSequence()
|
||||
.filter { it.event is AppRecommendationEvent }
|
||||
.mapNotNull {
|
||||
val noteEvent = it.event as? AppRecommendationEvent
|
||||
if (noteEvent != null && noteEvent.pubKey == user.pubkeyHex) {
|
||||
noteEvent.recommendations()
|
||||
} else {
|
||||
null
|
||||
}
|
||||
}.flatten().map {
|
||||
LocalCache.getOrCreateAddressableNote(it)
|
||||
}.toSet().toList()
|
||||
}.toSet()
|
||||
|
||||
return recommendations
|
||||
}
|
||||
|
||||
override fun sort(collection: Set<Note>): List<Note> {
|
||||
return collection.sortedWith(compareBy({ it.createdAt() }, { it.idHex }))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,18 +1,45 @@
|
||||
package com.vitorpamplona.amethyst.ui.dal
|
||||
|
||||
import com.vitorpamplona.amethyst.model.Account
|
||||
import com.vitorpamplona.amethyst.model.LocalCache
|
||||
import com.vitorpamplona.amethyst.model.Note
|
||||
import com.vitorpamplona.amethyst.model.User
|
||||
import com.vitorpamplona.amethyst.service.model.ChannelMessageEvent
|
||||
import com.vitorpamplona.amethyst.service.model.LiveActivitiesChatMessageEvent
|
||||
import com.vitorpamplona.amethyst.service.model.PollNoteEvent
|
||||
import com.vitorpamplona.amethyst.service.model.TextNoteEvent
|
||||
|
||||
class UserProfileConversationsFeedFilter(val user: User, val account: Account) : FeedFilter<Note>() {
|
||||
class UserProfileConversationsFeedFilter(val user: User, val account: Account) : AdditiveFeedFilter<Note>() {
|
||||
override fun feedKey(): String {
|
||||
return account.userProfile().pubkeyHex + "-" + user.pubkeyHex
|
||||
}
|
||||
|
||||
override fun feed(): List<Note> {
|
||||
return user.notes
|
||||
.filter { account.isAcceptable(it) == true && !it.isNewThread() }
|
||||
.sortedWith(compareBy({ it.createdAt() }, { it.idHex }))
|
||||
.reversed() ?: emptyList()
|
||||
return sort(innerApplyFilter(LocalCache.notes.values))
|
||||
}
|
||||
|
||||
override fun applyFilter(collection: Set<Note>): Set<Note> {
|
||||
return innerApplyFilter(collection)
|
||||
}
|
||||
|
||||
private fun innerApplyFilter(collection: Collection<Note>): Set<Note> {
|
||||
return collection
|
||||
.filter {
|
||||
it.author == user &&
|
||||
(
|
||||
it.event is TextNoteEvent ||
|
||||
it.event is PollNoteEvent ||
|
||||
it.event is ChannelMessageEvent ||
|
||||
it.event is LiveActivitiesChatMessageEvent
|
||||
) &&
|
||||
!it.isNewThread() &&
|
||||
account.isAcceptable(it) == true
|
||||
}.toSet()
|
||||
}
|
||||
|
||||
override fun sort(collection: Set<Note>): List<Note> {
|
||||
return collection.sortedWith(compareBy({ it.createdAt() }, { it.idHex })).reversed()
|
||||
}
|
||||
|
||||
override fun limit() = 200
|
||||
}
|
||||
|
||||
@@ -12,4 +12,6 @@ class UserProfileFollowersFeedFilter(val user: User, val account: Account) : Fee
|
||||
override fun feed(): List<User> {
|
||||
return LocalCache.users.values.filter { it.isFollowing(user) && !account.isHidden(it) }
|
||||
}
|
||||
|
||||
override fun limit() = 400
|
||||
}
|
||||
|
||||
@@ -8,7 +8,7 @@ import com.vitorpamplona.amethyst.service.model.ContactListEvent
|
||||
class UserProfileFollowsFeedFilter(val user: User, val account: Account) : FeedFilter<User>() {
|
||||
|
||||
override fun feedKey(): String {
|
||||
return account.userProfile()?.pubkeyHex + "-" + user.pubkeyHex
|
||||
return account.userProfile().pubkeyHex + "-" + user.pubkeyHex
|
||||
}
|
||||
|
||||
val cache: MutableMap<ContactListEvent, List<User>> = mutableMapOf()
|
||||
|
||||
@@ -4,32 +4,53 @@ import com.vitorpamplona.amethyst.model.Account
|
||||
import com.vitorpamplona.amethyst.model.LocalCache
|
||||
import com.vitorpamplona.amethyst.model.Note
|
||||
import com.vitorpamplona.amethyst.model.User
|
||||
import com.vitorpamplona.amethyst.service.model.AppRecommendationEvent
|
||||
import com.vitorpamplona.amethyst.service.model.BookmarkListEvent
|
||||
import com.vitorpamplona.amethyst.service.model.EmojiPackSelectionEvent
|
||||
import com.vitorpamplona.amethyst.service.model.PeopleListEvent
|
||||
import com.vitorpamplona.amethyst.service.model.AudioTrackEvent
|
||||
import com.vitorpamplona.amethyst.service.model.ClassifiedsEvent
|
||||
import com.vitorpamplona.amethyst.service.model.GenericRepostEvent
|
||||
import com.vitorpamplona.amethyst.service.model.HighlightEvent
|
||||
import com.vitorpamplona.amethyst.service.model.LongTextNoteEvent
|
||||
import com.vitorpamplona.amethyst.service.model.PollNoteEvent
|
||||
import com.vitorpamplona.amethyst.service.model.RepostEvent
|
||||
import com.vitorpamplona.amethyst.service.model.TextNoteEvent
|
||||
|
||||
class UserProfileNewThreadFeedFilter(val user: User, val account: Account) : FeedFilter<Note>() {
|
||||
class UserProfileNewThreadFeedFilter(val user: User, val account: Account) : AdditiveFeedFilter<Note>() {
|
||||
override fun feedKey(): String {
|
||||
return account.userProfile().pubkeyHex + "-" + user.pubkeyHex
|
||||
}
|
||||
|
||||
override fun feed(): List<Note> {
|
||||
val longFormNotes = LocalCache.addressables.values
|
||||
val notes = innerApplyFilter(LocalCache.notes.values)
|
||||
val longFormNotes = innerApplyFilter(LocalCache.addressables.values)
|
||||
|
||||
return sort(notes + longFormNotes)
|
||||
}
|
||||
|
||||
override fun applyFilter(collection: Set<Note>): Set<Note> {
|
||||
return innerApplyFilter(collection)
|
||||
}
|
||||
|
||||
private fun innerApplyFilter(collection: Collection<Note>): Set<Note> {
|
||||
return collection
|
||||
.filter {
|
||||
it.author == user &&
|
||||
(
|
||||
it.event !is PeopleListEvent &&
|
||||
it.event !is BookmarkListEvent &&
|
||||
it.event !is AppRecommendationEvent &&
|
||||
it.event !is EmojiPackSelectionEvent
|
||||
)
|
||||
}
|
||||
|
||||
return user.notes
|
||||
.plus(longFormNotes)
|
||||
.filter { account.isAcceptable(it) == true && it.isNewThread() }
|
||||
.sortedWith(compareBy({ it.createdAt() }, { it.idHex }))
|
||||
.reversed() ?: emptyList()
|
||||
it.event is TextNoteEvent ||
|
||||
it.event is ClassifiedsEvent ||
|
||||
it.event is RepostEvent ||
|
||||
it.event is GenericRepostEvent ||
|
||||
it.event is LongTextNoteEvent ||
|
||||
it.event is PollNoteEvent ||
|
||||
it.event is HighlightEvent ||
|
||||
it.event is AudioTrackEvent
|
||||
) &&
|
||||
it.isNewThread() &&
|
||||
account.isAcceptable(it) == true
|
||||
}.toSet()
|
||||
}
|
||||
|
||||
override fun sort(collection: Set<Note>): List<Note> {
|
||||
return collection.sortedWith(compareBy({ it.createdAt() }, { it.idHex })).reversed()
|
||||
}
|
||||
|
||||
override fun limit() = 200
|
||||
}
|
||||
|
||||
@@ -1,20 +1,29 @@
|
||||
package com.vitorpamplona.amethyst.ui.dal
|
||||
|
||||
import com.vitorpamplona.amethyst.model.LocalCache
|
||||
import com.vitorpamplona.amethyst.model.Note
|
||||
import com.vitorpamplona.amethyst.model.User
|
||||
import com.vitorpamplona.amethyst.service.model.ReportEvent
|
||||
|
||||
class UserProfileReportsFeedFilter(val user: User) : FeedFilter<Note>() {
|
||||
class UserProfileReportsFeedFilter(val user: User) : AdditiveFeedFilter<Note>() {
|
||||
override fun feedKey(): String {
|
||||
return user.pubkeyHex ?: ""
|
||||
return user.pubkeyHex
|
||||
}
|
||||
|
||||
override fun feed(): List<Note> {
|
||||
val reportNotes = LocalCache.notes.values.filter { (it.event as? ReportEvent)?.isTaggedUser(user.pubkeyHex) == true }
|
||||
|
||||
return reportNotes
|
||||
.sortedWith(compareBy({ it.createdAt() }, { it.idHex }))
|
||||
.reversed()
|
||||
return sort(innerApplyFilter(user.reports.values.flatten()))
|
||||
}
|
||||
|
||||
override fun applyFilter(collection: Set<Note>): Set<Note> {
|
||||
return innerApplyFilter(collection)
|
||||
}
|
||||
|
||||
private fun innerApplyFilter(collection: Collection<Note>): Set<Note> {
|
||||
return collection.filter { it.event is ReportEvent && it.event?.isTaggedUser(user.pubkeyHex) == true }.toSet()
|
||||
}
|
||||
|
||||
override fun sort(collection: Set<Note>): List<Note> {
|
||||
return collection.sortedWith(compareBy({ it.createdAt() }, { it.idHex })).reversed()
|
||||
}
|
||||
|
||||
override fun limit() = 400
|
||||
}
|
||||
|
||||
@@ -13,4 +13,6 @@ class UserProfileZapsFeedFilter(val user: User) : FeedFilter<ZapReqResponse>() {
|
||||
override fun feed(): List<ZapReqResponse> {
|
||||
return UserZaps.forProfileFeed(user.zaps)
|
||||
}
|
||||
|
||||
override fun limit() = 400
|
||||
}
|
||||
|
||||
@@ -12,6 +12,10 @@ class VideoFeedFilter(val account: Account) : AdditiveFeedFilter<Note>() {
|
||||
return account.userProfile().pubkeyHex + "-" + account.defaultStoriesFollowList
|
||||
}
|
||||
|
||||
override fun showHiddenKey(): Boolean {
|
||||
return account.defaultStoriesFollowList == PeopleListEvent.blockList
|
||||
}
|
||||
|
||||
override fun feed(): List<Note> {
|
||||
val notes = innerApplyFilter(LocalCache.notes.values)
|
||||
|
||||
@@ -25,6 +29,7 @@ class VideoFeedFilter(val account: Account) : AdditiveFeedFilter<Note>() {
|
||||
private fun innerApplyFilter(collection: Collection<Note>): Set<Note> {
|
||||
val now = TimeUtils.now()
|
||||
val isGlobal = account.defaultStoriesFollowList == GLOBAL_FOLLOWS
|
||||
val isHiddenList = account.defaultStoriesFollowList == PeopleListEvent.blockList
|
||||
|
||||
val followingKeySet = account.selectedUsersFollowList(account.defaultStoriesFollowList) ?: emptySet()
|
||||
val followingTagSet = account.selectedTagsFollowList(account.defaultStoriesFollowList) ?: emptySet()
|
||||
@@ -33,7 +38,7 @@ class VideoFeedFilter(val account: Account) : AdditiveFeedFilter<Note>() {
|
||||
.asSequence()
|
||||
.filter { it.event is FileHeaderEvent || it.event is FileStorageHeaderEvent }
|
||||
.filter { isGlobal || it.author?.pubkeyHex in followingKeySet || (it.event?.isTaggedHashes(followingTagSet) ?: false) }
|
||||
.filter { account.isAcceptable(it) }
|
||||
.filter { isHiddenList || account.isAcceptable(it) }
|
||||
.filter { it.createdAt()!! <= now }
|
||||
.toSet()
|
||||
}
|
||||
|
||||
@@ -160,25 +160,23 @@ fun ProfileContent(
|
||||
}
|
||||
|
||||
Column(modifier = modifier) {
|
||||
profilePicture?.let {
|
||||
RobohashAsyncImageProxy(
|
||||
robot = profilePubHex,
|
||||
model = profilePicture,
|
||||
contentDescription = stringResource(id = R.string.profile_image),
|
||||
modifier = Modifier
|
||||
.width(100.dp)
|
||||
.height(100.dp)
|
||||
.clip(shape = CircleShape)
|
||||
.border(3.dp, MaterialTheme.colors.background, CircleShape)
|
||||
.background(MaterialTheme.colors.background)
|
||||
.clickable(onClick = {
|
||||
nav(route)
|
||||
coroutineScope.launch {
|
||||
scaffoldState.drawerState.close()
|
||||
}
|
||||
})
|
||||
)
|
||||
}
|
||||
RobohashAsyncImageProxy(
|
||||
robot = profilePubHex,
|
||||
model = profilePicture,
|
||||
contentDescription = stringResource(id = R.string.profile_image),
|
||||
modifier = Modifier
|
||||
.width(100.dp)
|
||||
.height(100.dp)
|
||||
.clip(shape = CircleShape)
|
||||
.border(3.dp, MaterialTheme.colors.background, CircleShape)
|
||||
.background(MaterialTheme.colors.background)
|
||||
.clickable(onClick = {
|
||||
nav(route)
|
||||
coroutineScope.launch {
|
||||
scaffoldState.drawerState.close()
|
||||
}
|
||||
})
|
||||
)
|
||||
|
||||
if (bestDisplayName != null) {
|
||||
CreateTextWithEmoji(
|
||||
|
||||
@@ -43,13 +43,17 @@ import kotlinx.coroutines.launch
|
||||
|
||||
@OptIn(ExperimentalFoundationApi::class)
|
||||
@Composable
|
||||
fun BadgeCompose(likeSetCard: BadgeCard, isInnerNote: Boolean = false, routeForLastRead: String, accountViewModel: AccountViewModel, nav: (String) -> Unit) {
|
||||
fun BadgeCompose(likeSetCard: BadgeCard, isInnerNote: Boolean = false, routeForLastRead: String, showHidden: Boolean = false, accountViewModel: AccountViewModel, nav: (String) -> Unit) {
|
||||
val noteState by likeSetCard.note.live().metadata.observeAsState()
|
||||
val note = noteState?.note
|
||||
|
||||
val context = LocalContext.current.applicationContext
|
||||
|
||||
var popupExpanded by remember { mutableStateOf(false) }
|
||||
val popupExpanded = remember { mutableStateOf(false) }
|
||||
val enablePopup = remember {
|
||||
{ popupExpanded.value = true }
|
||||
}
|
||||
|
||||
val scope = rememberCoroutineScope()
|
||||
|
||||
if (note == null) {
|
||||
@@ -89,7 +93,7 @@ fun BadgeCompose(likeSetCard: BadgeCard, isInnerNote: Boolean = false, routeForL
|
||||
)?.let { nav(it) }
|
||||
}
|
||||
},
|
||||
onLongClick = { popupExpanded = true }
|
||||
onLongClick = enablePopup
|
||||
)
|
||||
) {
|
||||
Row(
|
||||
@@ -136,7 +140,7 @@ fun BadgeCompose(likeSetCard: BadgeCard, isInnerNote: Boolean = false, routeForL
|
||||
|
||||
IconButton(
|
||||
modifier = Modifier.then(Modifier.size(24.dp)),
|
||||
onClick = { popupExpanded = true }
|
||||
onClick = enablePopup
|
||||
) {
|
||||
Icon(
|
||||
imageVector = Icons.Default.MoreVert,
|
||||
@@ -145,7 +149,7 @@ fun BadgeCompose(likeSetCard: BadgeCard, isInnerNote: Boolean = false, routeForL
|
||||
tint = MaterialTheme.colors.placeholderText
|
||||
)
|
||||
|
||||
NoteDropDownMenu(note, popupExpanded, { popupExpanded = false }, accountViewModel)
|
||||
NoteDropDownMenu(note, popupExpanded, accountViewModel)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -154,6 +158,7 @@ fun BadgeCompose(likeSetCard: BadgeCard, isInnerNote: Boolean = false, routeForL
|
||||
baseNote = it,
|
||||
routeForLastRead = null,
|
||||
isBoostedNote = true,
|
||||
showHidden = showHidden,
|
||||
parentBackgroundColor = backgroundColor,
|
||||
accountViewModel = accountViewModel,
|
||||
nav = nav
|
||||
|
||||
@@ -86,6 +86,8 @@ fun ChannelCardCompose(
|
||||
routeForLastRead: String? = null,
|
||||
modifier: Modifier = Modifier,
|
||||
parentBackgroundColor: MutableState<Color>? = null,
|
||||
forceEventKind: Int?,
|
||||
showHidden: Boolean = false,
|
||||
accountViewModel: AccountViewModel,
|
||||
nav: (String) -> Unit
|
||||
) {
|
||||
@@ -107,14 +109,17 @@ fun ChannelCardCompose(
|
||||
)
|
||||
}
|
||||
} else {
|
||||
CheckHiddenChannelCardCompose(
|
||||
baseNote,
|
||||
routeForLastRead,
|
||||
modifier,
|
||||
parentBackgroundColor,
|
||||
accountViewModel,
|
||||
nav
|
||||
)
|
||||
if (forceEventKind == null || baseNote.event?.kind() == forceEventKind) {
|
||||
CheckHiddenChannelCardCompose(
|
||||
baseNote,
|
||||
routeForLastRead,
|
||||
modifier,
|
||||
parentBackgroundColor,
|
||||
showHidden,
|
||||
accountViewModel,
|
||||
nav
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -125,24 +130,47 @@ fun CheckHiddenChannelCardCompose(
|
||||
routeForLastRead: String? = null,
|
||||
modifier: Modifier = Modifier,
|
||||
parentBackgroundColor: MutableState<Color>? = null,
|
||||
showHidden: Boolean,
|
||||
accountViewModel: AccountViewModel,
|
||||
nav: (String) -> Unit
|
||||
) {
|
||||
val isHidden by accountViewModel.accountLiveData.map {
|
||||
accountViewModel.isNoteHidden(note)
|
||||
}.distinctUntilChanged().observeAsState(accountViewModel.isNoteHidden(note))
|
||||
|
||||
Crossfade(targetState = isHidden) {
|
||||
if (!it) {
|
||||
LoadedChannelCardCompose(
|
||||
note,
|
||||
routeForLastRead,
|
||||
modifier,
|
||||
parentBackgroundColor,
|
||||
accountViewModel,
|
||||
nav
|
||||
if (showHidden) {
|
||||
var state by remember {
|
||||
mutableStateOf(
|
||||
NoteComposeReportState(
|
||||
isAcceptable = true,
|
||||
canPreview = true,
|
||||
relevantReports = persistentSetOf()
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
RenderChannelCardReportState(
|
||||
state = state,
|
||||
note = note,
|
||||
routeForLastRead = routeForLastRead,
|
||||
modifier = modifier,
|
||||
parentBackgroundColor = parentBackgroundColor,
|
||||
accountViewModel = accountViewModel,
|
||||
nav = nav
|
||||
)
|
||||
} else {
|
||||
val isHidden by accountViewModel.account.liveHiddenUsers.map {
|
||||
note.isHiddenFor(it)
|
||||
}.distinctUntilChanged().observeAsState(accountViewModel.isNoteHidden(note))
|
||||
|
||||
Crossfade(targetState = isHidden) {
|
||||
if (!it) {
|
||||
LoadedChannelCardCompose(
|
||||
note,
|
||||
routeForLastRead,
|
||||
modifier,
|
||||
parentBackgroundColor,
|
||||
accountViewModel,
|
||||
nav
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -492,6 +520,7 @@ fun RenderLiveActivityThumb(
|
||||
showVideo = false,
|
||||
showBottomDiviser = false,
|
||||
showFlag = false,
|
||||
sendToChannel = true,
|
||||
modifier = remember {
|
||||
Modifier.padding(start = 0.dp, end = 0.dp, top = 5.dp, bottom = 5.dp)
|
||||
},
|
||||
@@ -631,11 +660,11 @@ fun RenderChannelThumb(baseNote: Note, accountViewModel: AccountViewModel, nav:
|
||||
fun RenderChannelThumb(baseNote: Note, channel: Channel, accountViewModel: AccountViewModel, nav: (String) -> Unit) {
|
||||
val channelUpdates by channel.live.observeAsState()
|
||||
|
||||
val name = remember(channelUpdates) { channel.toBestDisplayName() }
|
||||
val description = remember(channelUpdates) { channel.summary() }
|
||||
val name = remember(channelUpdates) { channelUpdates?.channel?.toBestDisplayName() ?: "" }
|
||||
val description = remember(channelUpdates) { channelUpdates?.channel?.summary() }
|
||||
val cover by remember(channelUpdates) {
|
||||
derivedStateOf {
|
||||
channel.profilePicture()?.ifBlank { null }
|
||||
channelUpdates?.channel?.profilePicture()?.ifBlank { null }
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -35,13 +35,16 @@ import kotlinx.coroutines.launch
|
||||
|
||||
@OptIn(ExperimentalFoundationApi::class)
|
||||
@Composable
|
||||
fun MessageSetCompose(messageSetCard: MessageSetCard, routeForLastRead: String, accountViewModel: AccountViewModel, nav: (String) -> Unit) {
|
||||
fun MessageSetCompose(messageSetCard: MessageSetCard, routeForLastRead: String, showHidden: Boolean = false, accountViewModel: AccountViewModel, nav: (String) -> Unit) {
|
||||
val baseNote = remember { messageSetCard.note }
|
||||
|
||||
val accountState by accountViewModel.accountLiveData.observeAsState()
|
||||
val loggedIn = remember(accountState) { accountState?.account?.userProfile() } ?: return
|
||||
|
||||
var popupExpanded by remember { mutableStateOf(false) }
|
||||
val popupExpanded = remember { mutableStateOf(false) }
|
||||
val enablePopup = remember {
|
||||
{ popupExpanded.value = true }
|
||||
}
|
||||
|
||||
val scope = rememberCoroutineScope()
|
||||
|
||||
@@ -84,7 +87,7 @@ fun MessageSetCompose(messageSetCard: MessageSetCard, routeForLastRead: String,
|
||||
)?.let { nav(it) }
|
||||
}
|
||||
},
|
||||
onLongClick = { popupExpanded = true }
|
||||
onLongClick = enablePopup
|
||||
)
|
||||
.fillMaxWidth()
|
||||
}
|
||||
@@ -99,12 +102,13 @@ fun MessageSetCompose(messageSetCard: MessageSetCard, routeForLastRead: String,
|
||||
routeForLastRead = null,
|
||||
isBoostedNote = true,
|
||||
addMarginTop = false,
|
||||
showHidden = showHidden,
|
||||
parentBackgroundColor = backgroundColor,
|
||||
accountViewModel = accountViewModel,
|
||||
nav = nav
|
||||
)
|
||||
|
||||
NoteDropDownMenu(baseNote, popupExpanded, { popupExpanded = false }, accountViewModel)
|
||||
NoteDropDownMenu(baseNote, popupExpanded, accountViewModel)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -32,7 +32,6 @@ import androidx.compose.runtime.livedata.observeAsState
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.rememberCoroutineScope
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
@@ -86,10 +85,14 @@ import kotlin.time.measureTimedValue
|
||||
|
||||
@OptIn(ExperimentalFoundationApi::class, ExperimentalTime::class)
|
||||
@Composable
|
||||
fun MultiSetCompose(multiSetCard: MultiSetCard, routeForLastRead: String, accountViewModel: AccountViewModel, nav: (String) -> Unit) {
|
||||
fun MultiSetCompose(multiSetCard: MultiSetCard, routeForLastRead: String, showHidden: Boolean = false, accountViewModel: AccountViewModel, nav: (String) -> Unit) {
|
||||
val baseNote = remember { multiSetCard.note }
|
||||
|
||||
var popupExpanded by remember { mutableStateOf(false) }
|
||||
val popupExpanded = remember { mutableStateOf(false) }
|
||||
val enablePopup = remember {
|
||||
{ popupExpanded.value = true }
|
||||
}
|
||||
|
||||
val scope = rememberCoroutineScope()
|
||||
|
||||
val defaultBackgroundColor = MaterialTheme.colors.background
|
||||
@@ -130,7 +133,7 @@ fun MultiSetCompose(multiSetCard: MultiSetCard, routeForLastRead: String, accoun
|
||||
routeFor(baseNote, accountViewModel.userProfile())?.let { nav(it) }
|
||||
}
|
||||
},
|
||||
onLongClick = { popupExpanded = true }
|
||||
onLongClick = enablePopup
|
||||
)
|
||||
.fillMaxWidth()
|
||||
}
|
||||
@@ -150,6 +153,7 @@ fun MultiSetCompose(multiSetCard: MultiSetCard, routeForLastRead: String, accoun
|
||||
routeForLastRead = null,
|
||||
modifier = remember { Modifier.padding(top = 5.dp) },
|
||||
isBoostedNote = true,
|
||||
showHidden = showHidden,
|
||||
parentBackgroundColor = backgroundColor,
|
||||
accountViewModel = accountViewModel,
|
||||
nav = nav
|
||||
@@ -157,7 +161,7 @@ fun MultiSetCompose(multiSetCard: MultiSetCard, routeForLastRead: String, accoun
|
||||
}
|
||||
Log.d("Rendering Metrics", "Complete: ${baseNote.event?.content()?.split("\n")?.getOrNull(0)?.take(15)}.. $elapsed")
|
||||
|
||||
NoteDropDownMenu(baseNote, popupExpanded, { popupExpanded = false }, accountViewModel)
|
||||
NoteDropDownMenu(baseNote, popupExpanded, accountViewModel)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -558,7 +562,7 @@ fun WatchUserMetadataAndFollowsAndRenderUserProfilePicture(
|
||||
}
|
||||
}
|
||||
|
||||
WatchFollows(author.pubkeyHex, accountViewModel) { isFollowing ->
|
||||
WatchUserFollows(author.pubkeyHex, accountViewModel) { isFollowing ->
|
||||
Crossfade(targetState = isFollowing) {
|
||||
if (it) {
|
||||
Box(modifier = Size35Modifier, contentAlignment = Alignment.TopEnd) {
|
||||
|
||||
@@ -183,7 +183,7 @@ private fun NIP05VerifiedSymbol(nip05Verified: MutableState<Boolean?>) {
|
||||
} else if (nip05Verified.value == true) {
|
||||
Icon(
|
||||
painter = painterResource(R.drawable.ic_verified_transparent),
|
||||
"NIP-05 Verified",
|
||||
"Nostr Address Verified",
|
||||
tint = Nip05.copy(0.52f),
|
||||
modifier = Modifier
|
||||
.size(14.dp)
|
||||
@@ -193,7 +193,7 @@ private fun NIP05VerifiedSymbol(nip05Verified: MutableState<Boolean?>) {
|
||||
Icon(
|
||||
tint = Color.Red,
|
||||
imageVector = Icons.Default.Report,
|
||||
contentDescription = "Invalid Nip05",
|
||||
contentDescription = "Invalid Nostr Address",
|
||||
modifier = Modifier
|
||||
.size(14.dp)
|
||||
.padding(top = 1.dp)
|
||||
|
||||
@@ -130,6 +130,7 @@ import com.vitorpamplona.amethyst.ui.components.ClickableUrl
|
||||
import com.vitorpamplona.amethyst.ui.components.CreateClickableTextWithEmoji
|
||||
import com.vitorpamplona.amethyst.ui.components.CreateTextWithEmoji
|
||||
import com.vitorpamplona.amethyst.ui.components.LoadThumbAndThenVideoView
|
||||
import com.vitorpamplona.amethyst.ui.components.MeasureSpaceWidth
|
||||
import com.vitorpamplona.amethyst.ui.components.ObserveDisplayNip05Status
|
||||
import com.vitorpamplona.amethyst.ui.components.RobohashAsyncImageProxy
|
||||
import com.vitorpamplona.amethyst.ui.components.SensitivityWarning
|
||||
@@ -213,6 +214,7 @@ fun NoteCompose(
|
||||
unPackReply: Boolean = true,
|
||||
makeItShort: Boolean = false,
|
||||
addMarginTop: Boolean = true,
|
||||
showHidden: Boolean = false,
|
||||
parentBackgroundColor: MutableState<Color>? = null,
|
||||
accountViewModel: AccountViewModel,
|
||||
nav: (String) -> Unit
|
||||
@@ -244,6 +246,7 @@ fun NoteCompose(
|
||||
unPackReply,
|
||||
makeItShort,
|
||||
addMarginTop,
|
||||
showHidden,
|
||||
parentBackgroundColor,
|
||||
accountViewModel,
|
||||
nav
|
||||
@@ -262,30 +265,59 @@ fun CheckHiddenNoteCompose(
|
||||
unPackReply: Boolean = true,
|
||||
makeItShort: Boolean = false,
|
||||
addMarginTop: Boolean = true,
|
||||
showHidden: Boolean = false,
|
||||
parentBackgroundColor: MutableState<Color>? = null,
|
||||
accountViewModel: AccountViewModel,
|
||||
nav: (String) -> Unit
|
||||
) {
|
||||
val isHidden by accountViewModel.accountLiveData.map {
|
||||
accountViewModel.isNoteHidden(note)
|
||||
}.distinctUntilChanged().observeAsState(accountViewModel.isNoteHidden(note))
|
||||
|
||||
Crossfade(targetState = isHidden) {
|
||||
if (!it) {
|
||||
LoadedNoteCompose(
|
||||
note,
|
||||
routeForLastRead,
|
||||
modifier,
|
||||
isBoostedNote,
|
||||
isQuotedNote,
|
||||
unPackReply,
|
||||
makeItShort,
|
||||
addMarginTop,
|
||||
parentBackgroundColor,
|
||||
accountViewModel,
|
||||
nav
|
||||
if (showHidden) {
|
||||
// Ignores reports as well
|
||||
var state by remember {
|
||||
mutableStateOf(
|
||||
NoteComposeReportState(
|
||||
isAcceptable = true,
|
||||
canPreview = true,
|
||||
relevantReports = persistentSetOf()
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
RenderReportState(
|
||||
state,
|
||||
note,
|
||||
routeForLastRead,
|
||||
modifier,
|
||||
isBoostedNote,
|
||||
isQuotedNote,
|
||||
unPackReply,
|
||||
makeItShort,
|
||||
addMarginTop,
|
||||
parentBackgroundColor,
|
||||
accountViewModel,
|
||||
nav
|
||||
)
|
||||
} else {
|
||||
val isHidden by accountViewModel.account.liveHiddenUsers.map {
|
||||
note.isHiddenFor(it)
|
||||
}.observeAsState(accountViewModel.isNoteHidden(note))
|
||||
|
||||
Crossfade(targetState = isHidden) {
|
||||
if (!it) {
|
||||
LoadedNoteCompose(
|
||||
note,
|
||||
routeForLastRead,
|
||||
modifier,
|
||||
isBoostedNote,
|
||||
isQuotedNote,
|
||||
unPackReply,
|
||||
makeItShort,
|
||||
addMarginTop,
|
||||
parentBackgroundColor,
|
||||
accountViewModel,
|
||||
nav
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -433,15 +465,19 @@ fun NormalNote(
|
||||
channelNote = baseNote,
|
||||
showVideo = !makeItShort,
|
||||
showBottomDiviser = true,
|
||||
sendToChannel = true,
|
||||
accountViewModel = accountViewModel,
|
||||
nav = nav
|
||||
)
|
||||
is CommunityDefinitionEvent -> CommunityHeader(
|
||||
baseNote = baseNote,
|
||||
showBottomDiviser = true,
|
||||
accountViewModel = accountViewModel,
|
||||
nav = nav
|
||||
)
|
||||
is CommunityDefinitionEvent -> (baseNote as? AddressableNote)?.let {
|
||||
CommunityHeader(
|
||||
baseNote = it,
|
||||
showBottomDiviser = true,
|
||||
sendToCommunity = true,
|
||||
accountViewModel = accountViewModel,
|
||||
nav = nav
|
||||
)
|
||||
}
|
||||
is BadgeDefinitionEvent -> BadgeDisplay(baseNote = baseNote)
|
||||
is FileHeaderEvent -> FileHeaderDisplay(baseNote, accountViewModel)
|
||||
is FileStorageHeaderEvent -> FileStorageHeaderDisplay(baseNote, accountViewModel)
|
||||
@@ -468,17 +504,24 @@ fun NormalNote(
|
||||
|
||||
@Composable
|
||||
fun CommunityHeader(
|
||||
baseNote: Note,
|
||||
baseNote: AddressableNote,
|
||||
showBottomDiviser: Boolean,
|
||||
sendToCommunity: Boolean,
|
||||
modifier: Modifier = StdPadding,
|
||||
accountViewModel: AccountViewModel,
|
||||
nav: (String) -> Unit
|
||||
) {
|
||||
var expanded = remember { mutableStateOf(false) }
|
||||
val expanded = remember { mutableStateOf(false) }
|
||||
|
||||
Column(
|
||||
modifier = modifier.clickable {
|
||||
expanded.value = !expanded.value
|
||||
if (sendToCommunity) {
|
||||
routeFor(baseNote, accountViewModel.userProfile())?.let {
|
||||
nav(it)
|
||||
}
|
||||
} else {
|
||||
expanded.value = !expanded.value
|
||||
}
|
||||
}
|
||||
) {
|
||||
ShortCommunityHeader(baseNote, expanded, accountViewModel, nav)
|
||||
@@ -496,7 +539,7 @@ fun CommunityHeader(
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun LongCommunityHeader(baseNote: Note, accountViewModel: AccountViewModel, nav: (String) -> Unit) {
|
||||
fun LongCommunityHeader(baseNote: AddressableNote, accountViewModel: AccountViewModel, nav: (String) -> Unit) {
|
||||
val noteState by baseNote.live().metadata.observeAsState()
|
||||
val noteEvent = remember(noteState) { noteState?.note?.event as? CommunityDefinitionEvent } ?: return
|
||||
|
||||
@@ -638,7 +681,7 @@ fun LongCommunityHeader(baseNote: Note, accountViewModel: AccountViewModel, nav:
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun ShortCommunityHeader(baseNote: Note, expanded: MutableState<Boolean>, accountViewModel: AccountViewModel, nav: (String) -> Unit) {
|
||||
fun ShortCommunityHeader(baseNote: AddressableNote, expanded: MutableState<Boolean>, accountViewModel: AccountViewModel, nav: (String) -> Unit) {
|
||||
val noteState by baseNote.live().metadata.observeAsState()
|
||||
val noteEvent = remember(noteState) { noteState?.note?.event as? CommunityDefinitionEvent } ?: return
|
||||
|
||||
@@ -703,44 +746,45 @@ fun ShortCommunityHeader(baseNote: Note, expanded: MutableState<Boolean>, accoun
|
||||
|
||||
@Composable
|
||||
private fun ShortCommunityActionOptions(
|
||||
note: Note,
|
||||
note: AddressableNote,
|
||||
accountViewModel: AccountViewModel,
|
||||
nav: (String) -> Unit
|
||||
) {
|
||||
val accountState by accountViewModel.accountLiveData.observeAsState()
|
||||
val isFollowing by remember(accountState) {
|
||||
derivedStateOf {
|
||||
accountState?.account?.followingCommunities?.contains(note.idHex) ?: false
|
||||
}
|
||||
}
|
||||
|
||||
Spacer(modifier = StdHorzSpacer)
|
||||
LikeReaction(baseNote = note, grayTint = MaterialTheme.colors.onSurface, accountViewModel = accountViewModel, nav)
|
||||
Spacer(modifier = StdHorzSpacer)
|
||||
ZapReaction(baseNote = note, grayTint = MaterialTheme.colors.onSurface, accountViewModel = accountViewModel)
|
||||
|
||||
if (!isFollowing) {
|
||||
Spacer(modifier = StdHorzSpacer)
|
||||
JoinCommunityButton(accountViewModel, note, nav)
|
||||
WatchAddressableNoteFollows(note, accountViewModel) { isFollowing ->
|
||||
if (!isFollowing) {
|
||||
Spacer(modifier = StdHorzSpacer)
|
||||
JoinCommunityButton(accountViewModel, note, nav)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun WatchAddressableNoteFollows(note: AddressableNote, accountViewModel: AccountViewModel, onFollowChanges: @Composable (Boolean) -> Unit) {
|
||||
val showFollowingMark by accountViewModel.userFollows.map {
|
||||
it.user.latestContactList?.isTaggedAddressableNote(note.idHex) ?: false
|
||||
}.distinctUntilChanged().observeAsState(
|
||||
accountViewModel.userProfile().latestContactList?.isTaggedAddressableNote(note.idHex) ?: false
|
||||
)
|
||||
|
||||
onFollowChanges(showFollowingMark)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun LongCommunityActionOptions(
|
||||
note: Note,
|
||||
note: AddressableNote,
|
||||
accountViewModel: AccountViewModel,
|
||||
nav: (String) -> Unit
|
||||
) {
|
||||
val accountState by accountViewModel.accountLiveData.observeAsState()
|
||||
val isFollowing by remember(accountState) {
|
||||
derivedStateOf {
|
||||
accountState?.account?.followingCommunities?.contains(note.idHex) ?: false
|
||||
WatchAddressableNoteFollows(note, accountViewModel) { isFollowing ->
|
||||
if (isFollowing) {
|
||||
LeaveCommunityButton(accountViewModel, note, nav)
|
||||
}
|
||||
}
|
||||
|
||||
if (isFollowing) {
|
||||
LeaveCommunityButton(accountViewModel, note, nav)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
@@ -1148,6 +1192,10 @@ fun routeFor(note: Note, loggedIn: User): String? {
|
||||
return null
|
||||
}
|
||||
|
||||
fun routeFor(note: Channel): String {
|
||||
return "Channel/${note.idHex}"
|
||||
}
|
||||
|
||||
fun routeFor(user: User): String {
|
||||
return "User/${user.pubkeyHex}"
|
||||
}
|
||||
@@ -1441,11 +1489,15 @@ private fun RenderHighlight(
|
||||
val url = remember() {
|
||||
(note.event as? HighlightEvent)?.inUrl()
|
||||
}
|
||||
val postHex = remember() {
|
||||
(note.event as? HighlightEvent)?.taggedAddresses()?.firstOrNull()
|
||||
}
|
||||
|
||||
DisplayHighlight(
|
||||
quote,
|
||||
author,
|
||||
url,
|
||||
postHex,
|
||||
makeItShort,
|
||||
canPreview,
|
||||
backgroundColor,
|
||||
@@ -2298,6 +2350,7 @@ private fun ReplyRow(
|
||||
channelHex = channelHex,
|
||||
showVideo = false,
|
||||
showBottomDiviser = false,
|
||||
sendToChannel = true,
|
||||
modifier = remember { Modifier.padding(vertical = 5.dp) },
|
||||
accountViewModel = accountViewModel,
|
||||
nav = nav
|
||||
@@ -2428,28 +2481,35 @@ fun MoreOptionsButton(
|
||||
baseNote: Note,
|
||||
accountViewModel: AccountViewModel
|
||||
) {
|
||||
var moreActionsExpanded by remember { mutableStateOf(false) }
|
||||
val popupExpanded = remember { mutableStateOf(false) }
|
||||
val enablePopup = remember {
|
||||
{ popupExpanded.value = true }
|
||||
}
|
||||
|
||||
IconButton(
|
||||
modifier = Size24Modifier,
|
||||
onClick = { moreActionsExpanded = true }
|
||||
onClick = enablePopup
|
||||
) {
|
||||
Icon(
|
||||
imageVector = Icons.Default.MoreVert,
|
||||
null,
|
||||
modifier = Size15Modifier,
|
||||
tint = MaterialTheme.colors.placeholderText
|
||||
)
|
||||
VerticalDotsIcon()
|
||||
|
||||
NoteDropDownMenu(
|
||||
baseNote,
|
||||
moreActionsExpanded,
|
||||
{ moreActionsExpanded = false },
|
||||
popupExpanded,
|
||||
accountViewModel
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun VerticalDotsIcon() {
|
||||
Icon(
|
||||
imageVector = Icons.Default.MoreVert,
|
||||
null,
|
||||
modifier = Size15Modifier,
|
||||
tint = MaterialTheme.colors.placeholderText
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun TimeAgo(note: Note) {
|
||||
val time = remember(note) { note.createdAt() } ?: return
|
||||
@@ -2613,6 +2673,7 @@ fun DisplayHighlight(
|
||||
highlight: String,
|
||||
authorHex: String?,
|
||||
url: String?,
|
||||
postAddress: ATag?,
|
||||
makeItShort: Boolean,
|
||||
canPreview: Boolean,
|
||||
backgroundColor: MutableState<Color>,
|
||||
@@ -2637,13 +2698,15 @@ fun DisplayHighlight(
|
||||
nav
|
||||
)
|
||||
|
||||
DisplayQuoteAuthor(authorHex ?: "", url, nav)
|
||||
DisplayQuoteAuthor(authorHex ?: "", url, postAddress, accountViewModel, nav)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun DisplayQuoteAuthor(
|
||||
authorHex: String,
|
||||
url: String?,
|
||||
postAddress: ATag?,
|
||||
accountViewModel: AccountViewModel,
|
||||
nav: (String) -> Unit
|
||||
) {
|
||||
var userBase by remember { mutableStateOf<User?>(LocalCache.getUserIfExists(authorHex)) }
|
||||
@@ -2659,13 +2722,47 @@ private fun DisplayQuoteAuthor(
|
||||
}
|
||||
}
|
||||
|
||||
Row {
|
||||
userBase?.let { userBase ->
|
||||
LoadAndDisplayUser(userBase, nav)
|
||||
}
|
||||
MeasureSpaceWidth {
|
||||
Row(horizontalArrangement = Arrangement.spacedBy(it), verticalAlignment = Alignment.CenterVertically) {
|
||||
userBase?.let { userBase ->
|
||||
LoadAndDisplayUser(userBase, nav)
|
||||
}
|
||||
|
||||
url?.let { url ->
|
||||
LoadAndDisplayUrl(url)
|
||||
url?.let { url ->
|
||||
LoadAndDisplayUrl(url)
|
||||
}
|
||||
|
||||
postAddress?.let { address ->
|
||||
LoadAndDisplayPost(address, accountViewModel, nav)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun LoadAndDisplayPost(postAddress: ATag, accountViewModel: AccountViewModel, nav: (String) -> Unit) {
|
||||
LoadAddressableNote(aTag = postAddress) {
|
||||
it?.let { note ->
|
||||
val noteEvent by note.live().metadata.map {
|
||||
it.note.event
|
||||
}.distinctUntilChanged().observeAsState(note.event)
|
||||
|
||||
val title = remember(noteEvent) {
|
||||
(noteEvent as? LongTextNoteEvent)?.title()
|
||||
}
|
||||
|
||||
title?.let {
|
||||
Text(remember { "-" }, maxLines = 1)
|
||||
ClickableText(
|
||||
text = AnnotatedString(title),
|
||||
onClick = {
|
||||
routeFor(note, accountViewModel.userProfile())?.let {
|
||||
nav(it)
|
||||
}
|
||||
},
|
||||
style = LocalTextStyle.current.copy(color = MaterialTheme.colors.primary)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -2682,7 +2779,7 @@ private fun LoadAndDisplayUrl(url: String) {
|
||||
}
|
||||
|
||||
validatedUrl?.host?.let { host ->
|
||||
Text(remember { "on " }, maxLines = 1)
|
||||
Text(remember { "-" }, maxLines = 1)
|
||||
ClickableUrl(urlText = host, url = url)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -363,6 +363,20 @@ private fun WatchReactionsZapsBoostsAndDisplayIfExists(baseNote: Note, content:
|
||||
}
|
||||
}
|
||||
|
||||
fun <T, K, R> LiveData<T>.combineWith(
|
||||
liveData1: LiveData<K>,
|
||||
block: (T?, K?) -> R
|
||||
): LiveData<R> {
|
||||
val result = MediatorLiveData<R>()
|
||||
result.addSource(this) {
|
||||
result.value = block(this.value, liveData1.value)
|
||||
}
|
||||
result.addSource(liveData1) {
|
||||
result.value = block(this.value, liveData1.value)
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
fun <T, K, P, R> LiveData<T>.combineWith(
|
||||
liveData1: LiveData<K>,
|
||||
liveData2: LiveData<P>,
|
||||
|
||||
@@ -20,6 +20,7 @@ import androidx.compose.material.ripple.rememberRipple
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.Immutable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.MutableState
|
||||
import androidx.compose.runtime.derivedStateOf
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.livedata.observeAsState
|
||||
@@ -263,7 +264,7 @@ fun PictureAndFollowingMark(
|
||||
|
||||
@Composable
|
||||
fun ObserveAndDisplayFollowingMark(userHex: String, iconSize: Dp, accountViewModel: AccountViewModel) {
|
||||
WatchFollows(userHex, accountViewModel) { newFollowingState ->
|
||||
WatchUserFollows(userHex, accountViewModel) { newFollowingState ->
|
||||
Crossfade(targetState = newFollowingState) { following ->
|
||||
if (following) {
|
||||
Box(contentAlignment = Alignment.TopEnd) {
|
||||
@@ -275,7 +276,7 @@ fun ObserveAndDisplayFollowingMark(userHex: String, iconSize: Dp, accountViewMod
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun WatchFollows(userHex: String, accountViewModel: AccountViewModel, onFollowChanges: @Composable (Boolean) -> Unit) {
|
||||
fun WatchUserFollows(userHex: String, accountViewModel: AccountViewModel, onFollowChanges: @Composable (Boolean) -> Unit) {
|
||||
val showFollowingMark by accountViewModel.userFollows.map {
|
||||
it.user.isFollowingCached(userHex) || (userHex == accountViewModel.account.userProfile().pubkeyHex)
|
||||
}.distinctUntilChanged().observeAsState(
|
||||
@@ -310,7 +311,7 @@ data class DropDownParams(
|
||||
)
|
||||
|
||||
@Composable
|
||||
fun NoteDropDownMenu(note: Note, popupExpanded: Boolean, onDismiss: () -> Unit, accountViewModel: AccountViewModel) {
|
||||
fun NoteDropDownMenu(note: Note, popupExpanded: MutableState<Boolean>, accountViewModel: AccountViewModel) {
|
||||
var reportDialogShowing by remember { mutableStateOf(false) }
|
||||
|
||||
var state by remember {
|
||||
@@ -326,8 +327,12 @@ fun NoteDropDownMenu(note: Note, popupExpanded: Boolean, onDismiss: () -> Unit,
|
||||
)
|
||||
}
|
||||
|
||||
val onDismiss = remember(popupExpanded) {
|
||||
{ popupExpanded.value = false }
|
||||
}
|
||||
|
||||
DropdownMenu(
|
||||
expanded = popupExpanded,
|
||||
expanded = popupExpanded.value,
|
||||
onDismissRequest = onDismiss
|
||||
) {
|
||||
val clipboardManager = LocalClipboardManager.current
|
||||
|
||||
@@ -174,7 +174,9 @@ fun UserActionOptions(
|
||||
) {
|
||||
val scope = rememberCoroutineScope()
|
||||
val accountState by accountViewModel.accountLiveData.observeAsState()
|
||||
val isHidden by remember(accountState) {
|
||||
val blockList by accountViewModel.account.getBlockListNote().live().metadata.observeAsState()
|
||||
|
||||
val isHidden by remember(accountState, blockList) {
|
||||
derivedStateOf {
|
||||
accountState?.account?.isHidden(baseAuthor) ?: false
|
||||
}
|
||||
|
||||
@@ -141,9 +141,10 @@ fun RenderCardFeed(
|
||||
FeedLoaded(
|
||||
state = state,
|
||||
listState = listState,
|
||||
routeForLastRead = routeForLastRead,
|
||||
showHidden = viewModel.showHidden(),
|
||||
accountViewModel = accountViewModel,
|
||||
nav = nav,
|
||||
routeForLastRead = routeForLastRead
|
||||
nav = nav
|
||||
)
|
||||
}
|
||||
CardFeedState.Loading -> {
|
||||
@@ -158,9 +159,10 @@ fun RenderCardFeed(
|
||||
private fun FeedLoaded(
|
||||
state: CardFeedState.Loaded,
|
||||
listState: LazyListState,
|
||||
showHidden: Boolean,
|
||||
routeForLastRead: String,
|
||||
accountViewModel: AccountViewModel,
|
||||
nav: (String) -> Unit,
|
||||
routeForLastRead: String
|
||||
nav: (String) -> Unit
|
||||
) {
|
||||
LazyColumn(
|
||||
modifier = remember { Modifier.fillMaxSize() },
|
||||
@@ -179,7 +181,7 @@ private fun FeedLoaded(
|
||||
}
|
||||
|
||||
Row(defaultModifier) {
|
||||
RenderCardItem(item, accountViewModel, nav, routeForLastRead)
|
||||
RenderCardItem(item, routeForLastRead, showHidden, accountViewModel, nav)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -188,15 +190,17 @@ private fun FeedLoaded(
|
||||
@Composable
|
||||
private fun RenderCardItem(
|
||||
item: Card,
|
||||
routeForLastRead: String,
|
||||
showHidden: Boolean,
|
||||
accountViewModel: AccountViewModel,
|
||||
nav: (String) -> Unit,
|
||||
routeForLastRead: String
|
||||
nav: (String) -> Unit
|
||||
) {
|
||||
when (item) {
|
||||
is NoteCard -> NoteCompose(
|
||||
item,
|
||||
isBoostedNote = false,
|
||||
accountViewModel = accountViewModel,
|
||||
showHidden = showHidden,
|
||||
nav = nav,
|
||||
routeForLastRead = routeForLastRead
|
||||
)
|
||||
@@ -212,6 +216,7 @@ private fun RenderCardItem(
|
||||
is MultiSetCard -> MultiSetCompose(
|
||||
item,
|
||||
accountViewModel = accountViewModel,
|
||||
showHidden = showHidden,
|
||||
nav = nav,
|
||||
routeForLastRead = routeForLastRead
|
||||
)
|
||||
@@ -219,6 +224,7 @@ private fun RenderCardItem(
|
||||
is BadgeCard -> BadgeCompose(
|
||||
item,
|
||||
accountViewModel = accountViewModel,
|
||||
showHidden = showHidden,
|
||||
nav = nav,
|
||||
routeForLastRead = routeForLastRead
|
||||
)
|
||||
@@ -226,6 +232,7 @@ private fun RenderCardItem(
|
||||
is MessageSetCard -> MessageSetCompose(
|
||||
messageSetCard = item,
|
||||
routeForLastRead = routeForLastRead,
|
||||
showHidden = showHidden,
|
||||
accountViewModel = accountViewModel,
|
||||
nav = nav
|
||||
)
|
||||
@@ -242,6 +249,7 @@ fun NoteCompose(
|
||||
unPackReply: Boolean = true,
|
||||
makeItShort: Boolean = false,
|
||||
addMarginTop: Boolean = true,
|
||||
showHidden: Boolean = false,
|
||||
parentBackgroundColor: MutableState<Color>? = null,
|
||||
accountViewModel: AccountViewModel,
|
||||
nav: (String) -> Unit
|
||||
@@ -259,6 +267,7 @@ fun NoteCompose(
|
||||
unPackReply = unPackReply,
|
||||
makeItShort = makeItShort,
|
||||
addMarginTop = addMarginTop,
|
||||
showHidden = showHidden,
|
||||
parentBackgroundColor = parentBackgroundColor,
|
||||
accountViewModel = accountViewModel,
|
||||
nav = nav
|
||||
|
||||
@@ -71,6 +71,10 @@ open class CardFeedViewModel(val localFilter: FeedFilter<Note>) : ViewModel() {
|
||||
scrolltoTopPending = false
|
||||
}
|
||||
|
||||
fun showHidden(): Boolean {
|
||||
return localFilter.showHiddenKey()
|
||||
}
|
||||
|
||||
private var lastAccount: Account? = null
|
||||
private var lastNotes: Set<Note>? = null
|
||||
|
||||
|
||||
@@ -151,6 +151,7 @@ private fun RenderFeed(
|
||||
state,
|
||||
listState,
|
||||
routeForLastRead,
|
||||
viewModel.showHidden(),
|
||||
accountViewModel,
|
||||
nav
|
||||
)
|
||||
@@ -184,6 +185,7 @@ private fun FeedLoaded(
|
||||
state: FeedState.Loaded,
|
||||
listState: LazyListState,
|
||||
routeForLastRead: String?,
|
||||
showHidden: Boolean = false,
|
||||
accountViewModel: AccountViewModel,
|
||||
nav: (String) -> Unit
|
||||
) {
|
||||
@@ -210,6 +212,7 @@ private fun FeedLoaded(
|
||||
routeForLastRead = routeForLastRead,
|
||||
modifier = baseModifier,
|
||||
isBoostedNote = false,
|
||||
showHidden = showHidden,
|
||||
accountViewModel = accountViewModel,
|
||||
nav = nav
|
||||
)
|
||||
|
||||
@@ -217,6 +217,10 @@ abstract class FeedViewModel(val localFilter: FeedFilter<Note>) : ViewModel(), I
|
||||
scrolltoTopPending = false
|
||||
}
|
||||
|
||||
fun showHidden(): Boolean {
|
||||
return localFilter.showHiddenKey()
|
||||
}
|
||||
|
||||
private fun refresh() {
|
||||
val scope = CoroutineScope(Job() + Dispatchers.Default)
|
||||
scope.launch {
|
||||
|
||||
@@ -240,7 +240,10 @@ fun NoteMaster(
|
||||
|
||||
val context = LocalContext.current
|
||||
|
||||
var moreActionsExpanded by remember { mutableStateOf(false) }
|
||||
val moreActionsExpanded = remember { mutableStateOf(false) }
|
||||
val enablePopup = remember {
|
||||
{ moreActionsExpanded.value = true }
|
||||
}
|
||||
|
||||
val noteEvent = note?.event
|
||||
|
||||
@@ -310,7 +313,7 @@ fun NoteMaster(
|
||||
|
||||
IconButton(
|
||||
modifier = Modifier.then(Modifier.size(24.dp)),
|
||||
onClick = { moreActionsExpanded = true }
|
||||
onClick = enablePopup
|
||||
) {
|
||||
Icon(
|
||||
imageVector = Icons.Default.MoreVert,
|
||||
@@ -319,7 +322,7 @@ fun NoteMaster(
|
||||
tint = MaterialTheme.colors.placeholderText
|
||||
)
|
||||
|
||||
NoteDropDownMenu(baseNote, moreActionsExpanded, { moreActionsExpanded = false }, accountViewModel)
|
||||
NoteDropDownMenu(baseNote, moreActionsExpanded, accountViewModel)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -363,6 +366,7 @@ fun NoteMaster(
|
||||
channelHex = note.channelHex()!!,
|
||||
showVideo = true,
|
||||
showBottomDiviser = false,
|
||||
sendToChannel = true,
|
||||
accountViewModel = accountViewModel,
|
||||
nav = nav
|
||||
)
|
||||
@@ -411,6 +415,7 @@ fun NoteMaster(
|
||||
noteEvent.quote(),
|
||||
noteEvent.author(),
|
||||
noteEvent.inUrl(),
|
||||
noteEvent.inPost(),
|
||||
false,
|
||||
true,
|
||||
backgroundColor,
|
||||
|
||||
@@ -13,6 +13,7 @@ import com.vitorpamplona.amethyst.service.checkNotInMainThread
|
||||
import com.vitorpamplona.amethyst.ui.components.BundledUpdate
|
||||
import com.vitorpamplona.amethyst.ui.dal.FeedFilter
|
||||
import com.vitorpamplona.amethyst.ui.dal.HiddenAccountsFeedFilter
|
||||
import com.vitorpamplona.amethyst.ui.dal.SpammerAccountsFeedFilter
|
||||
import com.vitorpamplona.amethyst.ui.dal.UserProfileFollowersFeedFilter
|
||||
import com.vitorpamplona.amethyst.ui.dal.UserProfileFollowsFeedFilter
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
@@ -49,6 +50,14 @@ class NostrHiddenAccountsFeedViewModel(val account: Account) : UserFeedViewModel
|
||||
}
|
||||
}
|
||||
|
||||
class NostrSpammerAccountsFeedViewModel(val account: Account) : UserFeedViewModel(SpammerAccountsFeedFilter(account)) {
|
||||
class Factory(val account: Account) : ViewModelProvider.Factory {
|
||||
override fun <NostrSpammerAccountsFeedViewModel : ViewModel> create(modelClass: Class<NostrSpammerAccountsFeedViewModel>): NostrSpammerAccountsFeedViewModel {
|
||||
return NostrSpammerAccountsFeedViewModel(account) as NostrSpammerAccountsFeedViewModel
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Stable
|
||||
open class UserFeedViewModel(val dataSource: FeedFilter<User>) : ViewModel(), InvalidatableViewModel {
|
||||
private val _feedContent = MutableStateFlow<UserFeedState>(UserFeedState.Loading)
|
||||
@@ -61,6 +70,10 @@ open class UserFeedViewModel(val dataSource: FeedFilter<User>) : ViewModel(), In
|
||||
}
|
||||
}
|
||||
|
||||
fun showHidden(): Boolean {
|
||||
return dataSource.showHiddenKey()
|
||||
}
|
||||
|
||||
private fun refreshSuspended() {
|
||||
checkNotInMainThread()
|
||||
|
||||
|
||||
@@ -80,6 +80,7 @@ import androidx.lifecycle.distinctUntilChanged
|
||||
import androidx.lifecycle.map
|
||||
import androidx.lifecycle.viewmodel.compose.viewModel
|
||||
import com.vitorpamplona.amethyst.R
|
||||
import com.vitorpamplona.amethyst.model.AddressableNote
|
||||
import com.vitorpamplona.amethyst.model.Channel
|
||||
import com.vitorpamplona.amethyst.model.LiveActivitiesChannel
|
||||
import com.vitorpamplona.amethyst.model.LocalCache
|
||||
@@ -107,6 +108,7 @@ import com.vitorpamplona.amethyst.ui.note.ChatroomMessageCompose
|
||||
import com.vitorpamplona.amethyst.ui.note.ClickableUserPicture
|
||||
import com.vitorpamplona.amethyst.ui.note.DisplayUncitedHashtags
|
||||
import com.vitorpamplona.amethyst.ui.note.LikeReaction
|
||||
import com.vitorpamplona.amethyst.ui.note.LoadChannel
|
||||
import com.vitorpamplona.amethyst.ui.note.MoreOptionsButton
|
||||
import com.vitorpamplona.amethyst.ui.note.NoteAuthorPicture
|
||||
import com.vitorpamplona.amethyst.ui.note.NoteUsernameDisplay
|
||||
@@ -114,6 +116,7 @@ import com.vitorpamplona.amethyst.ui.note.TimeAgo
|
||||
import com.vitorpamplona.amethyst.ui.note.UserPicture
|
||||
import com.vitorpamplona.amethyst.ui.note.UsernameDisplay
|
||||
import com.vitorpamplona.amethyst.ui.note.ZapReaction
|
||||
import com.vitorpamplona.amethyst.ui.note.routeFor
|
||||
import com.vitorpamplona.amethyst.ui.note.timeAgo
|
||||
import com.vitorpamplona.amethyst.ui.screen.NostrChannelFeedViewModel
|
||||
import com.vitorpamplona.amethyst.ui.screen.RefreshingChatroomFeedView
|
||||
@@ -143,18 +146,7 @@ fun ChannelScreen(
|
||||
) {
|
||||
if (channelId == null) return
|
||||
|
||||
var channelBase by remember { mutableStateOf<Channel?>(LocalCache.getChannelIfExists(channelId)) }
|
||||
|
||||
LaunchedEffect(channelId) {
|
||||
withContext(Dispatchers.IO) {
|
||||
val newChannelBase = LocalCache.checkGetOrCreateChannel(channelId)
|
||||
if (newChannelBase != channelBase) {
|
||||
channelBase = newChannelBase
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
channelBase?.let {
|
||||
LoadChannel(channelId) {
|
||||
PrepareChannelViewModels(
|
||||
baseChannel = it,
|
||||
accountViewModel = accountViewModel,
|
||||
@@ -516,6 +508,7 @@ fun ChannelHeader(
|
||||
channelNote: Note,
|
||||
showVideo: Boolean,
|
||||
showBottomDiviser: Boolean,
|
||||
sendToChannel: Boolean,
|
||||
modifier: Modifier = StdPadding,
|
||||
accountViewModel: AccountViewModel,
|
||||
nav: (String) -> Unit
|
||||
@@ -530,6 +523,7 @@ fun ChannelHeader(
|
||||
channelHex = it,
|
||||
showVideo = showVideo,
|
||||
showBottomDiviser = showBottomDiviser,
|
||||
sendToChannel = sendToChannel,
|
||||
accountViewModel = accountViewModel,
|
||||
nav = nav
|
||||
)
|
||||
@@ -542,26 +536,18 @@ fun ChannelHeader(
|
||||
showVideo: Boolean,
|
||||
showBottomDiviser: Boolean,
|
||||
showFlag: Boolean = true,
|
||||
sendToChannel: Boolean = false,
|
||||
modifier: Modifier = StdPadding,
|
||||
accountViewModel: AccountViewModel,
|
||||
nav: (String) -> Unit
|
||||
) {
|
||||
var baseChannel by remember { mutableStateOf(LocalCache.channels[channelHex]) }
|
||||
|
||||
if (baseChannel == null) {
|
||||
LaunchedEffect(key1 = channelHex) {
|
||||
launch(Dispatchers.IO) {
|
||||
baseChannel = LocalCache.checkGetOrCreateChannel(channelHex)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
baseChannel?.let {
|
||||
LoadChannel(channelHex) {
|
||||
ChannelHeader(
|
||||
it,
|
||||
showVideo,
|
||||
showBottomDiviser,
|
||||
showFlag,
|
||||
sendToChannel,
|
||||
modifier,
|
||||
accountViewModel,
|
||||
nav
|
||||
@@ -575,6 +561,7 @@ fun ChannelHeader(
|
||||
showVideo: Boolean,
|
||||
showBottomDiviser: Boolean,
|
||||
showFlag: Boolean = true,
|
||||
sendToChannel: Boolean = false,
|
||||
modifier: Modifier = StdPadding,
|
||||
accountViewModel: AccountViewModel,
|
||||
nav: (String) -> Unit
|
||||
@@ -588,7 +575,11 @@ fun ChannelHeader(
|
||||
|
||||
Column(
|
||||
modifier = modifier.clickable {
|
||||
expanded.value = !expanded.value
|
||||
if (sendToChannel) {
|
||||
nav(routeFor(baseChannel))
|
||||
} else {
|
||||
expanded.value = !expanded.value
|
||||
}
|
||||
}
|
||||
) {
|
||||
ShortChannelHeader(baseChannel, expanded, accountViewModel, nav, showFlag)
|
||||
@@ -873,13 +864,6 @@ private fun ShortChannelActionOptions(
|
||||
accountViewModel: AccountViewModel,
|
||||
nav: (String) -> Unit
|
||||
) {
|
||||
val accountState by accountViewModel.accountLiveData.observeAsState()
|
||||
val isFollowing by remember(accountState) {
|
||||
derivedStateOf {
|
||||
accountState?.account?.followingChannels?.contains(channel.idHex) ?: false
|
||||
}
|
||||
}
|
||||
|
||||
LoadNote(baseNoteHex = channel.idHex) {
|
||||
it?.let {
|
||||
var popupExpanded by remember { mutableStateOf(false) }
|
||||
@@ -892,11 +876,28 @@ private fun ShortChannelActionOptions(
|
||||
}
|
||||
}
|
||||
|
||||
if (!isFollowing) {
|
||||
JoinChatButton(accountViewModel, channel, nav)
|
||||
WatchChannelFollows(channel, accountViewModel) { isFollowing ->
|
||||
if (!isFollowing) {
|
||||
JoinChatButton(accountViewModel, channel, nav)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun WatchChannelFollows(
|
||||
channel: PublicChatChannel,
|
||||
accountViewModel: AccountViewModel,
|
||||
content: @Composable (Boolean) -> Unit
|
||||
) {
|
||||
val isFollowing by accountViewModel.userProfile().live().follows.map {
|
||||
it.user.latestContactList?.isTaggedEvent(channel.idHex) ?: false
|
||||
}.distinctUntilChanged().observeAsState(
|
||||
accountViewModel.userProfile().latestContactList?.isTaggedEvent(channel.idHex) ?: false
|
||||
)
|
||||
|
||||
content(isFollowing)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun LongChannelActionOptions(
|
||||
channel: PublicChatChannel,
|
||||
@@ -909,19 +910,14 @@ private fun LongChannelActionOptions(
|
||||
}
|
||||
}
|
||||
|
||||
val accountState by accountViewModel.accountLiveData.observeAsState()
|
||||
val isFollowing by remember(accountState) {
|
||||
derivedStateOf {
|
||||
accountState?.account?.followingChannels?.contains(channel.idHex) ?: false
|
||||
}
|
||||
}
|
||||
|
||||
if (isMe) {
|
||||
EditButton(accountViewModel, channel)
|
||||
}
|
||||
|
||||
if (isFollowing) {
|
||||
LeaveChatButton(accountViewModel, channel, nav)
|
||||
WatchChannelFollows(channel, accountViewModel) { isFollowing ->
|
||||
if (isFollowing) {
|
||||
LeaveChatButton(accountViewModel, channel, nav)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1084,10 +1080,14 @@ private fun EditButton(accountViewModel: AccountViewModel, channel: PublicChatCh
|
||||
|
||||
@Composable
|
||||
fun JoinChatButton(accountViewModel: AccountViewModel, channel: Channel, nav: (String) -> Unit) {
|
||||
val scope = rememberCoroutineScope()
|
||||
|
||||
Button(
|
||||
modifier = Modifier.padding(horizontal = 3.dp),
|
||||
onClick = {
|
||||
accountViewModel.account.joinChannel(channel.idHex)
|
||||
scope.launch(Dispatchers.IO) {
|
||||
accountViewModel.account.follow(channel)
|
||||
}
|
||||
},
|
||||
shape = ButtonBorder,
|
||||
colors = ButtonDefaults
|
||||
@@ -1102,10 +1102,14 @@ fun JoinChatButton(accountViewModel: AccountViewModel, channel: Channel, nav: (S
|
||||
|
||||
@Composable
|
||||
fun LeaveChatButton(accountViewModel: AccountViewModel, channel: Channel, nav: (String) -> Unit) {
|
||||
val scope = rememberCoroutineScope()
|
||||
|
||||
Button(
|
||||
modifier = Modifier.padding(horizontal = 3.dp),
|
||||
onClick = {
|
||||
accountViewModel.account.leaveChannel(channel.idHex)
|
||||
scope.launch(Dispatchers.IO) {
|
||||
accountViewModel.account.unfollow(channel)
|
||||
}
|
||||
},
|
||||
shape = ButtonBorder,
|
||||
colors = ButtonDefaults
|
||||
@@ -1119,11 +1123,15 @@ fun LeaveChatButton(accountViewModel: AccountViewModel, channel: Channel, nav: (
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun JoinCommunityButton(accountViewModel: AccountViewModel, note: Note, nav: (String) -> Unit) {
|
||||
fun JoinCommunityButton(accountViewModel: AccountViewModel, note: AddressableNote, nav: (String) -> Unit) {
|
||||
val scope = rememberCoroutineScope()
|
||||
|
||||
Button(
|
||||
modifier = Modifier.padding(horizontal = 3.dp),
|
||||
onClick = {
|
||||
accountViewModel.account.joinCommunity(note.idHex)
|
||||
scope.launch(Dispatchers.IO) {
|
||||
accountViewModel.account.follow(note)
|
||||
}
|
||||
},
|
||||
shape = ButtonBorder,
|
||||
colors = ButtonDefaults
|
||||
@@ -1137,11 +1145,15 @@ fun JoinCommunityButton(accountViewModel: AccountViewModel, note: Note, nav: (St
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun LeaveCommunityButton(accountViewModel: AccountViewModel, note: Note, nav: (String) -> Unit) {
|
||||
fun LeaveCommunityButton(accountViewModel: AccountViewModel, note: AddressableNote, nav: (String) -> Unit) {
|
||||
val scope = rememberCoroutineScope()
|
||||
|
||||
Button(
|
||||
modifier = Modifier.padding(horizontal = 3.dp),
|
||||
onClick = {
|
||||
accountViewModel.account.leaveCommunity((note.idHex))
|
||||
scope.launch(Dispatchers.IO) {
|
||||
accountViewModel.account.unfollow(note)
|
||||
}
|
||||
},
|
||||
shape = ButtonBorder,
|
||||
colors = ButtonDefaults
|
||||
|
||||
@@ -94,7 +94,13 @@ fun CommunityScreen(note: AddressableNote, feedViewModel: NostrCommunityFeedView
|
||||
}
|
||||
|
||||
Column(Modifier.fillMaxSize()) {
|
||||
CommunityHeader(baseNote = note, showBottomDiviser = true, accountViewModel = accountViewModel, nav = nav)
|
||||
CommunityHeader(
|
||||
baseNote = note,
|
||||
showBottomDiviser = true,
|
||||
sendToCommunity = false,
|
||||
accountViewModel = accountViewModel,
|
||||
nav = nav
|
||||
)
|
||||
RefresheableFeedView(
|
||||
feedViewModel,
|
||||
null,
|
||||
|
||||
@@ -35,6 +35,9 @@ import androidx.lifecycle.Lifecycle
|
||||
import androidx.lifecycle.LifecycleEventObserver
|
||||
import com.vitorpamplona.amethyst.R
|
||||
import com.vitorpamplona.amethyst.service.NostrDiscoveryDataSource
|
||||
import com.vitorpamplona.amethyst.service.model.ChannelCreateEvent
|
||||
import com.vitorpamplona.amethyst.service.model.CommunityDefinitionEvent
|
||||
import com.vitorpamplona.amethyst.service.model.LiveActivitiesEvent
|
||||
import com.vitorpamplona.amethyst.ui.navigation.Route
|
||||
import com.vitorpamplona.amethyst.ui.note.ChannelCardCompose
|
||||
import com.vitorpamplona.amethyst.ui.screen.FeedEmpty
|
||||
@@ -92,9 +95,9 @@ fun DiscoverScreen(
|
||||
val tabs by remember(discoveryLiveFeedViewModel, discoveryCommunityFeedViewModel, discoveryChatFeedViewModel) {
|
||||
mutableStateOf(
|
||||
listOf(
|
||||
TabItem(R.string.discover_live, discoveryLiveFeedViewModel, Route.Discover.base + "Live", ScrollStateKeys.DISCOVER_LIVE),
|
||||
TabItem(R.string.discover_community, discoveryCommunityFeedViewModel, Route.Discover.base + "Community", ScrollStateKeys.DISCOVER_COMMUNITY),
|
||||
TabItem(R.string.discover_chat, discoveryChatFeedViewModel, Route.Discover.base + "Chats", ScrollStateKeys.DISCOVER_CHATS)
|
||||
TabItem(R.string.discover_live, discoveryLiveFeedViewModel, Route.Discover.base + "Live", ScrollStateKeys.DISCOVER_LIVE, LiveActivitiesEvent.kind),
|
||||
TabItem(R.string.discover_community, discoveryCommunityFeedViewModel, Route.Discover.base + "Community", ScrollStateKeys.DISCOVER_COMMUNITY, CommunityDefinitionEvent.kind),
|
||||
TabItem(R.string.discover_chat, discoveryChatFeedViewModel, Route.Discover.base + "Chats", ScrollStateKeys.DISCOVER_CHATS, ChannelCreateEvent.kind)
|
||||
).toImmutableList()
|
||||
)
|
||||
}
|
||||
@@ -139,7 +142,14 @@ private fun DiscoverPages(
|
||||
HorizontalPager(pageCount = 3, state = pagerState) { page ->
|
||||
RefresheableView(tabs[page].viewModel, true) {
|
||||
SaveableFeedState(tabs[page].viewModel, scrollStateKey = tabs[page].scrollStateKey) { listState ->
|
||||
RenderDiscoverFeed(tabs[page].viewModel, tabs[page].routeForLastRead, accountViewModel, listState, nav)
|
||||
RenderDiscoverFeed(
|
||||
viewModel = tabs[page].viewModel,
|
||||
routeForLastRead = tabs[page].routeForLastRead,
|
||||
forceEventKind = tabs[page].forceEventKind,
|
||||
listState = listState,
|
||||
accountViewModel = accountViewModel,
|
||||
nav = nav
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -149,8 +159,9 @@ private fun DiscoverPages(
|
||||
private fun RenderDiscoverFeed(
|
||||
viewModel: FeedViewModel,
|
||||
routeForLastRead: String?,
|
||||
accountViewModel: AccountViewModel,
|
||||
forceEventKind: Int?,
|
||||
listState: LazyListState,
|
||||
accountViewModel: AccountViewModel,
|
||||
nav: (String) -> Unit
|
||||
) {
|
||||
val feedState by viewModel.feedContent.collectAsState()
|
||||
@@ -177,6 +188,7 @@ private fun RenderDiscoverFeed(
|
||||
state,
|
||||
routeForLastRead,
|
||||
listState,
|
||||
forceEventKind,
|
||||
accountViewModel,
|
||||
nav
|
||||
)
|
||||
@@ -212,6 +224,7 @@ private fun DiscoverFeedLoaded(
|
||||
state: FeedState.Loaded,
|
||||
routeForLastRead: String?,
|
||||
listState: LazyListState,
|
||||
forceEventKind: Int?,
|
||||
accountViewModel: AccountViewModel,
|
||||
nav: (String) -> Unit
|
||||
) {
|
||||
@@ -232,6 +245,7 @@ private fun DiscoverFeedLoaded(
|
||||
baseNote = item,
|
||||
routeForLastRead = routeForLastRead,
|
||||
modifier = Modifier,
|
||||
forceEventKind = forceEventKind,
|
||||
accountViewModel = accountViewModel,
|
||||
nav = nav
|
||||
)
|
||||
|
||||
@@ -32,23 +32,30 @@ import androidx.lifecycle.viewmodel.compose.viewModel
|
||||
import com.vitorpamplona.amethyst.LocalPreferences
|
||||
import com.vitorpamplona.amethyst.R
|
||||
import com.vitorpamplona.amethyst.ui.screen.NostrHiddenAccountsFeedViewModel
|
||||
import com.vitorpamplona.amethyst.ui.screen.NostrSpammerAccountsFeedViewModel
|
||||
import com.vitorpamplona.amethyst.ui.screen.RefreshingFeedUserFeedView
|
||||
import com.vitorpamplona.amethyst.ui.screen.UserFeedViewModel
|
||||
import com.vitorpamplona.amethyst.ui.theme.TabRowHeight
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
@Composable
|
||||
fun HiddenUsersScreen(accountViewModel: AccountViewModel, nav: (String) -> Unit) {
|
||||
val feedViewModel: NostrHiddenAccountsFeedViewModel = viewModel(
|
||||
val hiddenFeedViewModel: NostrHiddenAccountsFeedViewModel = viewModel(
|
||||
factory = NostrHiddenAccountsFeedViewModel.Factory(accountViewModel.account)
|
||||
)
|
||||
|
||||
HiddenUsersScreen(feedViewModel, accountViewModel, nav)
|
||||
val spammerFeedViewModel: NostrSpammerAccountsFeedViewModel = viewModel(
|
||||
factory = NostrSpammerAccountsFeedViewModel.Factory(accountViewModel.account)
|
||||
)
|
||||
|
||||
HiddenUsersScreen(hiddenFeedViewModel, spammerFeedViewModel, accountViewModel, nav)
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalFoundationApi::class)
|
||||
@Composable
|
||||
fun HiddenUsersScreen(
|
||||
feedViewModel: NostrHiddenAccountsFeedViewModel,
|
||||
hiddenFeedViewModel: NostrHiddenAccountsFeedViewModel,
|
||||
spammerFeedViewModel: NostrSpammerAccountsFeedViewModel,
|
||||
accountViewModel: AccountViewModel,
|
||||
nav: (String) -> Unit
|
||||
) {
|
||||
@@ -58,10 +65,8 @@ fun HiddenUsersScreen(
|
||||
val observer = LifecycleEventObserver { _, event ->
|
||||
if (event == Lifecycle.Event.ON_RESUME) {
|
||||
println("Hidden Users Start")
|
||||
feedViewModel.invalidateData()
|
||||
}
|
||||
if (event == Lifecycle.Event.ON_PAUSE) {
|
||||
println("Hidden Users Stop")
|
||||
hiddenFeedViewModel.invalidateData()
|
||||
spammerFeedViewModel.invalidateData()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -116,10 +121,18 @@ fun HiddenUsersScreen(
|
||||
Text(text = stringResource(R.string.blocked_users))
|
||||
}
|
||||
)
|
||||
Tab(
|
||||
selected = pagerState.currentPage == 1,
|
||||
onClick = { coroutineScope.launch { pagerState.animateScrollToPage(1) } },
|
||||
text = {
|
||||
Text(text = stringResource(R.string.spamming_users))
|
||||
}
|
||||
)
|
||||
}
|
||||
HorizontalPager(pageCount = 1, state = pagerState) { page ->
|
||||
HorizontalPager(pageCount = 2, state = pagerState) { page ->
|
||||
when (page) {
|
||||
0 -> RefreshingUserFeedView(feedViewModel, accountViewModel, nav)
|
||||
0 -> RefreshingUserFeedView(hiddenFeedViewModel, accountViewModel, nav)
|
||||
1 -> RefreshingUserFeedView(spammerFeedViewModel, accountViewModel, nav)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -128,22 +141,23 @@ fun HiddenUsersScreen(
|
||||
|
||||
@Composable
|
||||
fun RefreshingUserFeedView(
|
||||
feedViewModel: NostrHiddenAccountsFeedViewModel,
|
||||
feedViewModel: UserFeedViewModel,
|
||||
accountViewModel: AccountViewModel,
|
||||
nav: (String) -> Unit
|
||||
) {
|
||||
WatchAccount(feedViewModel, accountViewModel)
|
||||
WatchAccountAndBlockList(feedViewModel, accountViewModel)
|
||||
RefreshingFeedUserFeedView(feedViewModel, accountViewModel, nav)
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun WatchAccount(
|
||||
feedViewModel: NostrHiddenAccountsFeedViewModel,
|
||||
fun WatchAccountAndBlockList(
|
||||
feedViewModel: UserFeedViewModel,
|
||||
accountViewModel: AccountViewModel
|
||||
) {
|
||||
val accountState by accountViewModel.accountLiveData.observeAsState()
|
||||
val blockListState by accountViewModel.account.getBlockListNote().live().metadata.observeAsState()
|
||||
|
||||
LaunchedEffect(accountViewModel, accountState) {
|
||||
LaunchedEffect(accountViewModel, accountState, blockListState) {
|
||||
feedViewModel.invalidateData()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -175,5 +175,6 @@ class TabItem(
|
||||
val resource: Int,
|
||||
val viewModel: FeedViewModel,
|
||||
val routeForLastRead: String,
|
||||
val scrollStateKey: String
|
||||
val scrollStateKey: String,
|
||||
val forceEventKind: Int? = null
|
||||
)
|
||||
|
||||
@@ -31,6 +31,7 @@ import androidx.navigation.compose.currentBackStackEntryAsState
|
||||
import androidx.navigation.compose.rememberNavController
|
||||
import com.vitorpamplona.amethyst.ui.buttons.ChannelFabColumn
|
||||
import com.vitorpamplona.amethyst.ui.buttons.NewCommunityNoteButton
|
||||
import com.vitorpamplona.amethyst.ui.buttons.NewImageButton
|
||||
import com.vitorpamplona.amethyst.ui.buttons.NewNoteButton
|
||||
import com.vitorpamplona.amethyst.ui.navigation.*
|
||||
import com.vitorpamplona.amethyst.ui.navigation.AccountSwitchBottomSheet
|
||||
|
||||
@@ -49,6 +49,8 @@ import androidx.compose.ui.unit.sp
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.lifecycle.Lifecycle
|
||||
import androidx.lifecycle.LifecycleEventObserver
|
||||
import androidx.lifecycle.distinctUntilChanged
|
||||
import androidx.lifecycle.map
|
||||
import androidx.lifecycle.viewmodel.compose.viewModel
|
||||
import coil.compose.AsyncImage
|
||||
import com.vitorpamplona.amethyst.R
|
||||
@@ -57,6 +59,7 @@ import com.vitorpamplona.amethyst.model.LocalCache
|
||||
import com.vitorpamplona.amethyst.model.Note
|
||||
import com.vitorpamplona.amethyst.model.User
|
||||
import com.vitorpamplona.amethyst.service.NostrUserProfileDataSource
|
||||
import com.vitorpamplona.amethyst.service.model.ATag
|
||||
import com.vitorpamplona.amethyst.service.model.AppDefinitionEvent
|
||||
import com.vitorpamplona.amethyst.service.model.BadgeDefinitionEvent
|
||||
import com.vitorpamplona.amethyst.service.model.BadgeProfilesEvent
|
||||
@@ -78,6 +81,7 @@ import com.vitorpamplona.amethyst.ui.components.figureOutMimeType
|
||||
import com.vitorpamplona.amethyst.ui.dal.UserProfileReportsFeedFilter
|
||||
import com.vitorpamplona.amethyst.ui.navigation.ShowQRDialog
|
||||
import com.vitorpamplona.amethyst.ui.note.ClickableUserPicture
|
||||
import com.vitorpamplona.amethyst.ui.note.LoadAddressableNote
|
||||
import com.vitorpamplona.amethyst.ui.screen.FeedState
|
||||
import com.vitorpamplona.amethyst.ui.screen.LnZapFeedView
|
||||
import com.vitorpamplona.amethyst.ui.screen.NostrUserAppRecommendationsFeedViewModel
|
||||
@@ -97,6 +101,7 @@ import com.vitorpamplona.amethyst.ui.theme.BitcoinOrange
|
||||
import com.vitorpamplona.amethyst.ui.theme.ButtonBorder
|
||||
import com.vitorpamplona.amethyst.ui.theme.Size35dp
|
||||
import com.vitorpamplona.amethyst.ui.theme.placeholderText
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
import kotlinx.collections.immutable.toImmutableList
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
@@ -696,45 +701,48 @@ private fun ProfileHeader(
|
||||
private fun ProfileActions(
|
||||
baseUser: User,
|
||||
accountViewModel: AccountViewModel
|
||||
) {
|
||||
val isMe by remember(accountViewModel) {
|
||||
derivedStateOf {
|
||||
accountViewModel.userProfile() == baseUser
|
||||
}
|
||||
}
|
||||
|
||||
if (isMe) {
|
||||
EditButton(accountViewModel.account)
|
||||
}
|
||||
|
||||
WatchIsHiddenUser(baseUser, accountViewModel) { isHidden ->
|
||||
if (isHidden) {
|
||||
val scope = rememberCoroutineScope()
|
||||
ShowUserButton {
|
||||
scope.launch(Dispatchers.IO) {
|
||||
accountViewModel.account.showUser(baseUser.pubkeyHex)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
DisplayFollowUnfollowButton(baseUser, accountViewModel)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun DisplayFollowUnfollowButton(
|
||||
baseUser: User,
|
||||
accountViewModel: AccountViewModel
|
||||
) {
|
||||
val scope = rememberCoroutineScope()
|
||||
val context = LocalContext.current
|
||||
|
||||
val accountLocalUserState by accountViewModel.accountLiveData.observeAsState()
|
||||
val account = remember(accountLocalUserState) { accountLocalUserState?.account } ?: return
|
||||
val isLoggedInFollowingUser by accountViewModel.account.userProfile().live().follows.map {
|
||||
it.user.isFollowing(baseUser)
|
||||
}.distinctUntilChanged().observeAsState(initial = accountViewModel.account.isFollowing(baseUser))
|
||||
|
||||
val accountUserState by accountViewModel.account.userProfile().live().follows.observeAsState()
|
||||
val baseUserState by baseUser.live().follows.observeAsState()
|
||||
val isUserFollowingLoggedIn by baseUser.live().follows.map {
|
||||
it.user.isFollowing(accountViewModel.account.userProfile())
|
||||
}.distinctUntilChanged().observeAsState(initial = baseUser.isFollowing(accountViewModel.account.userProfile()))
|
||||
|
||||
val accountUser = remember(accountUserState) { accountUserState?.user } ?: return
|
||||
|
||||
val isHidden by remember(accountUserState, accountLocalUserState) {
|
||||
derivedStateOf {
|
||||
account.isHidden(baseUser)
|
||||
}
|
||||
}
|
||||
|
||||
val isLoggedInFollowingUser by remember(accountUserState, accountLocalUserState) {
|
||||
derivedStateOf {
|
||||
accountUser.isFollowingCached(baseUser)
|
||||
}
|
||||
}
|
||||
|
||||
val isUserFollowingLoggedIn by remember(baseUserState, accountLocalUserState) {
|
||||
derivedStateOf {
|
||||
baseUser.isFollowing(accountUser)
|
||||
}
|
||||
}
|
||||
|
||||
if (accountUser == baseUser) {
|
||||
EditButton(account)
|
||||
}
|
||||
|
||||
if (isHidden) {
|
||||
ShowUserButton {
|
||||
account.showUser(baseUser.pubkeyHex)
|
||||
}
|
||||
} else if (isLoggedInFollowingUser) {
|
||||
if (isLoggedInFollowingUser) {
|
||||
UnfollowButton {
|
||||
if (!accountViewModel.isWriteable()) {
|
||||
scope.launch {
|
||||
@@ -748,7 +756,7 @@ private fun ProfileActions(
|
||||
}
|
||||
} else {
|
||||
scope.launch(Dispatchers.IO) {
|
||||
account.unfollow(baseUser)
|
||||
accountViewModel.account.unfollow(baseUser)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -767,7 +775,7 @@ private fun ProfileActions(
|
||||
}
|
||||
} else {
|
||||
scope.launch(Dispatchers.IO) {
|
||||
account.follow(baseUser)
|
||||
accountViewModel.account.follow(baseUser)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -785,7 +793,7 @@ private fun ProfileActions(
|
||||
}
|
||||
} else {
|
||||
scope.launch(Dispatchers.IO) {
|
||||
account.follow(baseUser)
|
||||
accountViewModel.account.follow(baseUser)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -793,6 +801,15 @@ private fun ProfileActions(
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun WatchIsHiddenUser(baseUser: User, accountViewModel: AccountViewModel, content: @Composable (Boolean) -> Unit) {
|
||||
val isHidden by accountViewModel.account.liveHiddenUsers.map {
|
||||
it.hiddenUsers.contains(baseUser.pubkeyHex) || it.spammers.contains(baseUser.pubkeyHex)
|
||||
}.observeAsState(accountViewModel.account.isHidden(baseUser))
|
||||
|
||||
content(isHidden)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun DrawAdditionalInfo(
|
||||
baseUser: User,
|
||||
@@ -1110,31 +1127,42 @@ private fun WatchApp(baseApp: Note, nav: (String) -> Unit) {
|
||||
}
|
||||
|
||||
@Composable
|
||||
@OptIn(ExperimentalLayoutApi::class)
|
||||
private fun DisplayBadges(
|
||||
baseUser: User,
|
||||
nav: (String) -> Unit
|
||||
) {
|
||||
val userBadgeState by baseUser.live().badges.observeAsState()
|
||||
val badgeList by remember(userBadgeState) {
|
||||
derivedStateOf {
|
||||
val list = (userBadgeState?.user?.latestAcceptedBadges?.event as? BadgeProfilesEvent)?.badgeAwardEvents()
|
||||
if (list.isNullOrEmpty()) {
|
||||
null
|
||||
} else {
|
||||
list.toImmutableList()
|
||||
LoadAddressableNote(
|
||||
aTag = ATag(
|
||||
BadgeProfilesEvent.kind,
|
||||
baseUser.pubkeyHex,
|
||||
BadgeProfilesEvent.standardDTAg,
|
||||
null
|
||||
)
|
||||
) {
|
||||
if (it != null) {
|
||||
val badgeList by it.live().metadata.map {
|
||||
(it.note.event as? BadgeProfilesEvent)?.badgeAwardEvents()?.toImmutableList()
|
||||
}.distinctUntilChanged().observeAsState()
|
||||
|
||||
badgeList?.let { list ->
|
||||
RenderBadgeList(list, nav)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
badgeList?.let { list ->
|
||||
FlowRow(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
modifier = Modifier.padding(vertical = 5.dp)
|
||||
) {
|
||||
list.forEach { badgeAwardEvent ->
|
||||
LoadAndRenderBadge(badgeAwardEvent, nav)
|
||||
}
|
||||
@Composable
|
||||
@OptIn(ExperimentalLayoutApi::class)
|
||||
private fun RenderBadgeList(
|
||||
list: ImmutableList<String>,
|
||||
nav: (String) -> Unit
|
||||
) {
|
||||
FlowRow(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
modifier = Modifier.padding(vertical = 5.dp)
|
||||
) {
|
||||
list.forEach { badgeAwardEvent ->
|
||||
LoadAndRenderBadge(badgeAwardEvent, nav)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1622,6 +1650,7 @@ fun UserProfileDropDownMenu(user: User, popupExpanded: Boolean, onDismiss: () ->
|
||||
val clipboardManager = LocalClipboardManager.current
|
||||
val accountState by accountViewModel.accountLiveData.observeAsState()
|
||||
val account = accountState?.account!!
|
||||
val blockList by accountViewModel.account.getBlockListNote().live().metadata.observeAsState()
|
||||
|
||||
val scope = rememberCoroutineScope()
|
||||
|
||||
|
||||
@@ -40,7 +40,9 @@ import com.vitorpamplona.amethyst.ui.theme.DoubleVertSpacer
|
||||
import com.vitorpamplona.amethyst.ui.theme.StdPadding
|
||||
import kotlinx.collections.immutable.persistentListOf
|
||||
import kotlinx.collections.immutable.toImmutableList
|
||||
import kotlinx.coroutines.DelicateCoroutinesApi
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.GlobalScope
|
||||
import kotlinx.coroutines.launch
|
||||
import org.xmlpull.v1.XmlPullParser
|
||||
import org.xmlpull.v1.XmlPullParserException
|
||||
@@ -80,12 +82,19 @@ fun Context.getLangPreferenceDropdownEntries(): Map<String, String> {
|
||||
}
|
||||
|
||||
fun getLanguageIndex(languageEntries: Map<String, String>): Int {
|
||||
var languageIndex = languageEntries.values.toTypedArray().indexOf(Locale.current.toLanguageTag())
|
||||
val language = LocalPreferences.getPreferredLanguage()
|
||||
var languageIndex = -1
|
||||
if (language.isNotBlank()) {
|
||||
languageIndex = languageEntries.values.toTypedArray().indexOf(language)
|
||||
} else {
|
||||
languageIndex = languageEntries.values.toTypedArray().indexOf(Locale.current.toLanguageTag())
|
||||
}
|
||||
if (languageIndex == -1) languageIndex = languageEntries.values.toTypedArray().indexOf(Locale.current.language)
|
||||
if (languageIndex == -1) languageIndex = languageEntries.values.toTypedArray().indexOf("en")
|
||||
return languageIndex
|
||||
}
|
||||
|
||||
@OptIn(DelicateCoroutinesApi::class)
|
||||
@Composable
|
||||
fun SettingsScreen(
|
||||
accountViewModel: AccountViewModel,
|
||||
@@ -139,10 +148,13 @@ fun SettingsScreen(
|
||||
placeholder = languageList[languageIndex],
|
||||
options = languageList,
|
||||
onSelect = {
|
||||
scope.launch(Dispatchers.IO) {
|
||||
val locale = languageEntries[languageList[it]]
|
||||
accountViewModel.account.settings.preferredLanguage = locale
|
||||
LocalPreferences.saveToEncryptedStorage(accountViewModel.account)
|
||||
GlobalScope.launch(Dispatchers.Main) {
|
||||
val job = scope.launch(Dispatchers.IO) {
|
||||
val locale = languageEntries[languageList[it]]
|
||||
accountViewModel.account.settings.preferredLanguage = locale
|
||||
LocalPreferences.saveToEncryptedStorage(accountViewModel.account)
|
||||
}
|
||||
job.join()
|
||||
val appLocale: LocaleListCompat = LocaleListCompat.forLanguageTags(languageEntries[languageList[it]])
|
||||
AppCompatDelegate.setApplicationLocales(appLocale)
|
||||
}
|
||||
|
||||
@@ -1,18 +1,13 @@
|
||||
package com.vitorpamplona.amethyst.ui.screen.loggedIn
|
||||
|
||||
import android.Manifest
|
||||
import android.net.Uri
|
||||
import android.os.Build
|
||||
import androidx.compose.animation.Crossfade
|
||||
import androidx.compose.animation.core.tween
|
||||
import androidx.compose.foundation.ExperimentalFoundationApi
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.ExperimentalLayoutApi
|
||||
import androidx.compose.foundation.layout.FlowRow
|
||||
import androidx.compose.foundation.layout.PaddingValues
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.fillMaxHeight
|
||||
@@ -23,14 +18,9 @@ import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.layout.width
|
||||
import androidx.compose.foundation.pager.PagerState
|
||||
import androidx.compose.foundation.pager.VerticalPager
|
||||
import androidx.compose.foundation.shape.CircleShape
|
||||
import androidx.compose.material.ButtonDefaults
|
||||
import androidx.compose.material.CircularProgressIndicator
|
||||
import androidx.compose.material.Icon
|
||||
import androidx.compose.material.IconButton
|
||||
import androidx.compose.material.MaterialTheme
|
||||
import androidx.compose.material.OutlinedButton
|
||||
import androidx.compose.material.Text
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.MoreVert
|
||||
import androidx.compose.runtime.Composable
|
||||
@@ -42,34 +32,21 @@ import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.livedata.observeAsState
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.rememberCoroutineScope
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.platform.LocalLifecycleOwner
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import androidx.lifecycle.Lifecycle
|
||||
import androidx.lifecycle.LifecycleEventObserver
|
||||
import androidx.lifecycle.viewmodel.compose.viewModel
|
||||
import com.google.accompanist.permissions.ExperimentalPermissionsApi
|
||||
import com.google.accompanist.permissions.isGranted
|
||||
import com.google.accompanist.permissions.rememberPermissionState
|
||||
import com.vitorpamplona.amethyst.R
|
||||
import com.vitorpamplona.amethyst.model.Note
|
||||
import com.vitorpamplona.amethyst.service.NostrVideoDataSource
|
||||
import com.vitorpamplona.amethyst.service.model.FileHeaderEvent
|
||||
import com.vitorpamplona.amethyst.service.model.FileStorageHeaderEvent
|
||||
import com.vitorpamplona.amethyst.ui.actions.GallerySelect
|
||||
import com.vitorpamplona.amethyst.ui.actions.NewMediaModel
|
||||
import com.vitorpamplona.amethyst.ui.actions.NewMediaView
|
||||
import com.vitorpamplona.amethyst.ui.actions.NewPostView
|
||||
import com.vitorpamplona.amethyst.ui.components.ObserveDisplayNip05Status
|
||||
import com.vitorpamplona.amethyst.ui.navigation.Route
|
||||
import com.vitorpamplona.amethyst.ui.note.FileHeaderDisplay
|
||||
import com.vitorpamplona.amethyst.ui.note.FileStorageHeaderDisplay
|
||||
import com.vitorpamplona.amethyst.ui.note.LikeReaction
|
||||
@@ -91,10 +68,6 @@ import com.vitorpamplona.amethyst.ui.theme.Size35dp
|
||||
import com.vitorpamplona.amethyst.ui.theme.onBackgroundColorFilter
|
||||
import com.vitorpamplona.amethyst.ui.theme.placeholderText
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
|
||||
@Composable
|
||||
fun VideoScreen(
|
||||
@@ -112,10 +85,6 @@ fun VideoScreen(
|
||||
println("Video Start")
|
||||
NostrVideoDataSource.start()
|
||||
}
|
||||
if (event == Lifecycle.Event.ON_PAUSE) {
|
||||
println("Video Stop")
|
||||
NostrVideoDataSource.stop()
|
||||
}
|
||||
}
|
||||
|
||||
lifeCycleOwner.lifecycle.addObserver(observer)
|
||||
@@ -331,11 +300,14 @@ private fun VideoUserOptionAction(
|
||||
note: Note,
|
||||
accountViewModel: AccountViewModel
|
||||
) {
|
||||
var moreActionsExpanded by remember { mutableStateOf(false) }
|
||||
val popupExpanded = remember { mutableStateOf(false) }
|
||||
val enablePopup = remember {
|
||||
{ popupExpanded.value = true }
|
||||
}
|
||||
|
||||
IconButton(
|
||||
modifier = remember { Modifier.size(22.dp) },
|
||||
onClick = { moreActionsExpanded = true }
|
||||
onClick = enablePopup
|
||||
) {
|
||||
Icon(
|
||||
imageVector = Icons.Default.MoreVert,
|
||||
@@ -346,8 +318,7 @@ private fun VideoUserOptionAction(
|
||||
|
||||
NoteDropDownMenu(
|
||||
note,
|
||||
moreActionsExpanded,
|
||||
{ moreActionsExpanded = false },
|
||||
popupExpanded,
|
||||
accountViewModel
|
||||
)
|
||||
}
|
||||
@@ -401,110 +372,3 @@ fun ReactionsColumn(baseNote: Note, accountViewModel: AccountViewModel, nav: (St
|
||||
ViewCountReaction(baseNote, grayTint = MaterialTheme.colors.onBackground, barChartSize = 39.dp, viewCountColorFilter = MaterialTheme.colors.onBackgroundColorFilter)
|
||||
}
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalPermissionsApi::class)
|
||||
@Composable
|
||||
fun NewImageButton(accountViewModel: AccountViewModel, nav: (String) -> Unit) {
|
||||
var wantsToPost by remember {
|
||||
mutableStateOf(false)
|
||||
}
|
||||
|
||||
var pickedURI by remember {
|
||||
mutableStateOf<Uri?>(null)
|
||||
}
|
||||
|
||||
val scope = rememberCoroutineScope()
|
||||
|
||||
val postViewModel: NewMediaModel = viewModel()
|
||||
postViewModel.onceUploaded {
|
||||
scope.launch(Dispatchers.Default) {
|
||||
// awaits an refresh on the list
|
||||
delay(250)
|
||||
withContext(Dispatchers.Main) {
|
||||
val route = Route.Video.route.replace("{scrollToTop}", "true")
|
||||
nav(route)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (wantsToPost) {
|
||||
val cameraPermissionState =
|
||||
rememberPermissionState(
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|
||||
Manifest.permission.READ_MEDIA_IMAGES
|
||||
} else {
|
||||
Manifest.permission.READ_EXTERNAL_STORAGE
|
||||
}
|
||||
)
|
||||
|
||||
if (cameraPermissionState.status.isGranted) {
|
||||
var showGallerySelect by remember { mutableStateOf(false) }
|
||||
if (showGallerySelect) {
|
||||
GallerySelect(
|
||||
onImageUri = { uri ->
|
||||
wantsToPost = false
|
||||
showGallerySelect = false
|
||||
pickedURI = uri
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
showGallerySelect = true
|
||||
} else {
|
||||
LaunchedEffect(key1 = accountViewModel) {
|
||||
cameraPermissionState.launchPermissionRequest()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pickedURI?.let {
|
||||
NewMediaView(
|
||||
uri = it,
|
||||
onClose = { pickedURI = null },
|
||||
postViewModel = postViewModel,
|
||||
accountViewModel = accountViewModel,
|
||||
nav = nav
|
||||
)
|
||||
}
|
||||
|
||||
if (postViewModel.isUploadingImage) {
|
||||
ShowProgress(postViewModel)
|
||||
} else {
|
||||
OutlinedButton(
|
||||
onClick = { wantsToPost = true },
|
||||
modifier = Modifier.size(55.dp),
|
||||
shape = CircleShape,
|
||||
colors = ButtonDefaults.outlinedButtonColors(backgroundColor = MaterialTheme.colors.primary),
|
||||
contentPadding = PaddingValues(0.dp)
|
||||
) {
|
||||
Icon(
|
||||
painter = painterResource(R.drawable.ic_compose),
|
||||
null,
|
||||
modifier = Modifier.size(26.dp),
|
||||
tint = Color.White
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun ShowProgress(postViewModel: NewMediaModel) {
|
||||
Box(Modifier.size(55.dp), contentAlignment = Alignment.Center) {
|
||||
CircularProgressIndicator(
|
||||
progress = postViewModel.uploadingPercentage.value,
|
||||
modifier = Modifier
|
||||
.size(55.dp)
|
||||
.clip(CircleShape)
|
||||
.background(MaterialTheme.colors.background),
|
||||
strokeWidth = 5.dp
|
||||
)
|
||||
postViewModel.uploadingDescription.value?.let {
|
||||
Text(
|
||||
it,
|
||||
color = MaterialTheme.colors.onSurface,
|
||||
fontSize = 10.sp,
|
||||
textAlign = TextAlign.Center
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -158,7 +158,7 @@
|
||||
<string name="translations_to">al</string>
|
||||
<string name="translations_show_in_lang_first">Montri en %1$s unua</string>
|
||||
<string name="translations_always_translate_to_lang">Ĉiam traduki al</string>
|
||||
<string name="nip_05" translatable="false">NIP-05</string>
|
||||
<string name="nip_05">Nostr Address</string>
|
||||
<string name="lnurl" translatable="false">LNURL...</string>
|
||||
<string name="never">neniam</string>
|
||||
<string name="now">nun</string>
|
||||
|
||||
@@ -164,7 +164,7 @@
|
||||
<string name="translations_show_in_lang_first">ابتدا به نشان بده %1$s</string>
|
||||
<string name="translations_always_translate_to_lang">همیشه به ترجمه کن %1$s</string>
|
||||
<string name="translations_never_translate_from_lang">هرگز به ترجمه نکن %1$s</string>
|
||||
<string name="nip_05" translatable="false">NIP-05</string>
|
||||
<string name="nip_05">Nostr Address</string>
|
||||
<string name="lnurl" translatable="false">LNURL...</string>
|
||||
<string name="never">هرگز</string>
|
||||
<string name="now">اکنون</string>
|
||||
|
||||
@@ -164,7 +164,7 @@
|
||||
<string name="translations_show_in_lang_first">最初に%1$sで表示</string>
|
||||
<string name="translations_always_translate_to_lang">常に%1$sに翻訳</string>
|
||||
<string name="translations_never_translate_from_lang">%1$sを翻訳しない</string>
|
||||
<string name="never">never</string>
|
||||
<string name="never">なし</string>
|
||||
<string name="now">今</string>
|
||||
<string name="h">時間</string>
|
||||
<string name="m">分</string>
|
||||
@@ -459,4 +459,17 @@
|
||||
|
||||
<string name="add_sensitive_content_label">センシティブなコンテンツ</string>
|
||||
<string name="add_sensitive_content_description">コンテンツを表示する前に、センシティブなコンテンツであることを示す警告を表示します</string>
|
||||
<string name="settings">設定</string>
|
||||
<string name="always">常に</string>
|
||||
<string name="wifi_only">Wifiのみ</string>
|
||||
<string name="system">システム</string>
|
||||
<string name="light">ライト</string>
|
||||
<string name="dark">ダーク</string>
|
||||
<string name="application_preferences">アプリ設定</string>
|
||||
<string name="language">言語</string>
|
||||
<string name="theme">テーマ</string>
|
||||
<string name="automatically_load_images_gifs">自動的に画像/GIFを読み込み</string>
|
||||
<string name="automatically_play_videos">自動的に動画を再生</string>
|
||||
<string name="automatically_show_url_preview">自動的にURLプレビューを表示</string>
|
||||
<string name="load_image">画像読み込み</string>
|
||||
</resources>
|
||||
|
||||
@@ -161,7 +161,7 @@
|
||||
<string name="translations_show_in_lang_first">Eerst laten zien in %1$s</string>
|
||||
<string name="translations_always_translate_to_lang">Altijd vertalen naar %1$s</string>
|
||||
<string name="translations_never_translate_from_lang">Nooit vertalen vanuit %1$s</string>
|
||||
<string name="nip_05" translatable="false">NIP-05</string>
|
||||
<string name="nip_05">Nostr Address</string>
|
||||
<string name="lnurl" translatable="false">LNURL...</string>
|
||||
<string name="never">nooit</string>
|
||||
<string name="now">nu</string>
|
||||
|
||||
@@ -23,6 +23,7 @@
|
||||
<string name="copy_note_id">Copiar ID da Nota</string>
|
||||
<string name="broadcast">Transmitir</string>
|
||||
<string name="request_deletion">Pedir para excluir</string>
|
||||
<string name="block_report">Bloquear / Denunciar</string>
|
||||
<string name="block_hide_user">Bloquear e ocultar usuário</string>
|
||||
<string name="report_spam_scam">Denunciar spam/fraude</string>
|
||||
<string name="report_impersonation">Denunciar representação</string>
|
||||
@@ -33,6 +34,8 @@
|
||||
<string name="login_with_a_private_key_to_like_posts">Faça login com uma chave privada para curtir postagens</string>
|
||||
<string name="no_zap_amount_setup_long_press_to_change">Nenhuma configuração de quantidade de Zap. Pressione e segure para alterar</string>
|
||||
<string name="login_with_a_private_key_to_be_able_to_send_zaps">Faça login com uma chave privada para poder enviar Zaps</string>
|
||||
<string name="login_with_a_private_key_to_be_able_to_follow">Faça login com uma chave privada para poder seguir</string>
|
||||
<string name="login_with_a_private_key_to_be_able_to_unfollow">Faça login com uma chave privada para poder remover seguidores</string>
|
||||
<string name="zaps">Zaps</string>
|
||||
<string name="view_count">Contagem de visualizações</string>
|
||||
<string name="boost">Impulsionar</string>
|
||||
@@ -121,6 +124,7 @@
|
||||
<string name="send_a_direct_message">Enviar uma mensagem direta</string>
|
||||
<string name="edits_the_user_s_metadata">Muda os dados do usuário</string>
|
||||
<string name="follow">Seguir</string>
|
||||
<string name="follow_back">Seguir de volta</string>
|
||||
<string name="unblock">Desbloquear</string>
|
||||
<string name="copy_user_id">Copiar ID do usuário</string>
|
||||
<string name="unblock_user">Desbloquear Usuário</string>
|
||||
@@ -247,5 +251,180 @@
|
||||
<string name="wallet_connect_service_explainer">Use sua chave privada para pagar zaps sem sair do app. Qualquer pessoa com acesso à sua chave privada Nostr poderá gastar o saldo da sua carteira. Guarde apenas os fundos que você pode perder e use um retransmissor privado, se possível. O operador de Relay pode ver seus metadados de pagamentos.</string>
|
||||
<string name="wallet_connect_service_pubkey">Wallet Connect Pubkey</string>
|
||||
<string name="wallet_connect_service_relay">Wallet Connect Relay</string>
|
||||
<string name="wallet_connect_service_secret">Wallet Connect Secret</string>
|
||||
<string name="wallet_connect_service_show_secret">Mostrar chave secreta</string>
|
||||
<string name="wallet_connect_service_secret_placeholder">chave privada nsec / hex</string>
|
||||
<string name="pledge_amount_in_sats">Valor em Sats</string>
|
||||
<string name="post_poll">Postar enquete</string>
|
||||
<string name="poll_heading_required">Campos obrigatórios:</string>
|
||||
<string name="poll_zap_recipients">Destinatários Zap</string>
|
||||
<string name="poll_primary_description">Descrição da enquete principal...</string>
|
||||
<string name="poll_option_index">Opção %s</string>
|
||||
<string name="poll_option_description">Descrição da opção de enquete</string>
|
||||
<string name="poll_heading_optional">Campos opcionais:</string>
|
||||
<string name="poll_zap_value_min">Zap mínimo</string>
|
||||
<string name="poll_zap_value_max">Zap máximo</string>
|
||||
<string name="poll_consensus_threshold">Consenso</string>
|
||||
<string name="poll_consensus_threshold_percent">(0–100)%</string>
|
||||
<string name="poll_closing_time">Fechar depois</string>
|
||||
<string name="poll_closing_time_days">dias</string>
|
||||
<string name="poll_is_closed">Enquete está fechada para novos votos</string>
|
||||
<string name="poll_zap_amount">Valor do Zap</string>
|
||||
<string name="one_vote_per_user_on_atomic_votes">Apenas um voto por usuário é permitido neste tipo de enquete</string>
|
||||
<string name="looking_for_event">Procurando o evento %1$s</string>
|
||||
<string name="custom_zaps_add_a_message">Adicionar uma mensagem pública</string>
|
||||
<string name="custom_zaps_add_a_message_private">Adicionar uma mensagem privada</string>
|
||||
<string name="custom_zaps_add_a_message_nonzap">Adicionar uma mensagem</string>
|
||||
<string name="custom_zaps_add_a_message_example">Obrigado por todo o seu trabalho!</string>
|
||||
<string name="lightning_create_and_add_invoice">Criar e adicionar</string>
|
||||
<string name="poll_author_no_vote">Os autores de enquetes não podem votar em suas próprias enquetes.</string>
|
||||
<string name="hash_verification_passed">A imagem é a mesma desde o post</string>
|
||||
<string name="hash_verification_failed">A imagem mudou. O autor pode não ter visto a mudança</string>
|
||||
<string name="content_description_add_image">Adicionar Imagem</string>
|
||||
<string name="content_description_add_video">Adicionar Video</string>
|
||||
<string name="content_description_add_document">Adicionar Documento</string>
|
||||
<string name="add_content">Adicionar à mensagem</string>
|
||||
<string name="content_description">Descrição do conteúdo</string>
|
||||
<string name="content_description_example">Um barco azul em uma praia de areia branca ao pôr do sol</string>
|
||||
<string name="zap_type">Tipo de Zap</string>
|
||||
<string name="zap_type_explainer">Tipo de Zap para todas as opções</string>
|
||||
<string name="zap_type_public">Público</string>
|
||||
<string name="zap_type_public_explainer">Todos podem ver a transação e a mensagem</string>
|
||||
<string name="zap_type_private">Privado</string>
|
||||
<string name="zap_type_private_explainer">O remetente e o destinatário podem se ver e ler a mensagem</string>
|
||||
<string name="zap_type_anonymous">Anônimo</string>
|
||||
<string name="zap_type_anonymous_explainer">Destinatário e o público não sabem quem enviou o pagamento</string>
|
||||
<string name="zap_type_nonzap">Sem Zap</string>
|
||||
<string name="zap_type_nonzap_explainer">Nenhum traço no Nostr, apenas na Lightning</string>
|
||||
<string name="file_server">Servidor de arquivos</string>
|
||||
<string name="zap_forward_lnAddress">LnAddress ou @Usuario</string>
|
||||
<string name="upload_server_imgur">imgur.com - confiável</string>
|
||||
<string name="upload_server_imgur_explainer">Imgur pode modificar o arquivo</string>
|
||||
<string name="upload_server_nostrimg">nostrimg.com - confiável</string>
|
||||
<string name="upload_server_nostrimg_explainer">NostrImg pode modificar o arquivo</string>
|
||||
<string name="upload_server_nostrbuild">nostr.build - confiável</string>
|
||||
<string name="upload_server_nostrbuild_explainer">Nostr.build pode modificar o arquivo</string>
|
||||
<string name="upload_server_nostrfilesdev">nostrfiles.dev - confiável</string>
|
||||
<string name="upload_server_nostrfilesdev_explainer">Nostrfiles.dev pode modificar o arquivo</string>
|
||||
<string name="upload_server_nostrcheckme">nostrcheck.me - confiável</string>
|
||||
<string name="upload_server_nostrcheckme_explainer">nostrcheck.me pode modificar o arquivo</string>
|
||||
<string name="upload_server_imgur_nip94">Imgur verificável (NIP-94)</string>
|
||||
<string name="upload_server_imgur_nip94_explainer">Verifica se o Imgur modificou o arquivo. Novo NIP: outros clientes podem não ver</string>
|
||||
<string name="upload_server_nostrimg_nip94">NostrImg verificável (NIP-94)</string>
|
||||
<string name="upload_server_nostrimg_nip94_explainer">Verifica se NostrImg modificou o arquivo. Novo NIP: outros clientes podem não ver</string>
|
||||
<string name="upload_server_nostrbuild_nip94">Nostr.build verificável (NIP-94)</string>
|
||||
<string name="upload_server_nostrbuild_nip94_explainer">Verifica se Nostr.build modificou o arquivo. Novo NIP: outros clientes podem não ver</string>
|
||||
<string name="upload_server_nostrfilesdev_nip94">Nostrfiles.dev verificável (NIP-94)</string>
|
||||
<string name="upload_server_nostrfilesdev_nip94_explainer">Verifica se Nostrfiles.dev modificou o arquivo. Novo NIP: outros clientes podem não ver</string>
|
||||
<string name="upload_server_nostrcheckme_nip94">Verificável Nostrcheck.me (NIP-94)</string>
|
||||
<string name="upload_server_nostrcheckme_nip94_explainer">Verifica se Nostrcheck.me modificou o arquivo. Novo NIP: outros clientes podem não ver</string>
|
||||
<string name="upload_server_relays_nip95">Seus relays (NIP-95)</string>
|
||||
<string name="upload_server_relays_nip95_explainer">Os arquivos são hospedados por seus relays. Novo NIP: verifique se eles suportam</string>
|
||||
<string name="connect_via_tor_short">Configuração do Tor/Orbot</string>
|
||||
<string name="connect_via_tor">Conecte-se através da configuração do Orbot</string>
|
||||
<string name="do_you_really_want_to_disable_tor_title">Desconectar do Orbot/Tor?</string>
|
||||
<string name="do_you_really_want_to_disable_tor_text">Seus dados serão transferidos imediatamente na rede regular</string>
|
||||
<string name="yes">Sim</string>
|
||||
<string name="no">Não</string>
|
||||
<string name="follow_list_selection">Lista de seguidores</string>
|
||||
<string name="follow_list_kind3follows">Seguindo</string>
|
||||
<string name="follow_list_global">Global</string>
|
||||
<string name="connect_through_your_orbot_setup_markdown">
|
||||
## Conecte-se através do Tor com o Orbot
|
||||
\n\n1. Instale o [Orbot](https://play.google.com/store/apps/details?id=org.torproject.android)
|
||||
\n2. Iniciar Orbot
|
||||
\n3. No Orbot, verifique a porta Socks. O padrão usa 9050
|
||||
\n4. Se necessário altere a porta no Orbot
|
||||
\n5. Configure a porta Socks nesta tela
|
||||
\n6. Pressione o botão Ativar para usar o Orbot como proxy
|
||||
</string>
|
||||
<string name="orbot_socks_port">Porta Socks Orbot</string>
|
||||
<string name="invalid_port_number">Número de porta inválido</string>
|
||||
<string name="use_orbot">Usar Orbot</string>
|
||||
<string name="disconnect_from_your_orbot_setup">Desconectar Tor/Orbot</string>
|
||||
<string name="app_notification_dms_channel_name">Mensagens privadas</string>
|
||||
<string name="app_notification_dms_channel_description">Notifica você quando chega uma mensagem privada</string>
|
||||
<string name="app_notification_zaps_channel_name">Zaps Recebidos</string>
|
||||
<string name="app_notification_zaps_channel_description">Notifica quando alguém te manda um zap</string>
|
||||
<string name="app_notification_zaps_channel_message">%1$s sats</string>
|
||||
<string name="app_notification_zaps_channel_message_from">De %1$s</string>
|
||||
<string name="app_notification_zaps_channel_message_for">por %1$s</string>
|
||||
<string name="reply_notify">"Notificar: "</string>
|
||||
<string name="channel_list_join_conversation">Entrar na conversa</string>
|
||||
<string name="channel_list_user_or_group_id">ID do usuário ou grupo</string>
|
||||
<string name="channel_list_user_or_group_id_demo">npub, nevent ou hex</string>
|
||||
<string name="channel_list_create_channel">Criar</string>
|
||||
<string name="channel_list_join_channel">Entrar</string>
|
||||
<string name="today">Hoje</string>
|
||||
<string name="content_warning">Aviso de conteúdo</string>
|
||||
<string name="content_warning_explanation">Esta nota contém conteúdo sensível que algumas pessoas podem achar ofensivo ou perturbador</string>
|
||||
<string name="content_warning_hide_all_sensitive_content">Sempre ocultar conteúdo sensível</string>
|
||||
<string name="content_warning_show_all_sensitive_content">Sempre mostrar conteúdo sensível</string>
|
||||
<string name="content_warning_see_warnings">Sempre mostrar avisos de conteúdo</string>
|
||||
<string name="recommended_apps">"Recomenda: "</string>
|
||||
<string name="filter_spam_from_strangers">Filtrar spam de estranhos</string>
|
||||
<string name="warn_when_posts_have_reports_from_your_follows">Avise quando as postagens tiverem denuncias de seus seguidores</string>
|
||||
<string name="new_reaction_symbol">Novo símbolo de reação</string>
|
||||
<string name="no_reaction_type_setup_long_press_to_change">Nenhum tipo de reação selecionado. Pressione e segure para alterar</string>
|
||||
<string name="zapraiser">Arrecadação de Zaps</string>
|
||||
<string name="zapraiser_explainer">Adiciona uma quantidade alvo de sats para financiar esta postagem. Os clientes podem mostrar isso como uma barra de progresso para incentivar doações</string>
|
||||
<string name="zapraiser_target_amount_in_sats">Valor alvo em Sats</string>
|
||||
<string name="sats_to_complete">Arrecadação de Zaps em %1$s. %2$s sats para meta</string>
|
||||
<string name="read_from_relay">Ler do Relay</string>
|
||||
<string name="write_to_relay">Enviar para o Relay</string>
|
||||
<string name="an_error_ocurred_trying_to_get_relay_information">Ocorreu um erro ao tentar obter informações do relay de %1$s</string>
|
||||
<string name="owner">Proprietário</string>
|
||||
<string name="version">Versão</string>
|
||||
<string name="software">Programa</string>
|
||||
<string name="contact">Contato</string>
|
||||
<string name="supports">NIPs Suportados</string>
|
||||
<string name="admission_fees">Taxas de admissão</string>
|
||||
<string name="payments_url">URL de pagamentos</string>
|
||||
<string name="limitations">Limitações</string>
|
||||
<string name="countries">Países</string>
|
||||
<string name="languages">Línguas</string>
|
||||
<string name="tags">Tags</string>
|
||||
<string name="posting_policy">Política de postagem</string>
|
||||
<string name="message_length">Tamanho da mensagem</string>
|
||||
<string name="subscriptions">Assinaturas</string>
|
||||
<string name="filters">Filtros</string>
|
||||
<string name="subscription_id_length">Comprimento do ID da assinatura</string>
|
||||
<string name="minimum_prefix">Prefixo mínimo</string>
|
||||
<string name="maximum_event_tags">Máximo de tags de evento</string>
|
||||
<string name="content_length">Tamanho do conteúdo</string>
|
||||
<string name="minimum_pow">PoW mínimo</string>
|
||||
<string name="auth">Autenticação</string>
|
||||
<string name="payment">Pagamento</string>
|
||||
<string name="cashu">Token Cashu</string>
|
||||
<string name="cashu_redeem">Resgatar</string>
|
||||
<string name="live_stream_live_tag">AO VIVO</string>
|
||||
<string name="live_stream_offline_tag">DESLIGADA</string>
|
||||
<string name="live_stream_ended_tag">FINALIZADA</string>
|
||||
<string name="live_stream_planned_tag">AGENDADA</string>
|
||||
<string name="live_stream_is_offline">A transmissão ao vivo está offline</string>
|
||||
<string name="live_stream_has_ended">Transmissão ao vivo encerrada</string>
|
||||
<string name="are_you_sure_you_want_to_log_out">Sair exclui todas as suas informações locais. Certifique-se de fazer backup de suas chaves privadas para evitar a perda de sua conta. Você quer continuar?</string>
|
||||
<string name="followed_tags">Tags Seguidas</string>
|
||||
<string name="relay_setup">Relays</string>
|
||||
<string name="discover_live">Ao vivo</string>
|
||||
<string name="discover_community">Comunidade</string>
|
||||
<string name="discover_chat">Chats</string>
|
||||
<string name="community_approved_posts">Postagens Aprovadas</string>
|
||||
<string name="groups_no_descriptor">Este grupo não tem uma descrição ou regras. Fale com o proprietário para adicionar</string>
|
||||
<string name="community_no_descriptor">Esta comunidade não tem uma descrição. Fale com o proprietário para adicionar</string>
|
||||
<string name="add_sensitive_content_label">Conteúdo sensível</string>
|
||||
<string name="add_sensitive_content_description">Adiciona aviso de conteúdo sensível antes de mostrar este conteúdo</string>
|
||||
<string name="settings">Configurações</string>
|
||||
<string name="always">Sempre</string>
|
||||
<string name="wifi_only">Somente wifi</string>
|
||||
<string name="system">Sistema</string>
|
||||
<string name="light">Claro</string>
|
||||
<string name="dark">Escuro</string>
|
||||
<string name="application_preferences">Preferências do aplicativo</string>
|
||||
<string name="language">Linguagem</string>
|
||||
<string name="theme">Tema</string>
|
||||
<string name="automatically_load_images_gifs">Carregar imagens/gifs automaticamente</string>
|
||||
<string name="automatically_play_videos">Reproduzir vídeos automaticamente</string>
|
||||
<string name="automatically_show_url_preview">Mostrar automaticamente a visualização da URL</string>
|
||||
<string name="load_image">Carregar imagem</string>
|
||||
</resources>
|
||||
|
||||
@@ -167,7 +167,7 @@
|
||||
<string name="translations_show_in_lang_first">Show in %1$s first</string>
|
||||
<string name="translations_always_translate_to_lang">Always translate to %1$s</string>
|
||||
<string name="translations_never_translate_from_lang">Never translate from %1$s</string>
|
||||
<string name="nip_05" translatable="false">NIP-05</string>
|
||||
<string name="nip_05">Nostr Address</string>
|
||||
<string name="lnurl" translatable="false">LNURL...</string>
|
||||
<string name="never">never</string>
|
||||
<string name="now">now</string>
|
||||
@@ -360,9 +360,6 @@
|
||||
<string name="upload_server_nostrcheckme_nip94">Verifiable Nostrcheck.me (NIP-94)</string>
|
||||
<string name="upload_server_nostrcheckme_nip94_explainer">Checks if Nostrcheck.me modified the file. New NIP: other clients might not see it</string>
|
||||
|
||||
|
||||
|
||||
|
||||
<string name="upload_server_relays_nip95">Your relays (NIP-95)</string>
|
||||
<string name="upload_server_relays_nip95_explainer">Files are hosted by your relays. New NIP: check if they support</string>
|
||||
|
||||
@@ -497,4 +494,6 @@
|
||||
<string name="automatically_play_videos">Automatically play videos</string>
|
||||
<string name="automatically_show_url_preview">Automatically show url preview</string>
|
||||
<string name="load_image">Load Image</string>
|
||||
|
||||
<string name="spamming_users">Spammers</string>
|
||||
</resources>
|
||||
|
||||
@@ -8,7 +8,7 @@ buildscript {
|
||||
accompanist_version = '0.30.0'
|
||||
coil_version = '2.4.0'
|
||||
vico_version = '1.7.1'
|
||||
exoplayer_version = '2.19.0'
|
||||
exoplayer_version = '1.1.0'
|
||||
}
|
||||
dependencies {
|
||||
classpath 'com.google.gms:google-services:4.3.15'
|
||||
|
||||
Reference in New Issue
Block a user