mirror of
https://github.com/vitorpamplona/amethyst.git
synced 2026-07-22 15:52:22 +00:00
Compare commits
39 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
65ab756c2b | ||
|
|
e298285145 | ||
|
|
ad59b691fd | ||
|
|
a256e34878 | ||
|
|
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 255
|
||||
versionName "0.69.2"
|
||||
|
||||
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
|
||||
|
||||
@@ -12,7 +12,6 @@ import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.heightIn
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.LazyListState
|
||||
import androidx.compose.foundation.lazy.itemsIndexed
|
||||
@@ -45,7 +44,6 @@ import androidx.compose.ui.focus.onFocusChanged
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.platform.LocalLifecycleOwner
|
||||
import androidx.compose.ui.platform.LocalSoftwareKeyboardController
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
@@ -62,9 +60,11 @@ import com.vitorpamplona.amethyst.service.NostrSearchEventOrUserDataSource
|
||||
import com.vitorpamplona.amethyst.service.checkNotInMainThread
|
||||
import com.vitorpamplona.amethyst.ui.note.ChannelName
|
||||
import com.vitorpamplona.amethyst.ui.note.ClickableUserPicture
|
||||
import com.vitorpamplona.amethyst.ui.note.SearchIcon
|
||||
import com.vitorpamplona.amethyst.ui.note.UsernameDisplay
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.SearchBarViewModel
|
||||
import com.vitorpamplona.amethyst.ui.theme.Size20Modifier
|
||||
import com.vitorpamplona.amethyst.ui.theme.Size55dp
|
||||
import com.vitorpamplona.amethyst.ui.theme.placeholderText
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
@@ -254,12 +254,7 @@ private fun SearchEditTextForJoin(
|
||||
}
|
||||
},
|
||||
leadingIcon = {
|
||||
Icon(
|
||||
painter = painterResource(R.drawable.ic_search),
|
||||
contentDescription = null,
|
||||
modifier = Modifier.size(20.dp),
|
||||
tint = Color.Unspecified
|
||||
)
|
||||
SearchIcon(modifier = Size20Modifier, Color.Unspecified)
|
||||
},
|
||||
modifier = Modifier
|
||||
.weight(1f, true)
|
||||
|
||||
@@ -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))
|
||||
)
|
||||
|
||||
|
||||
@@ -21,7 +21,6 @@ import androidx.compose.material.*
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.ArrowForwardIos
|
||||
import androidx.compose.material.icons.filled.Bolt
|
||||
import androidx.compose.material.icons.filled.Cancel
|
||||
import androidx.compose.material.icons.filled.CurrencyBitcoin
|
||||
import androidx.compose.material.icons.filled.ShowChart
|
||||
import androidx.compose.material.icons.filled.Visibility
|
||||
@@ -73,7 +72,11 @@ import com.vitorpamplona.amethyst.model.User
|
||||
import com.vitorpamplona.amethyst.service.NostrSearchEventOrUserDataSource
|
||||
import com.vitorpamplona.amethyst.service.noProtocolUrlValidator
|
||||
import com.vitorpamplona.amethyst.ui.components.*
|
||||
import com.vitorpamplona.amethyst.ui.note.CancelIcon
|
||||
import com.vitorpamplona.amethyst.ui.note.CloseIcon
|
||||
import com.vitorpamplona.amethyst.ui.note.NoteCompose
|
||||
import com.vitorpamplona.amethyst.ui.note.PollIcon
|
||||
import com.vitorpamplona.amethyst.ui.note.RegularPostIcon
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.TextSpinner
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.UserLine
|
||||
@@ -479,19 +482,9 @@ private fun AddPollButton(
|
||||
}
|
||||
) {
|
||||
if (!isPollActive) {
|
||||
Icon(
|
||||
painter = painterResource(R.drawable.ic_poll),
|
||||
null,
|
||||
modifier = Modifier.size(20.dp),
|
||||
tint = MaterialTheme.colors.onBackground
|
||||
)
|
||||
PollIcon()
|
||||
} else {
|
||||
Icon(
|
||||
painter = painterResource(R.drawable.ic_lists),
|
||||
null,
|
||||
modifier = Modifier.size(20.dp),
|
||||
tint = MaterialTheme.colors.onBackground
|
||||
)
|
||||
RegularPostIcon()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -725,12 +718,7 @@ fun CloseButton(onCancel: () -> Unit) {
|
||||
backgroundColor = Color.Gray
|
||||
)
|
||||
) {
|
||||
Icon(
|
||||
painter = painterResource(id = R.drawable.ic_close),
|
||||
contentDescription = stringResource(id = R.string.cancel),
|
||||
modifier = Modifier.size(20.dp),
|
||||
tint = Color.White
|
||||
)
|
||||
CloseIcon()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -792,30 +780,6 @@ fun CreateButton(onPost: () -> Unit = {}, isActive: Boolean, modifier: Modifier
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun SearchButton(onPost: () -> Unit = {}, isActive: Boolean, modifier: Modifier = Modifier) {
|
||||
Button(
|
||||
modifier = modifier,
|
||||
onClick = {
|
||||
if (isActive) {
|
||||
onPost()
|
||||
}
|
||||
},
|
||||
shape = ButtonBorder,
|
||||
colors = ButtonDefaults
|
||||
.buttonColors(
|
||||
backgroundColor = if (isActive) MaterialTheme.colors.primary else Color.Gray
|
||||
)
|
||||
) {
|
||||
Icon(
|
||||
painter = painterResource(R.drawable.ic_search),
|
||||
null,
|
||||
modifier = Modifier.size(26.dp),
|
||||
tint = Color.White
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
enum class ServersAvailable {
|
||||
// IMGUR,
|
||||
NOSTR_BUILD,
|
||||
@@ -910,17 +874,10 @@ fun ImageVideoDescription(
|
||||
)
|
||||
|
||||
IconButton(
|
||||
modifier = Modifier.size(30.dp),
|
||||
modifier = Modifier.size(30.dp).padding(end = 5.dp),
|
||||
onClick = onCancel
|
||||
) {
|
||||
Icon(
|
||||
imageVector = Icons.Default.Cancel,
|
||||
null,
|
||||
modifier = Modifier
|
||||
.padding(end = 5.dp)
|
||||
.size(30.dp),
|
||||
tint = MaterialTheme.colors.placeholderText
|
||||
)
|
||||
CancelIcon()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
)
|
||||
) {
|
||||
|
||||
@@ -11,12 +11,8 @@ import androidx.compose.foundation.text.selection.SelectionContainer
|
||||
import androidx.compose.foundation.verticalScroll
|
||||
import androidx.compose.material.Card
|
||||
import androidx.compose.material.Divider
|
||||
import androidx.compose.material.Icon
|
||||
import androidx.compose.material.IconButton
|
||||
import androidx.compose.material.MaterialTheme
|
||||
import androidx.compose.material.Text
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.ArrowBack
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
@@ -25,6 +21,8 @@ import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.window.Dialog
|
||||
import com.vitorpamplona.amethyst.R
|
||||
import com.vitorpamplona.amethyst.ui.note.ArrowBackIcon
|
||||
import com.vitorpamplona.amethyst.ui.theme.Size24dp
|
||||
|
||||
@Composable
|
||||
fun SelectTextDialog(text: String, onDismiss: () -> Unit) {
|
||||
@@ -41,7 +39,7 @@ fun SelectTextDialog(text: String, onDismiss: () -> Unit) {
|
||||
) {
|
||||
Card {
|
||||
Column(
|
||||
modifier = Modifier.heightIn(24.dp, maxHeight)
|
||||
modifier = Modifier.heightIn(Size24dp, maxHeight)
|
||||
) {
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
@@ -50,11 +48,7 @@ fun SelectTextDialog(text: String, onDismiss: () -> Unit) {
|
||||
IconButton(
|
||||
onClick = onDismiss
|
||||
) {
|
||||
Icon(
|
||||
imageVector = Icons.Default.ArrowBack,
|
||||
contentDescription = null,
|
||||
tint = MaterialTheme.colors.primary
|
||||
)
|
||||
ArrowBackIcon(Size24dp)
|
||||
}
|
||||
Text(text = stringResource(R.string.select_text_dialog_top))
|
||||
}
|
||||
|
||||
@@ -11,17 +11,9 @@ import androidx.compose.foundation.layout.BoxWithConstraints
|
||||
import androidx.compose.foundation.layout.defaultMinSize
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.layout.width
|
||||
import androidx.compose.foundation.shape.CircleShape
|
||||
import androidx.compose.material.Icon
|
||||
import androidx.compose.material.IconButton
|
||||
import androidx.compose.material.MaterialTheme
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.VolumeOff
|
||||
import androidx.compose.material.icons.filled.VolumeUp
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.DisposableEffect
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
@@ -47,22 +39,26 @@ 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
|
||||
import com.vitorpamplona.amethyst.ui.note.MuteIcon
|
||||
import com.vitorpamplona.amethyst.ui.note.MutedIcon
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
||||
import com.vitorpamplona.amethyst.ui.theme.Size50Modifier
|
||||
import com.vitorpamplona.amethyst.ui.theme.VolumeBottomIconSize
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlin.time.ExperimentalTime
|
||||
@@ -175,6 +171,7 @@ fun VideoView(
|
||||
}
|
||||
|
||||
@Composable
|
||||
@androidx.annotation.OptIn(androidx.media3.common.util.UnstableApi::class)
|
||||
fun VideoView1(
|
||||
videoUri: String,
|
||||
description: String? = null,
|
||||
@@ -263,6 +260,7 @@ data class VideoThumb(
|
||||
)
|
||||
|
||||
@Composable
|
||||
@androidx.annotation.OptIn(androidx.media3.common.util.UnstableApi::class)
|
||||
private fun RenderVideoPlayer(
|
||||
playerData: VideoPlayer,
|
||||
thumbData: VideoThumb?,
|
||||
@@ -290,7 +288,7 @@ private fun RenderVideoPlayer(
|
||||
}
|
||||
},
|
||||
factory = {
|
||||
StyledPlayerView(context).apply {
|
||||
PlayerView(context).apply {
|
||||
player = playerData.exoPlayer
|
||||
layoutParams = FrameLayout.LayoutParams(
|
||||
ViewGroup.LayoutParams.MATCH_PARENT,
|
||||
@@ -350,14 +348,7 @@ fun LayoutCoordinates.isCompletelyVisible(view: View): Boolean {
|
||||
|
||||
@Composable
|
||||
private fun MuteButton(toggle: (Boolean) -> Unit) {
|
||||
Box(
|
||||
remember {
|
||||
Modifier
|
||||
.width(70.dp)
|
||||
.height(70.dp)
|
||||
.padding(10.dp)
|
||||
}
|
||||
) {
|
||||
Box(modifier = VolumeBottomIconSize) {
|
||||
Box(
|
||||
Modifier
|
||||
.clip(CircleShape)
|
||||
@@ -373,22 +364,12 @@ private fun MuteButton(toggle: (Boolean) -> Unit) {
|
||||
mutedInstance.value = !mutedInstance.value
|
||||
toggle(mutedInstance.value)
|
||||
},
|
||||
modifier = Modifier.size(50.dp)
|
||||
modifier = Size50Modifier
|
||||
) {
|
||||
if (mutedInstance.value) {
|
||||
Icon(
|
||||
imageVector = Icons.Default.VolumeOff,
|
||||
"Hash Verified",
|
||||
tint = MaterialTheme.colors.onBackground,
|
||||
modifier = Modifier.size(30.dp)
|
||||
)
|
||||
MutedIcon()
|
||||
} else {
|
||||
Icon(
|
||||
imageVector = Icons.Default.VolumeUp,
|
||||
"Hash Verified",
|
||||
tint = MaterialTheme.colors.onBackground,
|
||||
modifier = Modifier.size(30.dp)
|
||||
)
|
||||
MuteIcon()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -26,15 +26,11 @@ import androidx.compose.foundation.pager.PagerState
|
||||
import androidx.compose.foundation.pager.rememberPagerState
|
||||
import androidx.compose.foundation.text.InlineTextContent
|
||||
import androidx.compose.foundation.text.appendInlineContent
|
||||
import androidx.compose.material.Icon
|
||||
import androidx.compose.material.IconButton
|
||||
import androidx.compose.material.LocalTextStyle
|
||||
import androidx.compose.material.MaterialTheme
|
||||
import androidx.compose.material.Surface
|
||||
import androidx.compose.material.Text
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.DownloadForOffline
|
||||
import androidx.compose.material.icons.filled.Report
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.DisposableEffect
|
||||
import androidx.compose.runtime.Immutable
|
||||
@@ -47,12 +43,10 @@ import androidx.compose.runtime.rememberCoroutineScope
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.layout.ContentScale
|
||||
import androidx.compose.ui.platform.LocalClipboardManager
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.platform.LocalView
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.text.AnnotatedString
|
||||
import androidx.compose.ui.text.Placeholder
|
||||
import androidx.compose.ui.text.PlaceholderVerticalAlign
|
||||
@@ -75,8 +69,14 @@ import com.vitorpamplona.amethyst.ui.actions.CloseButton
|
||||
import com.vitorpamplona.amethyst.ui.actions.LoadingAnimation
|
||||
import com.vitorpamplona.amethyst.ui.actions.SaveToGallery
|
||||
import com.vitorpamplona.amethyst.ui.note.BlankNote
|
||||
import com.vitorpamplona.amethyst.ui.note.DownloadForOfflineIcon
|
||||
import com.vitorpamplona.amethyst.ui.note.HashCheckFailedIcon
|
||||
import com.vitorpamplona.amethyst.ui.note.HashCheckIcon
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
||||
import com.vitorpamplona.amethyst.ui.theme.Font17SP
|
||||
import com.vitorpamplona.amethyst.ui.theme.Size20dp
|
||||
import com.vitorpamplona.amethyst.ui.theme.Size24dp
|
||||
import com.vitorpamplona.amethyst.ui.theme.Size30dp
|
||||
import com.vitorpamplona.amethyst.ui.theme.imageModifier
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
import kotlinx.collections.immutable.toImmutableList
|
||||
@@ -367,15 +367,10 @@ fun ImageUrlWithDownloadButton(url: String, showImage: MutableState<Boolean>) {
|
||||
FlowRow() {
|
||||
ClickableUrl(urlText = url, url = url)
|
||||
IconButton(
|
||||
modifier = Modifier.size(20.dp),
|
||||
modifier = Modifier.size(Size20dp),
|
||||
onClick = { showImage.value = true }
|
||||
) {
|
||||
Icon(
|
||||
imageVector = Icons.Default.DownloadForOffline,
|
||||
null,
|
||||
modifier = Modifier.size(24.dp),
|
||||
tint = MaterialTheme.colors.primary
|
||||
)
|
||||
DownloadForOfflineIcon(Size24dp)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -717,12 +712,7 @@ private fun HashVerificationSymbol(verifiedHash: Boolean, modifier: Modifier) {
|
||||
}
|
||||
}
|
||||
) {
|
||||
Icon(
|
||||
painter = painterResource(R.drawable.ic_verified),
|
||||
"Hash Verified",
|
||||
tint = Color.Unspecified,
|
||||
modifier = Modifier.size(30.dp)
|
||||
)
|
||||
HashCheckIcon(Size30dp)
|
||||
}
|
||||
} else {
|
||||
IconButton(
|
||||
@@ -736,12 +726,7 @@ private fun HashVerificationSymbol(verifiedHash: Boolean, modifier: Modifier) {
|
||||
}
|
||||
}
|
||||
) {
|
||||
Icon(
|
||||
tint = Color.Red,
|
||||
imageVector = Icons.Default.Report,
|
||||
contentDescription = "Invalid Hash",
|
||||
modifier = Modifier.size(30.dp)
|
||||
)
|
||||
HashCheckFailedIcon(Size30dp)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
|
||||
@@ -77,10 +77,12 @@ import com.vitorpamplona.amethyst.service.model.PeopleListEvent
|
||||
import com.vitorpamplona.amethyst.service.relays.Client
|
||||
import com.vitorpamplona.amethyst.service.relays.RelayPool
|
||||
import com.vitorpamplona.amethyst.ui.components.RobohashAsyncImageProxy
|
||||
import com.vitorpamplona.amethyst.ui.note.SearchIcon
|
||||
import com.vitorpamplona.amethyst.ui.screen.equalImmutableLists
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.SpinnerSelectionDialog
|
||||
import com.vitorpamplona.amethyst.ui.theme.BottomTopHeight
|
||||
import com.vitorpamplona.amethyst.ui.theme.Size22Modifier
|
||||
import com.vitorpamplona.amethyst.ui.theme.placeholderText
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
import kotlinx.collections.immutable.toImmutableList
|
||||
@@ -246,7 +248,7 @@ fun GenericTopBar(scaffoldState: ScaffoldState, accountViewModel: AccountViewMod
|
||||
}
|
||||
},
|
||||
actions = {
|
||||
SearchIcon() {
|
||||
SearchButton() {
|
||||
nav(Route.Search.route)
|
||||
}
|
||||
}
|
||||
@@ -256,16 +258,11 @@ fun GenericTopBar(scaffoldState: ScaffoldState, accountViewModel: AccountViewMod
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun SearchIcon(onClick: () -> Unit) {
|
||||
private fun SearchButton(onClick: () -> Unit) {
|
||||
IconButton(
|
||||
onClick = onClick
|
||||
) {
|
||||
Icon(
|
||||
painter = painterResource(R.drawable.ic_search),
|
||||
contentDescription = null,
|
||||
modifier = Modifier.size(22.dp),
|
||||
tint = Color.Unspecified
|
||||
)
|
||||
SearchIcon(modifier = Size22Modifier, Color.Unspecified)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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 }
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
354
app/src/main/java/com/vitorpamplona/amethyst/ui/note/Icons.kt
Normal file
354
app/src/main/java/com/vitorpamplona/amethyst/ui/note/Icons.kt
Normal file
@@ -0,0 +1,354 @@
|
||||
package com.vitorpamplona.amethyst.ui.note
|
||||
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.material.Icon
|
||||
import androidx.compose.material.MaterialTheme
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.ArrowBack
|
||||
import androidx.compose.material.icons.filled.Bolt
|
||||
import androidx.compose.material.icons.filled.Cancel
|
||||
import androidx.compose.material.icons.filled.Clear
|
||||
import androidx.compose.material.icons.filled.DownloadForOffline
|
||||
import androidx.compose.material.icons.filled.Downloading
|
||||
import androidx.compose.material.icons.filled.ExpandLess
|
||||
import androidx.compose.material.icons.filled.ExpandMore
|
||||
import androidx.compose.material.icons.filled.Link
|
||||
import androidx.compose.material.icons.filled.MoreVert
|
||||
import androidx.compose.material.icons.filled.PushPin
|
||||
import androidx.compose.material.icons.filled.Report
|
||||
import androidx.compose.material.icons.filled.VolumeOff
|
||||
import androidx.compose.material.icons.filled.VolumeUp
|
||||
import androidx.compose.material.icons.outlined.BarChart
|
||||
import androidx.compose.material.icons.outlined.PlayCircle
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.unit.Dp
|
||||
import com.vitorpamplona.amethyst.R
|
||||
import com.vitorpamplona.amethyst.ui.theme.BitcoinOrange
|
||||
import com.vitorpamplona.amethyst.ui.theme.Size15Modifier
|
||||
import com.vitorpamplona.amethyst.ui.theme.Size20Modifier
|
||||
import com.vitorpamplona.amethyst.ui.theme.Size30Modifier
|
||||
import com.vitorpamplona.amethyst.ui.theme.placeholderText
|
||||
import com.vitorpamplona.amethyst.ui.theme.subtleButton
|
||||
|
||||
@Composable
|
||||
fun FollowingIcon(iconSize: Dp) {
|
||||
Icon(
|
||||
painter = painterResource(R.drawable.following),
|
||||
contentDescription = stringResource(id = R.string.following),
|
||||
modifier = remember(iconSize) { Modifier.size(iconSize) },
|
||||
tint = Color.Unspecified
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun ArrowBackIcon(iconSize: Dp) {
|
||||
Icon(
|
||||
imageVector = Icons.Default.ArrowBack,
|
||||
contentDescription = null,
|
||||
modifier = remember(iconSize) { Modifier.size(iconSize) },
|
||||
tint = MaterialTheme.colors.primary
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun MessageIcon(modifier: Modifier) {
|
||||
Icon(
|
||||
painter = painterResource(R.drawable.ic_dm),
|
||||
null,
|
||||
modifier = modifier,
|
||||
tint = MaterialTheme.colors.primary
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun DownloadForOfflineIcon(iconSize: Dp) {
|
||||
Icon(
|
||||
imageVector = Icons.Default.DownloadForOffline,
|
||||
null,
|
||||
modifier = remember(iconSize) { Modifier.size(iconSize) },
|
||||
tint = MaterialTheme.colors.primary
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun HashCheckIcon(iconSize: Dp) {
|
||||
Icon(
|
||||
painter = painterResource(R.drawable.original),
|
||||
contentDescription = stringResource(id = R.string.hash_verification_passed),
|
||||
modifier = remember(iconSize) { Modifier.size(iconSize) },
|
||||
tint = Color.Unspecified
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun HashCheckFailedIcon(iconSize: Dp) {
|
||||
Icon(
|
||||
imageVector = Icons.Default.Report,
|
||||
contentDescription = stringResource(id = R.string.hash_verification_failed),
|
||||
modifier = remember(iconSize) { Modifier.size(iconSize) },
|
||||
tint = Color.Red
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun LikedIcon(iconSize: Dp) {
|
||||
Icon(
|
||||
painter = painterResource(R.drawable.ic_liked),
|
||||
null,
|
||||
modifier = remember(iconSize) { Modifier.size(iconSize) },
|
||||
tint = Color.Unspecified
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun LikeIcon(iconSize: Dp, grayTint: Color) {
|
||||
Icon(
|
||||
painter = painterResource(R.drawable.ic_like),
|
||||
null,
|
||||
modifier = remember(iconSize) { Modifier.size(iconSize) },
|
||||
tint = grayTint
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun RepostedIcon(modifier: Modifier, tint: Color = Color.Unspecified) {
|
||||
Icon(
|
||||
painter = painterResource(R.drawable.ic_retweeted),
|
||||
null,
|
||||
modifier = modifier,
|
||||
tint = tint
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun LightningAddressIcon(modifier: Modifier, tint: Color = Color.Unspecified) {
|
||||
Icon(
|
||||
imageVector = Icons.Default.Bolt,
|
||||
contentDescription = stringResource(R.string.lightning_address),
|
||||
tint = tint,
|
||||
modifier = modifier
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun ZappedIcon(iconSize: Dp) {
|
||||
ZappedIcon(modifier = remember(iconSize) { Modifier.size(iconSize) })
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun ZappedIcon(modifier: Modifier) {
|
||||
ZapIcon(modifier = modifier, BitcoinOrange)
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun ZapIcon(iconSize: Dp, tint: Color = Color.Unspecified) {
|
||||
ZapIcon(modifier = remember(iconSize) { Modifier.size(iconSize) }, tint)
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun ZapIcon(modifier: Modifier, tint: Color = Color.Unspecified) {
|
||||
Icon(
|
||||
imageVector = Icons.Default.Bolt,
|
||||
contentDescription = stringResource(R.string.zaps),
|
||||
tint = tint,
|
||||
modifier = modifier
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun ExpandLessIcon(modifier: Modifier) {
|
||||
Icon(
|
||||
imageVector = Icons.Default.ExpandLess,
|
||||
null,
|
||||
modifier = modifier,
|
||||
tint = MaterialTheme.colors.subtleButton
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun ExpandMoreIcon(modifier: Modifier) {
|
||||
Icon(
|
||||
imageVector = Icons.Default.ExpandMore,
|
||||
null,
|
||||
modifier = modifier,
|
||||
tint = MaterialTheme.colors.subtleButton
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun CommentIcon(iconSize: Dp, tint: Color) {
|
||||
Icon(
|
||||
painter = painterResource(R.drawable.ic_comment),
|
||||
contentDescription = null,
|
||||
modifier = remember { Modifier.size(iconSize) },
|
||||
tint = tint
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun ViewCountIcon(iconSize: Dp, tint: Color = Color.Unspecified) {
|
||||
ViewCountIcon(remember { Modifier.size(iconSize) }, tint)
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun ViewCountIcon(modifier: Modifier, tint: Color = Color.Unspecified) {
|
||||
Icon(
|
||||
imageVector = Icons.Outlined.BarChart,
|
||||
null,
|
||||
modifier = modifier,
|
||||
tint = tint
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun PollIcon() {
|
||||
Icon(
|
||||
painter = painterResource(R.drawable.ic_poll),
|
||||
null,
|
||||
modifier = Size20Modifier,
|
||||
tint = MaterialTheme.colors.onBackground
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun RegularPostIcon() {
|
||||
Icon(
|
||||
painter = painterResource(R.drawable.ic_lists),
|
||||
null,
|
||||
modifier = Size20Modifier,
|
||||
tint = MaterialTheme.colors.onBackground
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun CancelIcon() {
|
||||
Icon(
|
||||
imageVector = Icons.Default.Cancel,
|
||||
null,
|
||||
modifier = Size30Modifier,
|
||||
tint = MaterialTheme.colors.placeholderText
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun CloseIcon() {
|
||||
Icon(
|
||||
painter = painterResource(id = R.drawable.ic_close),
|
||||
contentDescription = stringResource(id = R.string.cancel),
|
||||
modifier = Size20Modifier,
|
||||
tint = Color.White
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun MutedIcon() {
|
||||
Icon(
|
||||
imageVector = Icons.Default.VolumeOff,
|
||||
contentDescription = stringResource(id = R.string.muted_button),
|
||||
tint = MaterialTheme.colors.onBackground,
|
||||
modifier = Size30Modifier
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun MuteIcon() {
|
||||
Icon(
|
||||
imageVector = Icons.Default.VolumeUp,
|
||||
contentDescription = stringResource(id = R.string.mute_button),
|
||||
tint = MaterialTheme.colors.onBackground,
|
||||
modifier = Size30Modifier
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun SearchIcon(modifier: Modifier, tint: Color = Color.Unspecified) {
|
||||
Icon(
|
||||
painter = painterResource(R.drawable.ic_search),
|
||||
contentDescription = stringResource(id = R.string.search_button),
|
||||
modifier = modifier,
|
||||
tint = tint
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun PlayIcon(modifier: Modifier, tint: Color) {
|
||||
Icon(
|
||||
imageVector = Icons.Outlined.PlayCircle,
|
||||
contentDescription = null,
|
||||
modifier = modifier,
|
||||
tint = tint
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun PinIcon(modifier: Modifier, tint: Color) {
|
||||
Icon(
|
||||
imageVector = Icons.Default.PushPin,
|
||||
contentDescription = null,
|
||||
modifier = modifier,
|
||||
tint = tint
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun ClearTextIcon() {
|
||||
Icon(
|
||||
imageVector = Icons.Default.Clear,
|
||||
contentDescription = stringResource(R.string.clear)
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun LinkIcon(modifier: Modifier, tint: Color) {
|
||||
Icon(
|
||||
imageVector = Icons.Default.Link,
|
||||
contentDescription = stringResource(R.string.website),
|
||||
modifier = modifier,
|
||||
tint = tint
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun VerticalDotsIcon() {
|
||||
Icon(
|
||||
imageVector = Icons.Default.MoreVert,
|
||||
null,
|
||||
modifier = Size15Modifier,
|
||||
tint = MaterialTheme.colors.placeholderText
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun NIP05CheckingIcon(modifier: Modifier) {
|
||||
Icon(
|
||||
imageVector = Icons.Default.Downloading,
|
||||
contentDescription = stringResource(id = R.string.nip05_checking),
|
||||
modifier = modifier,
|
||||
tint = Color.Yellow
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun NIP05VerifiedIcon(modifier: Modifier) {
|
||||
Icon(
|
||||
painter = painterResource(R.drawable.nip_05_light),
|
||||
contentDescription = stringResource(id = R.string.nip05_verified),
|
||||
modifier = modifier,
|
||||
tint = Color.Unspecified
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun NIP05FailedVerification(modifier: Modifier) {
|
||||
Icon(
|
||||
imageVector = Icons.Default.Report,
|
||||
contentDescription = stringResource(id = R.string.nip05_failed),
|
||||
modifier = modifier,
|
||||
tint = Color.Red
|
||||
)
|
||||
}
|
||||
@@ -10,7 +10,6 @@ import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.layout.width
|
||||
import androidx.compose.material.Icon
|
||||
import androidx.compose.material.MaterialTheme
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
@@ -24,9 +23,7 @@ import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.compositeOver
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.vitorpamplona.amethyst.R
|
||||
import com.vitorpamplona.amethyst.ui.screen.MessageSetCard
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
||||
import com.vitorpamplona.amethyst.ui.theme.newItemBackgroundColor
|
||||
@@ -35,13 +32,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,14 +84,28 @@ fun MessageSetCompose(messageSetCard: MessageSetCard, routeForLastRead: String,
|
||||
)?.let { nav(it) }
|
||||
}
|
||||
},
|
||||
onLongClick = { popupExpanded = true }
|
||||
onLongClick = enablePopup
|
||||
)
|
||||
.fillMaxWidth()
|
||||
}
|
||||
|
||||
Column(columnModifier) {
|
||||
Row(Modifier.fillMaxWidth()) {
|
||||
MessageIcon()
|
||||
Box(
|
||||
modifier = remember {
|
||||
Modifier
|
||||
.width(55.dp)
|
||||
.padding(top = 5.dp, end = 5.dp)
|
||||
}
|
||||
) {
|
||||
MessageIcon(
|
||||
remember {
|
||||
Modifier
|
||||
.size(16.dp)
|
||||
.align(Alignment.TopEnd)
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
Column(modifier = remember { Modifier.padding(start = 10.dp) }) {
|
||||
NoteCompose(
|
||||
@@ -99,35 +113,14 @@ 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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun MessageIcon() {
|
||||
Box(
|
||||
modifier = remember {
|
||||
Modifier
|
||||
.width(55.dp)
|
||||
.padding(top = 5.dp, end = 5.dp)
|
||||
}
|
||||
) {
|
||||
Icon(
|
||||
painter = painterResource(R.drawable.ic_dm),
|
||||
null,
|
||||
modifier = remember {
|
||||
Modifier
|
||||
.size(16.dp)
|
||||
.align(Alignment.TopEnd)
|
||||
},
|
||||
tint = MaterialTheme.colors.primary
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,11 +17,8 @@ import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.layout.width
|
||||
import androidx.compose.foundation.shape.CircleShape
|
||||
import androidx.compose.material.Icon
|
||||
import androidx.compose.material.MaterialTheme
|
||||
import androidx.compose.material.Text
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.Bolt
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.Immutable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
@@ -32,14 +29,12 @@ 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.graphics.compositeOver
|
||||
import androidx.compose.ui.layout.ContentScale
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.SpanStyle
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
@@ -62,11 +57,11 @@ import com.vitorpamplona.amethyst.ui.screen.CombinedZap
|
||||
import com.vitorpamplona.amethyst.ui.screen.MultiSetCard
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.showAmountAxis
|
||||
import com.vitorpamplona.amethyst.ui.theme.BitcoinOrange
|
||||
import com.vitorpamplona.amethyst.ui.theme.NotificationIconModifier
|
||||
import com.vitorpamplona.amethyst.ui.theme.NotificationIconModifierSmaller
|
||||
import com.vitorpamplona.amethyst.ui.theme.Size10dp
|
||||
import com.vitorpamplona.amethyst.ui.theme.Size18dp
|
||||
import com.vitorpamplona.amethyst.ui.theme.Size19dp
|
||||
import com.vitorpamplona.amethyst.ui.theme.Size25dp
|
||||
import com.vitorpamplona.amethyst.ui.theme.Size35Modifier
|
||||
import com.vitorpamplona.amethyst.ui.theme.Size35dp
|
||||
@@ -86,10 +81,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 +129,7 @@ fun MultiSetCompose(multiSetCard: MultiSetCard, routeForLastRead: String, accoun
|
||||
routeFor(baseNote, accountViewModel.userProfile())?.let { nav(it) }
|
||||
}
|
||||
},
|
||||
onLongClick = { popupExpanded = true }
|
||||
onLongClick = enablePopup
|
||||
)
|
||||
.fillMaxWidth()
|
||||
}
|
||||
@@ -150,6 +149,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 +157,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)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -234,13 +234,7 @@ fun RenderLikeGallery(
|
||||
)
|
||||
} else {
|
||||
when (val shortReaction = reactionType) {
|
||||
"+" -> Icon(
|
||||
painter = painterResource(R.drawable.ic_liked),
|
||||
null,
|
||||
modifier = modifier.size(Size18dp),
|
||||
tint = Color.Unspecified
|
||||
)
|
||||
|
||||
"+" -> LikedIcon(Size18dp)
|
||||
"-" -> Text(text = "\uD83D\uDC4E", modifier = modifier)
|
||||
else -> Text(text = shortReaction, modifier = modifier)
|
||||
}
|
||||
@@ -260,30 +254,20 @@ fun RenderZapGallery(
|
||||
accountViewModel: AccountViewModel
|
||||
) {
|
||||
Row(Modifier.fillMaxWidth()) {
|
||||
ZapIcon()
|
||||
Box(
|
||||
modifier = WidthAuthorPictureModifier
|
||||
) {
|
||||
ZappedIcon(
|
||||
modifier = remember {
|
||||
Modifier.size(Size25dp).align(Alignment.TopEnd)
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
AuthorGalleryZaps(zapEvents, backgroundColor, nav, accountViewModel)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun ZapIcon() {
|
||||
Box(
|
||||
modifier = WidthAuthorPictureModifier
|
||||
) {
|
||||
Icon(
|
||||
imageVector = Icons.Default.Bolt,
|
||||
contentDescription = "Zaps",
|
||||
tint = BitcoinOrange,
|
||||
modifier =
|
||||
Modifier
|
||||
.size(Size25dp)
|
||||
.align(Alignment.TopEnd)
|
||||
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun RenderBoostGallery(
|
||||
boostEvents: ImmutableList<Note>,
|
||||
@@ -296,15 +280,8 @@ fun RenderBoostGallery(
|
||||
Box(
|
||||
modifier = NotificationIconModifierSmaller
|
||||
) {
|
||||
Icon(
|
||||
painter = painterResource(R.drawable.ic_retweeted),
|
||||
null,
|
||||
modifier = remember {
|
||||
Modifier
|
||||
.size(19.dp)
|
||||
.align(Alignment.TopEnd)
|
||||
},
|
||||
tint = Color.Unspecified
|
||||
RepostedIcon(
|
||||
modifier = remember { Modifier.size(Size19dp).align(Alignment.TopEnd) }
|
||||
)
|
||||
}
|
||||
|
||||
@@ -558,7 +535,7 @@ fun WatchUserMetadataAndFollowsAndRenderUserProfilePicture(
|
||||
}
|
||||
}
|
||||
|
||||
WatchFollows(author.pubkeyHex, accountViewModel) { isFollowing ->
|
||||
WatchUserFollows(author.pubkeyHex, accountViewModel) { isFollowing ->
|
||||
Crossfade(targetState = isFollowing) {
|
||||
if (it) {
|
||||
Box(modifier = Size35Modifier, contentAlignment = Alignment.TopEnd) {
|
||||
|
||||
@@ -6,13 +6,8 @@ import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.text.ClickableText
|
||||
import androidx.compose.material.Icon
|
||||
import androidx.compose.material.LocalTextStyle
|
||||
import androidx.compose.material.MaterialTheme
|
||||
import androidx.compose.material.Text
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.Downloading
|
||||
import androidx.compose.material.icons.filled.Report
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.MutableState
|
||||
@@ -23,21 +18,22 @@ import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.platform.LocalUriHandler
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.text.AnnotatedString
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.lifecycle.map
|
||||
import com.vitorpamplona.amethyst.R
|
||||
import com.vitorpamplona.amethyst.model.Note
|
||||
import com.vitorpamplona.amethyst.model.TimeUtils
|
||||
import com.vitorpamplona.amethyst.model.User
|
||||
import com.vitorpamplona.amethyst.model.UserMetadata
|
||||
import com.vitorpamplona.amethyst.service.Nip05Verifier
|
||||
import com.vitorpamplona.amethyst.ui.theme.Nip05
|
||||
import com.vitorpamplona.amethyst.ui.theme.placeholderText
|
||||
import com.vitorpamplona.amethyst.ui.note.NIP05CheckingIcon
|
||||
import com.vitorpamplona.amethyst.ui.note.NIP05FailedVerification
|
||||
import com.vitorpamplona.amethyst.ui.note.NIP05VerifiedIcon
|
||||
import com.vitorpamplona.amethyst.ui.theme.NIP05IconSize
|
||||
import com.vitorpamplona.amethyst.ui.theme.Nip05EmailColor
|
||||
import com.vitorpamplona.amethyst.ui.theme.Size16Modifier
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
@@ -152,52 +148,31 @@ private fun DisplayNIP05(
|
||||
if (user != "_") {
|
||||
Text(
|
||||
text = remember(nip05) { AnnotatedString(user) },
|
||||
color = MaterialTheme.colors.placeholderText,
|
||||
color = Nip05EmailColor,
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis
|
||||
)
|
||||
}
|
||||
|
||||
NIP05VerifiedSymbol(nip05Verified)
|
||||
NIP05VerifiedSymbol(nip05Verified, NIP05IconSize)
|
||||
|
||||
ClickableText(
|
||||
text = remember(nip05) { AnnotatedString(domain) },
|
||||
onClick = { runCatching { uri.openUri("https://$domain") } },
|
||||
style = LocalTextStyle.current.copy(color = MaterialTheme.colors.primary.copy(0.52f)),
|
||||
style = LocalTextStyle.current.copy(color = Nip05EmailColor),
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Visible
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun NIP05VerifiedSymbol(nip05Verified: MutableState<Boolean?>) {
|
||||
if (nip05Verified.value == null) {
|
||||
Icon(
|
||||
tint = Color.Yellow,
|
||||
imageVector = Icons.Default.Downloading,
|
||||
contentDescription = "Downloading",
|
||||
modifier = Modifier
|
||||
.size(14.dp)
|
||||
.padding(top = 1.dp)
|
||||
)
|
||||
} else if (nip05Verified.value == true) {
|
||||
Icon(
|
||||
painter = painterResource(R.drawable.ic_verified_transparent),
|
||||
"NIP-05 Verified",
|
||||
tint = Nip05.copy(0.52f),
|
||||
modifier = Modifier
|
||||
.size(14.dp)
|
||||
.padding(top = 1.dp)
|
||||
)
|
||||
} else {
|
||||
Icon(
|
||||
tint = Color.Red,
|
||||
imageVector = Icons.Default.Report,
|
||||
contentDescription = "Invalid Nip05",
|
||||
modifier = Modifier
|
||||
.size(14.dp)
|
||||
.padding(top = 1.dp)
|
||||
)
|
||||
private fun NIP05VerifiedSymbol(nip05Verified: MutableState<Boolean?>, modifier: Modifier) {
|
||||
Crossfade(targetState = nip05Verified.value) {
|
||||
when (it) {
|
||||
null -> NIP05CheckingIcon(modifier = modifier)
|
||||
true -> NIP05VerifiedIcon(modifier = modifier)
|
||||
false -> NIP05FailedVerification(modifier = modifier)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -207,31 +182,9 @@ fun DisplayNip05ProfileStatus(user: User) {
|
||||
|
||||
user.nip05()?.let { nip05 ->
|
||||
if (nip05.split("@").size <= 2) {
|
||||
val nip05Verified by nip05VerificationAsAState(user.info!!, user.pubkeyHex)
|
||||
val nip05Verified = nip05VerificationAsAState(user.info!!, user.pubkeyHex)
|
||||
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||
if (nip05Verified == null) {
|
||||
Icon(
|
||||
tint = Color.Yellow,
|
||||
imageVector = Icons.Default.Downloading,
|
||||
contentDescription = "Downloading",
|
||||
modifier = Modifier.size(16.dp)
|
||||
)
|
||||
} else if (nip05Verified == true) {
|
||||
Icon(
|
||||
painter = painterResource(R.drawable.ic_verified_transparent),
|
||||
"NIP-05 Verified",
|
||||
tint = Nip05,
|
||||
modifier = Modifier.size(16.dp)
|
||||
)
|
||||
} else {
|
||||
Icon(
|
||||
tint = Color.Red,
|
||||
imageVector = Icons.Default.Report,
|
||||
contentDescription = "Invalid Nip05",
|
||||
modifier = Modifier.size(16.dp)
|
||||
)
|
||||
}
|
||||
|
||||
NIP05VerifiedSymbol(nip05Verified, Size16Modifier)
|
||||
var domainPadStart = 5.dp
|
||||
|
||||
val (user, domain) = remember(nip05) {
|
||||
@@ -246,6 +199,7 @@ fun DisplayNip05ProfileStatus(user: User) {
|
||||
if (user != "_") {
|
||||
Text(
|
||||
text = remember { AnnotatedString(user + "@") },
|
||||
color = Nip05EmailColor,
|
||||
modifier = Modifier.padding(top = 1.dp, bottom = 1.dp, start = 5.dp),
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis
|
||||
@@ -256,7 +210,7 @@ fun DisplayNip05ProfileStatus(user: User) {
|
||||
ClickableText(
|
||||
text = AnnotatedString(domain),
|
||||
onClick = { nip05.let { runCatching { uri.openUri("https://${it.split("@")[1]}") } } },
|
||||
style = LocalTextStyle.current.copy(color = MaterialTheme.colors.primary),
|
||||
style = LocalTextStyle.current.copy(color = Nip05EmailColor),
|
||||
modifier = Modifier.padding(top = 1.dp, bottom = 1.dp, start = domainPadStart),
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis
|
||||
|
||||
@@ -29,17 +29,11 @@ import androidx.compose.foundation.text.ClickableText
|
||||
import androidx.compose.material.Button
|
||||
import androidx.compose.material.ButtonDefaults
|
||||
import androidx.compose.material.Divider
|
||||
import androidx.compose.material.Icon
|
||||
import androidx.compose.material.IconButton
|
||||
import androidx.compose.material.LocalTextStyle
|
||||
import androidx.compose.material.MaterialTheme
|
||||
import androidx.compose.material.Text
|
||||
import androidx.compose.material.darkColors
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.Bolt
|
||||
import androidx.compose.material.icons.filled.Link
|
||||
import androidx.compose.material.icons.filled.MoreVert
|
||||
import androidx.compose.material.icons.filled.PushPin
|
||||
import androidx.compose.material.lightColors
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.Immutable
|
||||
@@ -130,6 +124,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
|
||||
@@ -152,7 +147,6 @@ import com.vitorpamplona.amethyst.ui.screen.loggedIn.JoinCommunityButton
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.LeaveCommunityButton
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.LiveFlag
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.ScheduledFlag
|
||||
import com.vitorpamplona.amethyst.ui.theme.BitcoinOrange
|
||||
import com.vitorpamplona.amethyst.ui.theme.ButtonBorder
|
||||
import com.vitorpamplona.amethyst.ui.theme.DividerThickness
|
||||
import com.vitorpamplona.amethyst.ui.theme.DoubleHorzSpacer
|
||||
@@ -163,6 +157,8 @@ import com.vitorpamplona.amethyst.ui.theme.HalfStartPadding
|
||||
import com.vitorpamplona.amethyst.ui.theme.HalfVertSpacer
|
||||
import com.vitorpamplona.amethyst.ui.theme.QuoteBorder
|
||||
import com.vitorpamplona.amethyst.ui.theme.Size15Modifier
|
||||
import com.vitorpamplona.amethyst.ui.theme.Size16Modifier
|
||||
import com.vitorpamplona.amethyst.ui.theme.Size20Modifier
|
||||
import com.vitorpamplona.amethyst.ui.theme.Size24Modifier
|
||||
import com.vitorpamplona.amethyst.ui.theme.Size25dp
|
||||
import com.vitorpamplona.amethyst.ui.theme.Size30Modifier
|
||||
@@ -213,6 +209,7 @@ fun NoteCompose(
|
||||
unPackReply: Boolean = true,
|
||||
makeItShort: Boolean = false,
|
||||
addMarginTop: Boolean = true,
|
||||
showHidden: Boolean = false,
|
||||
parentBackgroundColor: MutableState<Color>? = null,
|
||||
accountViewModel: AccountViewModel,
|
||||
nav: (String) -> Unit
|
||||
@@ -236,17 +233,18 @@ fun NoteCompose(
|
||||
}
|
||||
} else {
|
||||
CheckHiddenNoteCompose(
|
||||
baseNote,
|
||||
routeForLastRead,
|
||||
modifier,
|
||||
isBoostedNote,
|
||||
isQuotedNote,
|
||||
unPackReply,
|
||||
makeItShort,
|
||||
addMarginTop,
|
||||
parentBackgroundColor,
|
||||
accountViewModel,
|
||||
nav
|
||||
note = baseNote,
|
||||
routeForLastRead = routeForLastRead,
|
||||
modifier = modifier,
|
||||
isBoostedNote = isBoostedNote,
|
||||
isQuotedNote = isQuotedNote,
|
||||
unPackReply = unPackReply,
|
||||
makeItShort = makeItShort,
|
||||
addMarginTop = addMarginTop,
|
||||
showHidden = showHidden,
|
||||
parentBackgroundColor = parentBackgroundColor,
|
||||
accountViewModel = accountViewModel,
|
||||
nav = nav
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -262,30 +260,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
|
||||
val state by remember {
|
||||
mutableStateOf(
|
||||
NoteComposeReportState(
|
||||
isAcceptable = true,
|
||||
canPreview = true,
|
||||
relevantReports = persistentSetOf()
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
RenderReportState(
|
||||
state = state,
|
||||
note = note,
|
||||
routeForLastRead = routeForLastRead,
|
||||
modifier = modifier,
|
||||
isBoostedNote = isBoostedNote,
|
||||
isQuotedNote = isQuotedNote,
|
||||
unPackReply = unPackReply,
|
||||
makeItShort = makeItShort,
|
||||
addMarginTop = addMarginTop,
|
||||
parentBackgroundColor = parentBackgroundColor,
|
||||
accountViewModel = accountViewModel,
|
||||
nav = nav
|
||||
)
|
||||
} else {
|
||||
val isHidden by accountViewModel.account.liveHiddenUsers.map {
|
||||
note.isHiddenFor(it)
|
||||
}.observeAsState(accountViewModel.isNoteHidden(note))
|
||||
|
||||
Crossfade(targetState = isHidden) {
|
||||
if (!it) {
|
||||
LoadedNoteCompose(
|
||||
note = note,
|
||||
routeForLastRead = routeForLastRead,
|
||||
modifier = modifier,
|
||||
isBoostedNote = isBoostedNote,
|
||||
isQuotedNote = isQuotedNote,
|
||||
unPackReply = unPackReply,
|
||||
makeItShort = makeItShort,
|
||||
addMarginTop = addMarginTop,
|
||||
parentBackgroundColor = parentBackgroundColor,
|
||||
accountViewModel = accountViewModel,
|
||||
nav = nav
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -433,15 +460,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 +499,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 +534,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 +676,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 +741,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 +1187,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}"
|
||||
}
|
||||
@@ -1383,12 +1426,7 @@ fun RenderAppDefinition(
|
||||
val website = remember(it) { it.website }
|
||||
if (!website.isNullOrEmpty()) {
|
||||
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||
Icon(
|
||||
tint = MaterialTheme.colors.placeholderText,
|
||||
imageVector = Icons.Default.Link,
|
||||
contentDescription = stringResource(R.string.website),
|
||||
modifier = Modifier.size(16.dp)
|
||||
)
|
||||
LinkIcon(Size16Modifier, MaterialTheme.colors.placeholderText)
|
||||
|
||||
ClickableText(
|
||||
text = AnnotatedString(website.removePrefix("https://")),
|
||||
@@ -1441,11 +1479,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,
|
||||
@@ -2130,12 +2172,7 @@ fun RenderPinListEvent(
|
||||
FlowRow(modifier = Modifier.padding(top = 5.dp)) {
|
||||
pinsToShow.forEach { pin ->
|
||||
Row(modifier = Modifier.fillMaxWidth(), verticalAlignment = CenterVertically) {
|
||||
Icon(
|
||||
imageVector = Icons.Default.PushPin,
|
||||
contentDescription = null,
|
||||
tint = MaterialTheme.colors.onBackground.copy(0.32f),
|
||||
modifier = Modifier.size(15.dp)
|
||||
)
|
||||
PinIcon(modifier = Size15Modifier, tint = MaterialTheme.colors.onBackground.copy(0.32f))
|
||||
|
||||
Spacer(modifier = Modifier.width(5.dp))
|
||||
|
||||
@@ -2298,6 +2335,7 @@ private fun ReplyRow(
|
||||
channelHex = channelHex,
|
||||
showVideo = false,
|
||||
showBottomDiviser = false,
|
||||
sendToChannel = true,
|
||||
modifier = remember { Modifier.padding(vertical = 5.dp) },
|
||||
accountViewModel = accountViewModel,
|
||||
nav = nav
|
||||
@@ -2428,23 +2466,20 @@ 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
|
||||
)
|
||||
}
|
||||
@@ -2613,6 +2648,7 @@ fun DisplayHighlight(
|
||||
highlight: String,
|
||||
authorHex: String?,
|
||||
url: String?,
|
||||
postAddress: ATag?,
|
||||
makeItShort: Boolean,
|
||||
canPreview: Boolean,
|
||||
backgroundColor: MutableState<Color>,
|
||||
@@ -2637,13 +2673,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 +2697,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 +2754,7 @@ private fun LoadAndDisplayUrl(url: String) {
|
||||
}
|
||||
|
||||
validatedUrl?.host?.let { host ->
|
||||
Text(remember { "on " }, maxLines = 1)
|
||||
Text(remember { "-" }, maxLines = 1)
|
||||
ClickableUrl(urlText = host, url = url)
|
||||
}
|
||||
}
|
||||
@@ -2928,19 +3000,9 @@ private fun RenderPledgeAmount(
|
||||
}
|
||||
|
||||
if (hasPledge) {
|
||||
Icon(
|
||||
imageVector = Icons.Default.Bolt,
|
||||
contentDescription = stringResource(R.string.zaps),
|
||||
modifier = Modifier.size(20.dp),
|
||||
tint = BitcoinOrange
|
||||
)
|
||||
ZappedIcon(modifier = Size20Modifier)
|
||||
} else {
|
||||
Icon(
|
||||
imageVector = Icons.Default.Bolt,
|
||||
contentDescription = stringResource(R.string.zaps),
|
||||
modifier = Modifier.size(20.dp),
|
||||
tint = MaterialTheme.colors.placeholderText
|
||||
)
|
||||
ZapIcon(modifier = Size20Modifier, MaterialTheme.colors.placeholderText)
|
||||
}
|
||||
|
||||
Text(
|
||||
|
||||
@@ -38,12 +38,6 @@ import androidx.compose.material.LinearProgressIndicator
|
||||
import androidx.compose.material.MaterialTheme
|
||||
import androidx.compose.material.ProgressIndicatorDefaults
|
||||
import androidx.compose.material.Text
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.Bolt
|
||||
import androidx.compose.material.icons.filled.ExpandLess
|
||||
import androidx.compose.material.icons.filled.ExpandMore
|
||||
import androidx.compose.material.icons.outlined.BarChart
|
||||
import androidx.compose.material.icons.outlined.Bolt
|
||||
import androidx.compose.material.ripple.rememberRipple
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
@@ -88,7 +82,6 @@ import com.vitorpamplona.amethyst.ui.components.InLineIconRenderer
|
||||
import com.vitorpamplona.amethyst.ui.components.TextType
|
||||
import com.vitorpamplona.amethyst.ui.screen.CombinedZap
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
||||
import com.vitorpamplona.amethyst.ui.theme.BitcoinOrange
|
||||
import com.vitorpamplona.amethyst.ui.theme.ButtonBorder
|
||||
import com.vitorpamplona.amethyst.ui.theme.DarkerGreen
|
||||
import com.vitorpamplona.amethyst.ui.theme.Font14SP
|
||||
@@ -112,7 +105,6 @@ import com.vitorpamplona.amethyst.ui.theme.TinyBorders
|
||||
import com.vitorpamplona.amethyst.ui.theme.mediumImportanceLink
|
||||
import com.vitorpamplona.amethyst.ui.theme.placeholderText
|
||||
import com.vitorpamplona.amethyst.ui.theme.placeholderTextColorFilter
|
||||
import com.vitorpamplona.amethyst.ui.theme.subtleButton
|
||||
import kotlinx.collections.immutable.toImmutableList
|
||||
import kotlinx.collections.immutable.toImmutableMap
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
@@ -363,6 +355,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>,
|
||||
@@ -391,19 +397,9 @@ private fun RenderShowIndividualReactionsButton(wantsToSeeReactions: MutableStat
|
||||
) {
|
||||
Crossfade(targetState = wantsToSeeReactions.value) {
|
||||
if (it) {
|
||||
Icon(
|
||||
imageVector = Icons.Default.ExpandLess,
|
||||
null,
|
||||
modifier = Size22Modifier,
|
||||
tint = MaterialTheme.colors.subtleButton
|
||||
)
|
||||
ExpandLessIcon(modifier = Size22Modifier)
|
||||
} else {
|
||||
Icon(
|
||||
imageVector = Icons.Default.ExpandMore,
|
||||
null,
|
||||
modifier = Size22Modifier,
|
||||
tint = MaterialTheme.colors.subtleButton
|
||||
)
|
||||
ExpandMoreIcon(modifier = Size22Modifier)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -544,12 +540,7 @@ fun ReplyReaction(
|
||||
}
|
||||
}
|
||||
) {
|
||||
Icon(
|
||||
painter = painterResource(R.drawable.ic_comment),
|
||||
contentDescription = null,
|
||||
modifier = remember { Modifier.size(iconSize) },
|
||||
tint = grayTint
|
||||
)
|
||||
CommentIcon(iconSize, grayTint)
|
||||
}
|
||||
|
||||
if (showCounter) {
|
||||
@@ -690,12 +681,7 @@ fun BoostIcon(baseNote: Note, iconSize: Dp = Size20dp, grayTint: Color, accountV
|
||||
Modifier.size(iconSize)
|
||||
}
|
||||
|
||||
Icon(
|
||||
painter = painterResource(R.drawable.ic_retweeted),
|
||||
null,
|
||||
modifier = iconModifier,
|
||||
tint = iconTint
|
||||
)
|
||||
RepostedIcon(iconModifier, iconTint)
|
||||
}
|
||||
|
||||
@Composable
|
||||
@@ -805,7 +791,7 @@ fun LikeIcon(
|
||||
if (value != null) {
|
||||
RenderReactionType(value, iconSize, iconFontSize)
|
||||
} else {
|
||||
RenderLikeIcon(iconSize, grayTint)
|
||||
LikeIcon(iconSize, grayTint)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -822,33 +808,12 @@ private fun WatchReactionTypeForNote(baseNote: Note, accountViewModel: AccountVi
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun RenderLikeIcon(
|
||||
iconSize: Dp = Size20dp,
|
||||
grayTint: Color
|
||||
) {
|
||||
val iconModifier = remember {
|
||||
Modifier.size(iconSize)
|
||||
}
|
||||
|
||||
Icon(
|
||||
painter = painterResource(R.drawable.ic_like),
|
||||
null,
|
||||
modifier = iconModifier,
|
||||
tint = grayTint
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun RenderReactionType(
|
||||
reactionType: String,
|
||||
iconSize: Dp = Size20dp,
|
||||
iconFontSize: TextUnit
|
||||
) {
|
||||
val iconModifier = remember {
|
||||
Modifier.size(iconSize)
|
||||
}
|
||||
|
||||
if (reactionType.startsWith(":")) {
|
||||
val noStartColon = reactionType.removePrefix(":")
|
||||
val url = noStartColon.substringAfter(":")
|
||||
@@ -865,15 +830,7 @@ private fun RenderReactionType(
|
||||
)
|
||||
} else {
|
||||
when (reactionType) {
|
||||
"+" -> {
|
||||
Icon(
|
||||
painter = painterResource(R.drawable.ic_liked),
|
||||
null,
|
||||
modifier = iconModifier,
|
||||
tint = Color.Unspecified
|
||||
)
|
||||
}
|
||||
|
||||
"+" -> LikedIcon(iconSize)
|
||||
"-" -> Text(text = "\uD83D\uDC4E", fontSize = iconFontSize)
|
||||
else -> Text(text = reactionType, fontSize = iconFontSize)
|
||||
}
|
||||
@@ -1137,19 +1094,9 @@ private fun ZapIcon(
|
||||
|
||||
Crossfade(targetState = wasZappedByLoggedInUser) {
|
||||
if (it.value) {
|
||||
Icon(
|
||||
imageVector = Icons.Default.Bolt,
|
||||
contentDescription = stringResource(R.string.zaps),
|
||||
modifier = remember { Modifier.size(iconSize) },
|
||||
tint = BitcoinOrange
|
||||
)
|
||||
ZappedIcon(iconSize)
|
||||
} else {
|
||||
Icon(
|
||||
imageVector = Icons.Outlined.Bolt,
|
||||
contentDescription = stringResource(id = R.string.zaps),
|
||||
modifier = remember { Modifier.size(iconSize) },
|
||||
tint = grayTint
|
||||
)
|
||||
ZapIcon(iconSize, grayTint)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1205,27 +1152,10 @@ fun ViewCountReaction(
|
||||
numberSize: Dp = Size24dp,
|
||||
viewCountColorFilter: ColorFilter
|
||||
) {
|
||||
DrawViewCountIcon(barChartSize, grayTint)
|
||||
ViewCountIcon(barChartSize, grayTint)
|
||||
DrawViewCount(note, numberSize, viewCountColorFilter)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun DrawViewCountIcon(
|
||||
barChartSize: Dp = Size19dp,
|
||||
grayTint: Color
|
||||
) {
|
||||
val iconButtonModifier = remember {
|
||||
Modifier.size(barChartSize)
|
||||
}
|
||||
|
||||
Icon(
|
||||
imageVector = Icons.Outlined.BarChart,
|
||||
null,
|
||||
modifier = iconButtonModifier,
|
||||
tint = grayTint
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun DrawViewCount(
|
||||
note: Note,
|
||||
|
||||
@@ -13,13 +13,13 @@ import androidx.compose.foundation.shape.CircleShape
|
||||
import androidx.compose.material.Divider
|
||||
import androidx.compose.material.DropdownMenu
|
||||
import androidx.compose.material.DropdownMenuItem
|
||||
import androidx.compose.material.Icon
|
||||
import androidx.compose.material.MaterialTheme
|
||||
import androidx.compose.material.Text
|
||||
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
|
||||
@@ -30,11 +30,9 @@ 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.layout.ContentScale
|
||||
import androidx.compose.ui.platform.LocalClipboardManager
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.semantics.Role
|
||||
import androidx.compose.ui.text.AnnotatedString
|
||||
@@ -263,7 +261,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 +273,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(
|
||||
@@ -285,20 +283,6 @@ fun WatchFollows(userHex: String, accountViewModel: AccountViewModel, onFollowCh
|
||||
onFollowChanges(showFollowingMark)
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun FollowingIcon(iconSize: Dp) {
|
||||
val modifier = remember {
|
||||
Modifier.size(iconSize)
|
||||
}
|
||||
|
||||
Icon(
|
||||
painter = painterResource(R.drawable.verified_follow_shield),
|
||||
contentDescription = stringResource(id = R.string.following),
|
||||
modifier = modifier,
|
||||
tint = Color.Unspecified
|
||||
)
|
||||
}
|
||||
|
||||
@Immutable
|
||||
data class DropDownParams(
|
||||
val isFollowingAuthor: Boolean,
|
||||
@@ -310,7 +294,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 +310,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
|
||||
|
||||
@@ -5,12 +5,9 @@ import android.util.Log
|
||||
import androidx.compose.animation.Crossfade
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.material.Icon
|
||||
import androidx.compose.material.IconButton
|
||||
import androidx.compose.material.MaterialTheme
|
||||
import androidx.compose.material.Text
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.outlined.PlayCircle
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.derivedStateOf
|
||||
import androidx.compose.runtime.getValue
|
||||
@@ -163,16 +160,8 @@ fun DrawPlayName(name: String) {
|
||||
|
||||
@Composable
|
||||
fun DrawPlayNameIcon(onClick: () -> Unit) {
|
||||
IconButton(
|
||||
onClick = onClick,
|
||||
modifier = StdButtonSizeModifier
|
||||
) {
|
||||
Icon(
|
||||
imageVector = Icons.Outlined.PlayCircle,
|
||||
contentDescription = null,
|
||||
modifier = StdButtonSizeModifier,
|
||||
tint = MaterialTheme.colors.placeholderText
|
||||
)
|
||||
IconButton(onClick = onClick, modifier = StdButtonSizeModifier) {
|
||||
PlayIcon(modifier = StdButtonSizeModifier, tint = MaterialTheme.colors.placeholderText)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -7,11 +7,7 @@ import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.layout.width
|
||||
import androidx.compose.material.Icon
|
||||
import androidx.compose.material.MaterialTheme
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.Bolt
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.derivedStateOf
|
||||
@@ -23,12 +19,11 @@ import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.compositeOver
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.vitorpamplona.amethyst.R
|
||||
import com.vitorpamplona.amethyst.ui.screen.ZapUserSetCard
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
||||
import com.vitorpamplona.amethyst.ui.theme.BitcoinOrange
|
||||
import com.vitorpamplona.amethyst.ui.theme.Size25dp
|
||||
import com.vitorpamplona.amethyst.ui.theme.Size55Modifier
|
||||
import com.vitorpamplona.amethyst.ui.theme.newItemBackgroundColor
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
@@ -75,17 +70,14 @@ fun ZapUserSetCompose(zapSetCard: ZapUserSetCard, isInnerNote: Boolean = false,
|
||||
// Draws the like picture outside the boosted card.
|
||||
if (!isInnerNote) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.width(55.dp)
|
||||
.padding(0.dp)
|
||||
modifier = Size55Modifier
|
||||
) {
|
||||
Icon(
|
||||
imageVector = Icons.Default.Bolt,
|
||||
contentDescription = stringResource(id = R.string.zaps),
|
||||
tint = BitcoinOrange,
|
||||
modifier = Modifier
|
||||
.size(25.dp)
|
||||
.align(Alignment.TopEnd)
|
||||
ZappedIcon(
|
||||
remember {
|
||||
Modifier
|
||||
.size(Size25dp)
|
||||
.align(Alignment.TopEnd)
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -90,7 +90,7 @@ sealed class CardFeedState {
|
||||
object Loading : CardFeedState()
|
||||
|
||||
@Stable
|
||||
class Loaded(val feed: MutableState<ImmutableList<Card>>) : CardFeedState()
|
||||
class Loaded(val feed: MutableState<ImmutableList<Card>>, val showHidden: MutableState<Boolean>) : CardFeedState()
|
||||
|
||||
@Immutable
|
||||
object Empty : CardFeedState()
|
||||
|
||||
@@ -141,9 +141,9 @@ fun RenderCardFeed(
|
||||
FeedLoaded(
|
||||
state = state,
|
||||
listState = listState,
|
||||
routeForLastRead = routeForLastRead,
|
||||
accountViewModel = accountViewModel,
|
||||
nav = nav,
|
||||
routeForLastRead = routeForLastRead
|
||||
nav = nav
|
||||
)
|
||||
}
|
||||
CardFeedState.Loading -> {
|
||||
@@ -158,9 +158,9 @@ fun RenderCardFeed(
|
||||
private fun FeedLoaded(
|
||||
state: CardFeedState.Loaded,
|
||||
listState: LazyListState,
|
||||
routeForLastRead: String,
|
||||
accountViewModel: AccountViewModel,
|
||||
nav: (String) -> Unit,
|
||||
routeForLastRead: String
|
||||
nav: (String) -> Unit
|
||||
) {
|
||||
LazyColumn(
|
||||
modifier = remember { Modifier.fillMaxSize() },
|
||||
@@ -179,7 +179,7 @@ private fun FeedLoaded(
|
||||
}
|
||||
|
||||
Row(defaultModifier) {
|
||||
RenderCardItem(item, accountViewModel, nav, routeForLastRead)
|
||||
RenderCardItem(item, routeForLastRead, showHidden = state.showHidden.value, accountViewModel, nav)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -188,15 +188,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 +214,7 @@ private fun RenderCardItem(
|
||||
is MultiSetCard -> MultiSetCompose(
|
||||
item,
|
||||
accountViewModel = accountViewModel,
|
||||
showHidden = showHidden,
|
||||
nav = nav,
|
||||
routeForLastRead = routeForLastRead
|
||||
)
|
||||
@@ -219,6 +222,7 @@ private fun RenderCardItem(
|
||||
is BadgeCard -> BadgeCompose(
|
||||
item,
|
||||
accountViewModel = accountViewModel,
|
||||
showHidden = showHidden,
|
||||
nav = nav,
|
||||
routeForLastRead = routeForLastRead
|
||||
)
|
||||
@@ -226,6 +230,7 @@ private fun RenderCardItem(
|
||||
is MessageSetCard -> MessageSetCompose(
|
||||
messageSetCard = item,
|
||||
routeForLastRead = routeForLastRead,
|
||||
showHidden = showHidden,
|
||||
accountViewModel = accountViewModel,
|
||||
nav = nav
|
||||
)
|
||||
@@ -242,6 +247,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 +265,7 @@ fun NoteCompose(
|
||||
unPackReply = unPackReply,
|
||||
makeItShort = makeItShort,
|
||||
addMarginTop = addMarginTop,
|
||||
showHidden = showHidden,
|
||||
parentBackgroundColor = parentBackgroundColor,
|
||||
accountViewModel = accountViewModel,
|
||||
nav = nav
|
||||
|
||||
@@ -223,9 +223,10 @@ open class CardFeedViewModel(val localFilter: FeedFilter<Note>) : ViewModel() {
|
||||
if (notes.isEmpty()) {
|
||||
_feedContent.update { CardFeedState.Empty }
|
||||
} else if (currentState is CardFeedState.Loaded) {
|
||||
currentState.showHidden.value = localFilter.showHiddenKey()
|
||||
currentState.feed.value = notes
|
||||
} else {
|
||||
_feedContent.update { CardFeedState.Loaded(mutableStateOf(notes)) }
|
||||
_feedContent.update { CardFeedState.Loaded(mutableStateOf(notes), mutableStateOf(localFilter.showHiddenKey())) }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,7 +6,7 @@ import kotlinx.collections.immutable.ImmutableList
|
||||
|
||||
sealed class FeedState {
|
||||
object Loading : FeedState()
|
||||
class Loaded(val feed: MutableState<ImmutableList<Note>>) : FeedState()
|
||||
class Loaded(val feed: MutableState<ImmutableList<Note>>, val showHidden: MutableState<Boolean>) : FeedState()
|
||||
object Empty : FeedState()
|
||||
class FeedError(val errorMessage: String) : FeedState()
|
||||
}
|
||||
|
||||
@@ -148,11 +148,11 @@ private fun RenderFeed(
|
||||
|
||||
is FeedState.Loaded -> {
|
||||
FeedLoaded(
|
||||
state,
|
||||
listState,
|
||||
routeForLastRead,
|
||||
accountViewModel,
|
||||
nav
|
||||
state = state,
|
||||
listState = listState,
|
||||
routeForLastRead = routeForLastRead,
|
||||
accountViewModel = accountViewModel,
|
||||
nav = nav
|
||||
)
|
||||
}
|
||||
|
||||
@@ -210,6 +210,7 @@ private fun FeedLoaded(
|
||||
routeForLastRead = routeForLastRead,
|
||||
modifier = baseModifier,
|
||||
isBoostedNote = false,
|
||||
showHidden = state.showHidden.value,
|
||||
accountViewModel = accountViewModel,
|
||||
nav = nav
|
||||
)
|
||||
|
||||
@@ -248,9 +248,10 @@ abstract class FeedViewModel(val localFilter: FeedFilter<Note>) : ViewModel(), I
|
||||
_feedContent.update { FeedState.Empty }
|
||||
} else if (currentState is FeedState.Loaded) {
|
||||
// updates the current list
|
||||
currentState.showHidden.value = localFilter.showHiddenKey()
|
||||
currentState.feed.value = notes
|
||||
} else {
|
||||
_feedContent.update { FeedState.Loaded(mutableStateOf(notes)) }
|
||||
_feedContent.update { FeedState.Loaded(mutableStateOf(notes), mutableStateOf(localFilter.showHiddenKey())) }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -15,7 +15,6 @@ import androidx.compose.foundation.shape.CircleShape
|
||||
import androidx.compose.foundation.text.ClickableText
|
||||
import androidx.compose.material.*
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.Bolt
|
||||
import androidx.compose.material.icons.filled.ContentCopy
|
||||
import androidx.compose.material.icons.filled.EditNote
|
||||
import androidx.compose.material.icons.filled.Link
|
||||
@@ -49,6 +48,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 +58,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 +80,8 @@ 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.LightningAddressIcon
|
||||
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
|
||||
@@ -93,10 +97,11 @@ import com.vitorpamplona.amethyst.ui.screen.RefreshingFeedUserFeedView
|
||||
import com.vitorpamplona.amethyst.ui.screen.RelayFeedView
|
||||
import com.vitorpamplona.amethyst.ui.screen.RelayFeedViewModel
|
||||
import com.vitorpamplona.amethyst.ui.screen.UserFeedViewModel
|
||||
import com.vitorpamplona.amethyst.ui.theme.BitcoinOrange
|
||||
import com.vitorpamplona.amethyst.ui.theme.ButtonBorder
|
||||
import com.vitorpamplona.amethyst.ui.theme.Size16Modifier
|
||||
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,
|
||||
@@ -967,12 +984,7 @@ fun DisplayLNAddress(
|
||||
|
||||
if (!lud16.isNullOrEmpty()) {
|
||||
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||
Icon(
|
||||
tint = BitcoinOrange,
|
||||
imageVector = Icons.Default.Bolt,
|
||||
contentDescription = stringResource(R.string.lightning_address),
|
||||
modifier = Modifier.size(16.dp)
|
||||
)
|
||||
LightningAddressIcon(modifier = Size16Modifier)
|
||||
|
||||
ClickableText(
|
||||
text = AnnotatedString(lud16),
|
||||
@@ -1110,31 +1122,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 +1645,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()
|
||||
|
||||
|
||||
@@ -10,7 +10,6 @@ import androidx.compose.foundation.layout.fillMaxHeight
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.LazyListState
|
||||
import androidx.compose.foundation.lazy.itemsIndexed
|
||||
@@ -18,14 +17,11 @@ import androidx.compose.foundation.lazy.rememberLazyListState
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.foundation.text.KeyboardOptions
|
||||
import androidx.compose.material.Divider
|
||||
import androidx.compose.material.Icon
|
||||
import androidx.compose.material.IconButton
|
||||
import androidx.compose.material.MaterialTheme
|
||||
import androidx.compose.material.Text
|
||||
import androidx.compose.material.TextField
|
||||
import androidx.compose.material.TextFieldDefaults
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.Clear
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.DisposableEffect
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
@@ -41,7 +37,6 @@ import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.platform.LocalLifecycleOwner
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.text.input.KeyboardCapitalization
|
||||
@@ -62,10 +57,13 @@ import com.vitorpamplona.amethyst.service.checkNotInMainThread
|
||||
import com.vitorpamplona.amethyst.ui.components.BundledUpdate
|
||||
import com.vitorpamplona.amethyst.ui.note.AboutDisplay
|
||||
import com.vitorpamplona.amethyst.ui.note.ChannelName
|
||||
import com.vitorpamplona.amethyst.ui.note.ClearTextIcon
|
||||
import com.vitorpamplona.amethyst.ui.note.ClickableUserPicture
|
||||
import com.vitorpamplona.amethyst.ui.note.NoteCompose
|
||||
import com.vitorpamplona.amethyst.ui.note.SearchIcon
|
||||
import com.vitorpamplona.amethyst.ui.note.UserCompose
|
||||
import com.vitorpamplona.amethyst.ui.note.UsernameDisplay
|
||||
import com.vitorpamplona.amethyst.ui.theme.Size20Modifier
|
||||
import com.vitorpamplona.amethyst.ui.theme.placeholderText
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.FlowPreview
|
||||
@@ -288,12 +286,7 @@ private fun SearchTextField(
|
||||
capitalization = KeyboardCapitalization.Sentences
|
||||
),
|
||||
leadingIcon = {
|
||||
Icon(
|
||||
painter = painterResource(R.drawable.ic_search),
|
||||
contentDescription = null,
|
||||
modifier = Modifier.size(20.dp),
|
||||
tint = Color.Unspecified
|
||||
)
|
||||
SearchIcon(modifier = Size20Modifier, Color.Unspecified)
|
||||
},
|
||||
modifier = Modifier
|
||||
.weight(1f, true)
|
||||
@@ -312,10 +305,7 @@ private fun SearchTextField(
|
||||
NostrSearchEventOrUserDataSource.clear()
|
||||
}
|
||||
) {
|
||||
Icon(
|
||||
imageVector = Icons.Default.Clear,
|
||||
contentDescription = stringResource(R.string.clear)
|
||||
)
|
||||
ClearTextIcon()
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -381,7 +371,7 @@ private fun DisplaySearchResults(
|
||||
channelPicture = item.profilePicture(),
|
||||
channelTitle = {
|
||||
Text(
|
||||
"${item.toBestDisplayName()}",
|
||||
item.toBestDisplayName(),
|
||||
fontWeight = FontWeight.Bold
|
||||
)
|
||||
},
|
||||
|
||||
@@ -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
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,7 +12,7 @@ val BitcoinOrange = Color(0xFFF7931A)
|
||||
val RoyalBlue = Color(0xFF4169E1)
|
||||
|
||||
val Following = Color(0xFF03DAC5)
|
||||
val Nip05 = Color(0xFF01BAFF)
|
||||
val Nip05EmailColor = Color(0xFFb198ec)
|
||||
val FollowsFollow = Color.Yellow
|
||||
val NIP05Verified = Color.Blue
|
||||
|
||||
|
||||
@@ -72,12 +72,15 @@ val StdPadding = Modifier.padding(10.dp)
|
||||
val Size6Modifier = Modifier.size(6.dp)
|
||||
val Size10Modifier = Modifier.size(10.dp)
|
||||
val Size15Modifier = Modifier.size(15.dp)
|
||||
val Size16Modifier = Modifier.size(16.dp)
|
||||
val Size18Modifier = Modifier.size(18.dp)
|
||||
val Size20Modifier = Modifier.size(20.dp)
|
||||
val Size22Modifier = Modifier.size(22.dp)
|
||||
val Size24Modifier = Modifier.size(24.dp)
|
||||
val Size26Modifier = Modifier.size(26.dp)
|
||||
val Size30Modifier = Modifier.size(30.dp)
|
||||
val Size35Modifier = Modifier.size(35.dp)
|
||||
val Size50Modifier = Modifier.size(50.dp)
|
||||
val Size55Modifier = Modifier.size(55.dp)
|
||||
|
||||
val TinyBorders = Modifier.padding(2.dp)
|
||||
@@ -112,3 +115,6 @@ val NotificationIconModifierSmaller = Modifier.width(55.dp).padding(end = 4.dp)
|
||||
|
||||
val ZapPictureCommentModifier = Modifier.height(35.dp).widthIn(min = 35.dp)
|
||||
val ChatHeadlineBorders = Modifier.padding(start = 12.dp, end = 12.dp, top = 10.dp)
|
||||
|
||||
val VolumeBottomIconSize = Modifier.size(70.dp).padding(10.dp)
|
||||
val NIP05IconSize = Modifier.size(14.dp).padding(top = 1.dp, start = 1.dp, end = 1.dp)
|
||||
|
||||
@@ -89,9 +89,6 @@ private val LightReplyItemBackground = LightColorPalette.onSurface.copy(alpha =
|
||||
private val DarkZapraiserBackground = BitcoinOrange.copy(0.52f).compositeOver(DarkColorPalette.background)
|
||||
private val LightZapraiserBackground = BitcoinOrange.copy(0.52f).compositeOver(LightColorPalette.background)
|
||||
|
||||
private val DarkImageVerifier = Nip05.copy(0.52f).compositeOver(DarkColorPalette.background)
|
||||
private val LightImageVerifier = Nip05.copy(0.52f).compositeOver(LightColorPalette.background)
|
||||
|
||||
private val DarkOverPictureBackground = DarkColorPalette.background.copy(0.62f)
|
||||
private val LightOverPictureBackground = LightColorPalette.background.copy(0.62f)
|
||||
|
||||
@@ -278,9 +275,6 @@ val Colors.subtleBorder: Color
|
||||
val Colors.subtleButton: Color
|
||||
get() = if (isLight) LightSubtleButton else DarkSubtleButton
|
||||
|
||||
val Colors.hashVerified: Color
|
||||
get() = if (isLight) LightImageVerifier else DarkImageVerifier
|
||||
|
||||
val Colors.overPictureBackground: Color
|
||||
get() = if (isLight) LightOverPictureBackground else DarkOverPictureBackground
|
||||
|
||||
|
||||
21
app/src/main/res/drawable/following.xml
Normal file
21
app/src/main/res/drawable/following.xml
Normal file
@@ -0,0 +1,21 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="30dp"
|
||||
android:height="30dp"
|
||||
android:viewportWidth="30"
|
||||
android:viewportHeight="30">
|
||||
<path
|
||||
android:pathData="M27.8,12.8L27.8,12.8C27.8,5 27.2,0 24.4,0S15,0 15,0S8.4,0 5.6,0C2.8,0 2.2,5 2.2,12.8v0l0,0c0,0 0,0 0,0C2.2,24.9 15,30 15,30S27.8,24.9 27.8,12.8C27.8,12.8 27.8,12.8 27.8,12.8L27.8,12.8z"
|
||||
android:fillColor="#7F2EFF"/>
|
||||
<path
|
||||
android:pathData="M15.1,24l-0.5,-0.2c-0.2,-0.1 -4.5,-2 -6.8,-6.4c-0.3,-0.6 -0.1,-1.4 0.6,-1.7c0.6,-0.3 1.4,-0.1 1.7,0.6c1.4,2.8 3.9,4.4 5,4.9c1.1,-0.6 3.6,-2.2 5,-4.9c0.3,-0.6 1.1,-0.9 1.7,-0.6c0.6,0.3 0.9,1.1 0.6,1.7c-2.2,4.4 -6.6,6.3 -6.8,6.4L15.1,24z"
|
||||
android:fillColor="#FFFFFF"/>
|
||||
<path
|
||||
android:pathData="M15.1,24.3l-0.6,-0.3c-0.2,-0.1 -4.6,-2 -6.9,-6.5c-0.2,-0.4 -0.2,-0.8 -0.1,-1.2c0.1,-0.4 0.4,-0.7 0.8,-0.9c0.4,-0.2 0.8,-0.2 1.2,-0.1c0.4,0.1 0.7,0.4 0.9,0.8c1.3,2.6 3.7,4.2 4.8,4.8c1.1,-0.6 3.5,-2.2 4.8,-4.8c0.2,-0.4 0.5,-0.6 0.9,-0.8c0.4,-0.1 0.8,-0.1 1.2,0.1c0.4,0.2 0.6,0.5 0.8,0.9c0.1,0.4 0.1,0.8 -0.1,1.2c-2.3,4.5 -6.7,6.4 -6.9,6.5L15.1,24.3zM8.9,15.8c-0.2,0 -0.3,0 -0.5,0.1C8.2,16 8,16.3 7.9,16.5c-0.1,0.3 -0.1,0.5 0.1,0.8c2.2,4.3 6.5,6.2 6.7,6.3l0.4,0.2l0.4,-0.2c0.2,-0.1 4.5,-2 6.7,-6.3c0.1,-0.2 0.1,-0.5 0.1,-0.8c-0.1,-0.3 -0.3,-0.5 -0.5,-0.6c-0.2,-0.1 -0.5,-0.1 -0.8,-0.1c-0.3,0.1 -0.5,0.3 -0.6,0.5c-1.4,2.8 -4,4.5 -5.1,5.1l-0.1,0.1l-0.1,-0.1c-1,-0.6 -3.6,-2.2 -5.1,-5.1c-0.1,-0.2 -0.3,-0.4 -0.6,-0.5C9.1,15.8 9,15.8 8.9,15.8z"
|
||||
android:fillColor="#7F2EFF"/>
|
||||
<path
|
||||
android:pathData="M15,15c-2.9,0 -5.2,-2.3 -5.2,-5.2s2.3,-5.2 5.2,-5.2s5.2,2.3 5.2,5.2S17.8,15 15,15zM15,7.2c-1.4,0 -2.6,1.2 -2.6,2.6s1.2,2.6 2.6,2.6s2.6,-1.2 2.6,-2.6S16.4,7.2 15,7.2z"
|
||||
android:fillColor="#FFFFFF"/>
|
||||
<path
|
||||
android:pathData="M15,15.2c-3,0 -5.4,-2.4 -5.4,-5.4S12,4.4 15,4.4s5.4,2.4 5.4,5.4S18,15.2 15,15.2zM15,4.9c-2.7,0 -4.9,2.2 -4.9,4.9s2.2,4.9 4.9,4.9c2.7,0 4.9,-2.2 4.9,-4.9S17.7,4.9 15,4.9zM15,12.6c-1.6,0 -2.9,-1.3 -2.9,-2.9s1.3,-2.9 2.9,-2.9c1.6,0 2.9,1.3 2.9,2.9S16.5,12.6 15,12.6zM15,7.4c-1.3,0 -2.4,1.1 -2.4,2.4s1.1,2.4 2.4,2.4c1.3,0 2.4,-1.1 2.4,-2.4S16.3,7.4 15,7.4z"
|
||||
android:fillColor="#7F2EFF"/>
|
||||
</vector>
|
||||
23
app/src/main/res/drawable/nip_05.xml
Normal file
23
app/src/main/res/drawable/nip_05.xml
Normal file
@@ -0,0 +1,23 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="30dp"
|
||||
android:height="30dp"
|
||||
android:viewportWidth="30"
|
||||
android:viewportHeight="30">
|
||||
<path
|
||||
android:pathData="M15,15m-15,0a15,15 0,1 1,30 0a15,15 0,1 1,-30 0"
|
||||
android:fillColor="#7F2EFF"/>
|
||||
<path
|
||||
android:pathData="M15.3,19.5l-4.1,-3.7c-0.5,-0.5 -0.6,-1.3 -0.1,-1.8s1.3,-0.6 1.8,-0.1l2.1,1.9l3.4,-3.8c0.5,-0.5 1.3,-0.6 1.8,-0.1s0.6,1.3 0.1,1.8L15.3,19.5z"
|
||||
android:fillColor="#FFFFFF"/>
|
||||
<path
|
||||
android:pathData="M15.4,19.9L11.1,16c-0.6,-0.6 -0.7,-1.5 -0.1,-2.2c0.3,-0.3 0.7,-0.5 1.1,-0.5c0.4,0 0.8,0.1 1.1,0.4l2,1.8l3.3,-3.7c0.6,-0.6 1.5,-0.7 2.2,-0.1c0.3,0.3 0.5,0.6 0.5,1.1c0,0.4 -0.1,0.8 -0.4,1.1L15.4,19.9zM12.1,13.9L12.1,13.9c-0.3,0 -0.6,0.1 -0.8,0.3c-0.4,0.4 -0.3,1.1 0.1,1.5l3.9,3.5l5,-5.6c0.2,-0.2 0.3,-0.5 0.3,-0.7c0,-0.3 -0.1,-0.5 -0.3,-0.7c-0.4,-0.4 -1.1,-0.3 -1.5,0.1l-3.6,4l-2.4,-2.2C12.6,14 12.4,13.9 12.1,13.9z"
|
||||
android:fillColor="#7F2EFF"/>
|
||||
<path
|
||||
android:pathData="M15,4C9,4 4,8.9 4,15s4.9,11 11,11c0.7,0 1.3,-0.6 1.3,-1.3c0,-0.7 -0.6,-1.3 -1.3,-1.3c-4.6,0 -8.4,-3.8 -8.4,-8.4s3.8,-8.4 8.4,-8.4s8.4,3.8 8.4,8.4c0,2 -1.1,3.9 -2.5,4.5c-0.9,0.4 -1.8,0.1 -2.7,-0.7l0,0l-1.7,1.9c1.6,1.5 3.5,2 5.4,1.2c2.4,-1 4.1,-3.8 4.1,-6.9C25.9,8.9 21,4 15,4z"
|
||||
android:fillColor="#FFFFFF"/>
|
||||
<path
|
||||
android:pathData="M15,26.2c-6.2,0 -11.2,-5 -11.2,-11.2S8.8,3.8 15,3.8s11.2,5 11.2,11.2c0,3.1 -1.7,6.1 -4.2,7.1c-1.9,0.8 -4,0.3 -5.6,-1.3l-0.2,-0.2l2,-2.3l0.2,0.2l0,0c0.8,0.8 1.6,1 2.4,0.7c1.3,-0.6 2.3,-2.4 2.3,-4.2c0,-4.5 -3.7,-8.1 -8.1,-8.1c-4.5,-0.1 -8.2,3.6 -8.2,8.1s3.7,8.1 8.1,8.1c0.8,0 1.5,0.7 1.5,1.5C16.5,25.5 15.8,26.2 15,26.2zM15,4.3C9.1,4.3 4.3,9.1 4.3,15S9.1,25.7 15,25.7c0.6,0 1,-0.5 1,-1c0,-0.6 -0.5,-1 -1,-1c-4.8,0 -8.6,-3.9 -8.6,-8.6c0,-4.8 3.9,-8.6 8.6,-8.6s8.6,3.9 8.6,8.6c0,2.1 -1.1,4.1 -2.6,4.7c-0.9,0.4 -1.9,0.2 -2.8,-0.6l-1.4,1.4c1.5,1.3 3.3,1.7 4.9,1c2.3,-1 3.9,-3.7 3.9,-6.6C25.7,9.1 20.9,4.3 15,4.3z"
|
||||
android:fillColor="#7F2EFF"/>
|
||||
</vector>
|
||||
|
||||
|
||||
12
app/src/main/res/drawable/nip_05_light.xml
Normal file
12
app/src/main/res/drawable/nip_05_light.xml
Normal file
@@ -0,0 +1,12 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="30dp"
|
||||
android:height="30dp"
|
||||
android:viewportWidth="30"
|
||||
android:viewportHeight="30">
|
||||
<path
|
||||
android:pathData="m15.408,21.113 l-5.57,-5.026c-0.679,-0.679 -0.815,-1.766 -0.136,-2.445 0.679,-0.679 1.766,-0.815 2.445,-0.136L15,16.087 19.619,10.924c0.679,-0.679 1.766,-0.815 2.445,-0.136 0.679,0.679 0.815,1.766 0.136,2.445z"
|
||||
android:fillColor="#b198ec"/>
|
||||
<path
|
||||
android:pathData="M15,0.056C6.849,0.056 0.056,6.713 0.056,15c0,8.287 6.657,14.944 14.944,14.944 0.951,0 1.766,-0.815 1.766,-1.766 0,-0.951 -0.815,-1.766 -1.766,-1.766 -6.249,0 -11.411,-5.162 -11.411,-11.411 0,-6.249 5.162,-11.411 11.411,-11.411 6.249,0 11.411,5.162 11.411,11.411 0,2.717 -1.494,5.298 -3.396,6.113 -1.223,0.543 -2.445,0.136 -3.668,-0.951l0,0l-2.309,2.581c2.174,2.038 4.755,2.717 7.336,1.63C27.634,23.015 29.944,19.211 29.944,15 29.808,6.713 23.151,0.056 15,0.056Z"
|
||||
android:fillColor="#b198ec"/>
|
||||
</vector>
|
||||
12
app/src/main/res/drawable/original.xml
Normal file
12
app/src/main/res/drawable/original.xml
Normal file
@@ -0,0 +1,12 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="30dp"
|
||||
android:height="30dp"
|
||||
android:viewportWidth="30"
|
||||
android:viewportHeight="30">
|
||||
<path
|
||||
android:pathData="M13.4,0.3c0.9,0.5 2.3,0.5 3.1,0c0.9,-0.5 2,-0.3 2.6,0.5s1.9,1.3 2.9,1.2c1,-0.1 2,0.5 2.2,1.5c0.3,1 1.2,1.9 2.2,2.2s1.6,1.3 1.5,2.2c-0.1,1 0.4,2.3 1.2,2.9c0.8,0.6 1,1.8 0.5,2.6c-0.5,0.9 -0.5,2.3 0,3.1c0.5,0.9 0.3,2 -0.5,2.6c-0.8,0.6 -1.3,1.9 -1.2,2.9c0.1,1 -0.5,2 -1.5,2.2c-1,0.3 -1.9,1.2 -2.2,2.2c-0.3,1 -1.3,1.6 -2.2,1.5c-1,-0.1 -2.3,0.4 -2.9,1.2c-0.6,0.8 -1.8,1 -2.6,0.5c-0.9,-0.5 -2.3,-0.5 -3.1,0c-0.9,0.5 -2,0.3 -2.6,-0.5c-0.6,-0.8 -1.9,-1.3 -2.9,-1.2c-1,0.1 -2,-0.5 -2.2,-1.5s-1.2,-1.9 -2.2,-2.2c-1,-0.3 -1.6,-1.3 -1.5,-2.2c0.1,-1 -0.4,-2.3 -1.2,-2.9s-1,-1.8 -0.5,-2.6c0.5,-0.9 0.5,-2.3 0,-3.1c-0.5,-0.9 -0.3,-2 0.5,-2.6S2.1,8.9 2,7.9s0.5,-2 1.5,-2.2s1.9,-1.2 2.2,-2.2S6.9,1.8 7.9,2s2.3,-0.4 2.9,-1.2S12.6,-0.2 13.4,0.3z"
|
||||
android:fillColor="#7F2EFF"/>
|
||||
<path
|
||||
android:pathData="M0.3,24.5c-0.4,0.4 -0.3,0.8 0.3,0.9l2.5,0.4c0.5,0.1 1.1,0.6 1.1,1.1l0.4,2.5c0.1,0.5 0.5,0.7 0.9,0.3L29.7,5.5c0.4,-0.4 0.3,-0.8 -0.3,-0.9l-2.5,-0.4c-0.5,-0.1 -1.1,-0.6 -1.1,-1.1l-0.4,-2.5c-0.1,-0.5 -0.5,-0.7 -0.9,-0.3L0.3,24.5z"
|
||||
android:fillColor="#BA8AF9"/>
|
||||
</vector>
|
||||
@@ -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>
|
||||
@@ -299,8 +299,8 @@
|
||||
<string name="poll_author_no_vote">Poll authors can\'t vote in their own polls.</string>
|
||||
<string name="poll_hashtag" translatable="false">#zappoll</string>
|
||||
|
||||
<string name="hash_verification_passed">Image is the same since the post</string>
|
||||
<string name="hash_verification_failed">Image has changed. The author might not have seen the change</string>
|
||||
<string name="hash_verification_passed">This content is the same since the post</string>
|
||||
<string name="hash_verification_failed">This content has changed. The author might not have seen or approved the change</string>
|
||||
|
||||
<string name="content_description_add_image">Add Image</string>
|
||||
<string name="content_description_add_video">Add Video</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,14 @@
|
||||
<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>
|
||||
|
||||
<string name="muted_button">Muted. Click to unmute</string>
|
||||
<string name="mute_button">Sound on. Click to mute</string>
|
||||
<string name="search_button">Search local and remote records</string>
|
||||
|
||||
<string name="nip05_verified">Nostr address was verified</string>
|
||||
<string name="nip05_failed">Nostr address failed verification</string>
|
||||
<string name="nip05_checking">Checking Nostr address</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'
|
||||
|
||||
28
docs/design/VerificationIcons/Address-Book-Icon-Amethyst.svg
Normal file
28
docs/design/VerificationIcons/Address-Book-Icon-Amethyst.svg
Normal file
@@ -0,0 +1,28 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- Generator: Adobe Illustrator 24.0.2, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
|
||||
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
|
||||
viewBox="0 0 30 30" style="enable-background:new 0 0 30 30;" xml:space="preserve">
|
||||
<style type="text/css">
|
||||
.st0{fill:#FFFFFF;}
|
||||
.st1{fill:#7F2EFF;}
|
||||
</style>
|
||||
<g>
|
||||
<g>
|
||||
<path class="st0" d="M25.7,1.3H5.6c-0.6,0-1,0.5-1,1s0.5,1,1,1l21.1,2.3V2.1C26.6,1.5,26.3,1.3,25.7,1.3z"/>
|
||||
</g>
|
||||
<g>
|
||||
<path class="st1" d="M24.7,0H5.3c-1.1,0-2,0.9-2,2v26c0,1.1,0.9,2,2,2h19.4c1.1,0,2-0.9,2-2V4.4c0-0.6-0.5-1-1-1H5.6
|
||||
c-0.5,0-1-0.4-1.1-0.9c0-0.7,0.5-1.2,1.1-1.2h20.1c0.5,0,0.9,0.4,1,0.8l0,0V2C26.7,0.9,25.8,0,24.7,0z"/>
|
||||
</g>
|
||||
<g>
|
||||
<g>
|
||||
<path class="st0" d="M14.9,21.8l-5.5-5c-0.5-0.5-0.6-1.3-0.1-1.8s1.3-0.6,1.8-0.1l3.6,3.2l5.4-6.1c0.5-0.5,1.3-0.6,1.8-0.1
|
||||
s0.6,1.3,0.1,1.8L14.9,21.8z"/>
|
||||
<path class="st1" d="M14.9,22.1L9.2,17c-0.3-0.3-0.5-0.7-0.5-1.1s0.1-0.8,0.4-1.1c0.3-0.3,0.7-0.5,1.1-0.5c0.4,0,0.8,0.1,1.1,0.4
|
||||
l3.4,3.1l5.3-5.9c0.3-0.3,0.7-0.5,1.1-0.5s0.8,0.1,1.1,0.4c0.3,0.3,0.5,0.7,0.5,1.1c0,0.4-0.1,0.8-0.4,1.1L14.9,22.1z M10.2,14.8
|
||||
L10.2,14.8c-0.3,0-0.6,0.1-0.8,0.3s-0.3,0.5-0.3,0.8s0.1,0.5,0.3,0.7l5.3,4.8l7-7.9c0.2-0.2,0.3-0.5,0.3-0.8s-0.1-0.5-0.3-0.7
|
||||
c-0.2-0.2-0.5-0.3-0.8-0.3s-0.5,0.1-0.7,0.3l-5.6,6.3l-3.8-3.4C10.7,14.9,10.5,14.8,10.2,14.8z"/>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.4 KiB |
3704
docs/design/VerificationIcons/Amethyst-Proposal.pdf
Normal file
3704
docs/design/VerificationIcons/Amethyst-Proposal.pdf
Normal file
File diff suppressed because one or more lines are too long
70
docs/design/VerificationIcons/App-Launch-Screen.svg
Normal file
70
docs/design/VerificationIcons/App-Launch-Screen.svg
Normal file
@@ -0,0 +1,70 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- Generator: Adobe Illustrator 24.0.2, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
|
||||
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
|
||||
viewBox="0 0 1125 2436" style="enable-background:new 0 0 1125 2436;" xml:space="preserve">
|
||||
<style type="text/css">
|
||||
.st0{opacity:0.16;fill:url(#SVGID_1_);}
|
||||
.st1{opacity:0.16;fill:url(#SVGID_2_);}
|
||||
.st2{opacity:0;fill:none;stroke:#D9CAFF;stroke-miterlimit:10;}
|
||||
.st3{fill:#D9CAFF;}
|
||||
.st4{fill:url(#SVGID_3_);}
|
||||
</style>
|
||||
<rect x="1" width="1121.8" height="2436"/>
|
||||
<radialGradient id="SVGID_1_" cx="1932.6792" cy="2840.8818" r="3111.124" gradientTransform="matrix(1.0398 0 0 2.2579 -22.5496 -2166.8965)" gradientUnits="userSpaceOnUse">
|
||||
<stop offset="0" style="stop-color:#00FFFF"/>
|
||||
<stop offset="1" style="stop-color:#FF00FF;stop-opacity:0.93"/>
|
||||
</radialGradient>
|
||||
<rect x="3.2" class="st0" width="1121.8" height="2436"/>
|
||||
<rect x="1" width="1121.8" height="2436"/>
|
||||
<radialGradient id="SVGID_2_" cx="1932.6792" cy="2840.8818" r="3111.124" gradientTransform="matrix(1.0398 0 0 2.2579 -22.5496 -2166.8965)" gradientUnits="userSpaceOnUse">
|
||||
<stop offset="0" style="stop-color:#558FFF"/>
|
||||
<stop offset="0.508" style="stop-color:#8337F9"/>
|
||||
<stop offset="1" style="stop-color:#AA1EF4"/>
|
||||
</radialGradient>
|
||||
<rect x="3.2" class="st1" width="1121.8" height="2436"/>
|
||||
<path class="st2" d="M426.5,832.5c-11.2,0-23.8,8.4-28,18.8l-263.6,642.6c-4.2,10.3-4.2,27.2,0,37.5l23.6,57.4
|
||||
c4.2,10.3,16.8,18.8,28,18.8h758.8c11.2,0,23.8-8.4,28-18.8l23.6-57.4c4.2-10.3,4.2-27.2,0-37.5L733.2,851.3
|
||||
c-4.2-10.3-16.8-18.8-28-18.8H426.5z"/>
|
||||
<g>
|
||||
<path class="st3" d="M261.4,1492.3l-8.9,20.1h-15.9l39-88.4h15.9l39,88.4h-15.9l-8.9-20.1H261.4z M299.6,1478.5l-16.1-36.4
|
||||
l-16.1,36.4H299.6z"/>
|
||||
<path class="st3" d="M359.9,1476v36.4h-14.2v-67.3h14.2v12.3c2-4.1,5.1-7.4,9.2-9.7c4.1-2.4,8.5-3.5,13.2-3.5
|
||||
c10.6,0,17.8,4.4,21.6,13.2c6.7-8.8,15-13.2,24.9-13.2c7.6,0,13.7,2.3,18.4,7c4.7,4.6,7,11.3,7,20.1v41.2h-14.2v-36.9
|
||||
c0-12.4-5.1-18.6-15.4-18.6c-4.8,0-9,1.5-12.6,4.6c-3.6,3.1-5.5,7.6-5.6,13.5v37.4h-14.2v-36.9c0-6.4-1.1-11.1-3.4-14.1
|
||||
c-2.3-3-5.9-4.5-10.8-4.5c-4.9,0-9.1,1.6-12.8,4.9C361.8,1465,359.9,1469.7,359.9,1476z"/>
|
||||
<path class="st3" d="M541.7,1484.9h-52.7c0.3,4.9,2.6,8.8,6.7,11.8c4.1,3,8.9,4.5,14.2,4.5c8.4,0,14.8-2.7,19.2-8l8.1,8.9
|
||||
c-7.3,7.5-16.7,11.3-28.3,11.3c-9.4,0-17.5-3.1-24.1-9.4c-6.6-6.3-9.9-14.7-9.9-25.3s3.4-19,10.1-25.2c6.7-6.2,14.7-9.4,23.8-9.4
|
||||
c9.1,0,16.9,2.8,23.3,8.3c6.4,5.5,9.6,13.1,9.6,22.8V1484.9z M488.9,1473.8h38.6c0-5.6-1.8-10-5.3-13c-3.5-3-7.9-4.6-13.1-4.6
|
||||
c-5.2,0-9.8,1.6-14,4.7C491,1464.1,488.9,1468.4,488.9,1473.8z"/>
|
||||
<path class="st3" d="M579.2,1456.5v34.1c0,3.2,0.8,5.8,2.5,7.7c1.7,1.9,4,2.8,7.1,2.8s5.9-1.5,8.7-4.4l5.8,10
|
||||
c-5,4.5-10.5,6.7-16.5,6.7c-6,0-11.2-2.1-15.4-6.3c-4.3-4.2-6.4-9.8-6.4-16.9v-33.8h-8.5v-11.4h8.5V1424h14.2v21.1H597v11.4H579.2z
|
||||
"/>
|
||||
<path class="st3" d="M633.9,1476v36.4h-14.2v-93.7h14.2v38.7c2-4.1,5.1-7.4,9.2-9.7c4.1-2.4,8.5-3.5,13.2-3.5
|
||||
c7.6,0,13.7,2.3,18.4,7c4.7,4.6,7,11.3,7,20.1v41.2h-14.2v-36.9c0-12.4-5.1-18.6-15.4-18.6c-4.9,0-9.1,1.6-12.8,4.9
|
||||
C635.7,1465,633.9,1469.7,633.9,1476z"/>
|
||||
<path class="st3" d="M714.9,1539.2c-6.2,0-11.7-2.2-16.7-6.7l6.5-11.5c3,2.8,6,4.2,9.2,4.2c3.2,0,5.7-1.1,7.7-3.4
|
||||
c2-2.3,3-4.8,3-7.5c0-1.2-9.1-24.2-27.2-69.2h15.2l20.1,49.6l20.1-49.6h15.2l-32.6,79.9c-1.9,4.4-4.7,7.8-8.3,10.4
|
||||
C723.3,1537.9,719.3,1539.2,714.9,1539.2z"/>
|
||||
<path class="st3" d="M831.3,1492.3c0,6.4-2.3,11.5-7,15.4c-4.6,3.8-10.6,5.8-17.8,5.8c-4.9,0-10-0.9-15.2-2.7
|
||||
c-5.3-1.8-9.9-4.3-14-7.5l6.8-11c7.8,5.9,15.4,8.9,22.8,8.9c3.2,0,5.7-0.7,7.6-2.2c1.9-1.5,2.8-3.3,2.8-5.6c0-3.4-4.5-6.4-13.4-9.1
|
||||
c-0.7-0.3-1.2-0.4-1.5-0.5c-14-3.8-21-10.1-21-19c0-6.2,2.4-11.2,7.3-15c4.9-3.7,11.2-5.6,18.9-5.6c7.8,0,15.1,2.3,22,6.8
|
||||
l-5.3,10.5c-5.5-3.6-11.4-5.4-17.8-5.4c-3.4,0-6.1,0.6-8.2,1.8c-2.1,1.2-3.2,3.1-3.2,5.5c0,2.2,1.1,3.8,3.3,4.8
|
||||
c1.5,0.8,3.6,1.5,6.4,2.2c2.7,0.7,5.2,1.3,7.3,2c2.1,0.7,4.2,1.4,6.1,2.3c2,0.8,4.1,2,6.4,3.5
|
||||
C829,1481.1,831.3,1485.9,831.3,1492.3z"/>
|
||||
<path class="st3" d="M869.9,1456.5v34.1c0,3.2,0.8,5.8,2.5,7.7c1.7,1.9,4,2.8,7.1,2.8s5.9-1.5,8.7-4.4l5.8,10
|
||||
c-5,4.5-10.5,6.7-16.5,6.7c-6,0-11.2-2.1-15.4-6.3c-4.3-4.2-6.4-9.8-6.4-16.9v-33.8h-8.5v-11.4h8.5V1424h14.2v21.1h17.7v11.4H869.9
|
||||
z"/>
|
||||
</g>
|
||||
<radialGradient id="SVGID_3_" cx="245.2459" cy="771.7286" r="880.4387" gradientUnits="userSpaceOnUse">
|
||||
<stop offset="0" style="stop-color:#558FFF"/>
|
||||
<stop offset="0.508" style="stop-color:#8337F9"/>
|
||||
<stop offset="1" style="stop-color:#AA1EF4"/>
|
||||
</radialGradient>
|
||||
<path class="st4" d="M714.5,1252.6c-2.9,7-11.4,12.7-19,12.7h-56.6c-7.6,0-13.8,6.2-13.8,13.8v66.7c0,7.6-2.4,8-5.2,1l-28.2-68.8
|
||||
c-2.9-7-5.2-12.7-5.2-12.7s-2.4-5.7-5.2-12.7l-28.2-68.8c-2.9-7,1-12.7,8.5-12.7h25.3c7.6,0,16.4-3.6,19.7-8.1
|
||||
c3.2-4.4,12.1-8.1,19.7-8.1h34.9c7.6,0,15.9-4.6,18.5-10.1c2.6-5.6-0.9-12.5-7.9-15.5l-32.1-13.6c-7-3-15-11.1-17.9-18.1l-13-32
|
||||
c-2.9-7-11.4-12.7-19-12.7H538c-7.6,0-16.1,5.7-19.1,12.7l-38.6,92.8c-2.9,7-2.9,18.4-0.1,25.4l28.2,68.8c2.9,7-1,12.7-8.5,12.7
|
||||
h-68.3c-7.6,0-16.1-5.7-19-12.7l-28.2-68.8c-2.9-7-2.9-18.5,0-25.5l91.3-222.5c2.9-7,11.4-12.7,19-12.7h51.5c7.6,0,20,0,27.5,0h58.7
|
||||
c7.6,0,16.1,5.7,19,12.7l91.3,222.5c2.9,7,2.9,18.5,0,25.5L714.5,1252.6z M579.2,1092.3c-4,0-9.7,5.7-12.6,12.7l-0.8,1.8
|
||||
c-2.9,7,0.9,12.7,8.5,12.7h9c7.6,0,11.5-5.8,8.8-12.8l-0.6-1.6C588.7,1098.1,583.2,1092.3,579.2,1092.3z"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 5.3 KiB |
36
docs/design/VerificationIcons/Follow-Icon-Amethyst.svg
Normal file
36
docs/design/VerificationIcons/Follow-Icon-Amethyst.svg
Normal file
@@ -0,0 +1,36 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- Generator: Adobe Illustrator 24.0.2, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
|
||||
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
|
||||
viewBox="0 0 30 30" style="enable-background:new 0 0 30 30;" xml:space="preserve">
|
||||
<style type="text/css">
|
||||
.st0{fill:#7F2EFF;}
|
||||
.st1{fill:#FFFFFF;}
|
||||
</style>
|
||||
<g>
|
||||
<path class="st0" d="M27.8,12.8L27.8,12.8C27.8,5,27.2,0,24.4,0S15,0,15,0S8.4,0,5.6,0C2.8,0,2.2,5,2.2,12.8v0l0,0c0,0,0,0,0,0
|
||||
C2.2,24.9,15,30,15,30S27.8,24.9,27.8,12.8C27.8,12.8,27.8,12.8,27.8,12.8L27.8,12.8z"/>
|
||||
</g>
|
||||
<g>
|
||||
<g>
|
||||
<path class="st1" d="M15.1,24l-0.5-0.2c-0.2-0.1-4.5-2-6.8-6.4c-0.3-0.6-0.1-1.4,0.6-1.7c0.6-0.3,1.4-0.1,1.7,0.6
|
||||
c1.4,2.8,3.9,4.4,5,4.9c1.1-0.6,3.6-2.2,5-4.9c0.3-0.6,1.1-0.9,1.7-0.6c0.6,0.3,0.9,1.1,0.6,1.7c-2.2,4.4-6.6,6.3-6.8,6.4L15.1,24
|
||||
z"/>
|
||||
<path class="st0" d="M15.1,24.3l-0.6-0.3c-0.2-0.1-4.6-2-6.9-6.5c-0.2-0.4-0.2-0.8-0.1-1.2c0.1-0.4,0.4-0.7,0.8-0.9
|
||||
c0.4-0.2,0.8-0.2,1.2-0.1c0.4,0.1,0.7,0.4,0.9,0.8c1.3,2.6,3.7,4.2,4.8,4.8c1.1-0.6,3.5-2.2,4.8-4.8c0.2-0.4,0.5-0.6,0.9-0.8
|
||||
c0.4-0.1,0.8-0.1,1.2,0.1c0.4,0.2,0.6,0.5,0.8,0.9c0.1,0.4,0.1,0.8-0.1,1.2c-2.3,4.5-6.7,6.4-6.9,6.5L15.1,24.3z M8.9,15.8
|
||||
c-0.2,0-0.3,0-0.5,0.1C8.2,16,8,16.3,7.9,16.5c-0.1,0.3-0.1,0.5,0.1,0.8c2.2,4.3,6.5,6.2,6.7,6.3l0.4,0.2l0.4-0.2
|
||||
c0.2-0.1,4.5-2,6.7-6.3c0.1-0.2,0.1-0.5,0.1-0.8c-0.1-0.3-0.3-0.5-0.5-0.6c-0.2-0.1-0.5-0.1-0.8-0.1c-0.3,0.1-0.5,0.3-0.6,0.5
|
||||
c-1.4,2.8-4,4.5-5.1,5.1l-0.1,0.1l-0.1-0.1c-1-0.6-3.6-2.2-5.1-5.1c-0.1-0.2-0.3-0.4-0.6-0.5C9.1,15.8,9,15.8,8.9,15.8z"/>
|
||||
</g>
|
||||
</g>
|
||||
<g>
|
||||
<g>
|
||||
<path class="st1" d="M15,15c-2.9,0-5.2-2.3-5.2-5.2s2.3-5.2,5.2-5.2s5.2,2.3,5.2,5.2S17.8,15,15,15z M15,7.2
|
||||
c-1.4,0-2.6,1.2-2.6,2.6s1.2,2.6,2.6,2.6s2.6-1.2,2.6-2.6S16.4,7.2,15,7.2z"/>
|
||||
<path class="st0" d="M15,15.2c-3,0-5.4-2.4-5.4-5.4S12,4.4,15,4.4s5.4,2.4,5.4,5.4S18,15.2,15,15.2z M15,4.9
|
||||
c-2.7,0-4.9,2.2-4.9,4.9s2.2,4.9,4.9,4.9c2.7,0,4.9-2.2,4.9-4.9S17.7,4.9,15,4.9z M15,12.6c-1.6,0-2.9-1.3-2.9-2.9
|
||||
s1.3-2.9,2.9-2.9c1.6,0,2.9,1.3,2.9,2.9S16.5,12.6,15,12.6z M15,7.4c-1.3,0-2.4,1.1-2.4,2.4s1.1,2.4,2.4,2.4
|
||||
c1.3,0,2.4-1.1,2.4-2.4S16.3,7.4,15,7.4z"/>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 2.2 KiB |
13
docs/design/VerificationIcons/Logo-Black-Amethyst.svg
Normal file
13
docs/design/VerificationIcons/Logo-Black-Amethyst.svg
Normal file
@@ -0,0 +1,13 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- Generator: Adobe Illustrator 24.0.2, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
|
||||
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
|
||||
viewBox="0 0 434 512" style="enable-background:new 0 0 434 512;" xml:space="preserve">
|
||||
<path d="M398.2,393.3c-3.4,8.4-13.7,15.2-22.7,15.2h-67.7c-9.1,0-16.5,7.4-16.5,16.5v79.8c0,9.1-2.8,9.6-6.3,1.2l-33.8-82.3
|
||||
c-3.4-8.4-6.3-15.2-6.3-15.2c0,0-2.8-6.9-6.3-15.2L204.9,311c-3.4-8.4,1.2-15.2,10.2-15.2h30.3c9.1,0,19.7-4.3,23.5-9.7
|
||||
c3.9-5.3,14.5-9.7,23.5-9.7h41.8c9.1,0,19-5.5,22.2-12.1c3.1-6.7-1.1-15-9.4-18.6l-38.4-16.3c-8.3-3.5-18-13.3-21.4-21.7l-15.6-38.3
|
||||
c-3.4-8.4-13.6-15.3-22.7-15.3h-62c-9.1,0-19.3,6.8-22.8,15.2l-46.2,111.1c-3.5,8.4-3.5,22.1-0.1,30.5l33.8,82.3
|
||||
c3.4,8.4-1.2,15.2-10.2,15.2H59.6c-9.1,0-19.3-6.9-22.7-15.2L3.1,311c-3.4-8.4-3.4-22.1,0-30.5L112.3,14.2
|
||||
c3.4-8.4,13.7-15.2,22.7-15.2h61.7c9.1,0,23.9,0,32.9,0h70.3c9.1,0,19.3,6.9,22.7,15.2l109.3,266.3c3.4,8.4,3.4,22.1,0,30.5
|
||||
L398.2,393.3z M236.3,201.4c-4.8,0-11.6,6.8-15,15.2l-0.9,2.2c-3.5,8.4,1.1,15.2,10.1,15.2h10.7c9.1,0,13.8-6.9,10.5-15.3l-0.7-1.9
|
||||
C247.6,208.3,241,201.4,236.3,201.4z"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.2 KiB |
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user