Compare commits

...

41 Commits

Author SHA1 Message Date
Vitor Pamplona
64bf17b1a0 v0.69.3 2023-07-17 12:16:40 -04:00
Vitor Pamplona
93b50468fd Adjusts colors for the NÏP05 to be more subtle. 2023-07-17 12:14:21 -04:00
Vitor Pamplona
65ab756c2b v0.69.2 2023-07-17 11:50:23 -04:00
Vitor Pamplona
e298285145 Migration to Niel Liesmons' icons 2023-07-17 11:48:29 -04:00
Vitor Pamplona
ad59b691fd v0.69.1 2023-07-16 20:17:53 -04:00
Vitor Pamplona
a256e34878 Fix for mute list not showing 2023-07-16 20:16:17 -04:00
Vitor Pamplona
6ad00c112a v0.69.0 2023-07-16 19:07:28 -04:00
Vitor Pamplona
0010a19c24 Fixes the profile display for new users without pictures. 2023-07-16 18:57:48 -04:00
Vitor Pamplona
d78c7a91cb Lot's of changes from local storage to relay storage.
- Moves Following Communities local DB to Contact List
- Moves Following Channels local DB to Contact List
- Moves Following BlockList local DB to Mute List (Private part)
- Migrates all deprecated local lists to event kinds.
- Views Mute Feed (disables hidden authors for that specific list)
- Breaks Security Filters screen in 2 tabs: Blocked and Spammers (automated filter)
- Restructures PeopleList event kind
- Fixes older channels and communities not loading on the discovery tab
2023-07-16 18:57:28 -04:00
Vitor Pamplona
4a2dced02f Formatting 2023-07-16 11:53:12 -04:00
Vitor Pamplona
2d1a63be57 Looks like relays accept id prefixes of 8 more frequently than 6 2023-07-16 11:50:55 -04:00
Vitor Pamplona
9e207d0f24 Adds parsing support for NNS Events. 2023-07-16 11:50:16 -04:00
Vitor Pamplona
efad94ddfd Moves contact list to closer to metadata in the Local Cache 2023-07-16 11:49:41 -04:00
Vitor Pamplona
d49fde755e BugFix for AppDefinitionEvent updates 2023-07-16 11:49:15 -04:00
Vitor Pamplona
0224af18c0 Forces a given event kind to be displayed in the Discovery tab 2023-07-16 11:47:05 -04:00
Vitor Pamplona
53a4dfb702 Moves video player to the new Exoplayer package 2023-07-16 11:42:55 -04:00
Vitor Pamplona
4e4e798133 v0.68.4 2023-07-14 18:58:45 -04:00
Vitor Pamplona
e8bd4a12ac Reduces profile feed size to 200 and zaps, reports and followers to 400 2023-07-14 18:55:57 -04:00
Vitor Pamplona
dffed6e78f Moves synchronized Zap allocation to after checking if the zap already exists for this user. 2023-07-14 18:52:11 -04:00
Vitor Pamplona
46a4f360f7 Moves UserProfile NewThreads, Conversations, Reports and App Recommendations to be additive filters 2023-07-14 18:22:53 -04:00
Vitor Pamplona
64a5071345 Moves NIP-05 captions from NIP-05 to Nostr Address 2023-07-14 17:41:16 -04:00
Vitor Pamplona
1fba93b4f0 Moves badges out of the User class 2023-07-14 17:19:40 -04:00
Vitor Pamplona
caa67b03a8 Keeping media feed active from the start of the app. 2023-07-14 16:54:57 -04:00
Vitor Pamplona
5f1b2f9eb8 Only logging pruning and printing stacktraces when it matters 2023-07-14 16:54:43 -04:00
Vitor Pamplona
2968cb12aa Adds EOSE limits to the Video tab 2023-07-14 16:31:57 -04:00
Vitor Pamplona
8261b38de9 Adds EOSE limits to the Discovery tab. 2023-07-14 16:31:48 -04:00
Vitor Pamplona
2c4489a0d0 Only triggers mutable state if the state actually changes. 2023-07-14 16:31:31 -04:00
Vitor Pamplona
52dcb73fd7 Puts the Wifi Processing in an IO Thread 2023-07-14 16:30:48 -04:00
Vitor Pamplona
91a2f8ecfa v0.68.3 2023-07-14 09:33:08 -04:00
Vitor Pamplona
6905e1b47f Clicks on Community and Channel headers now take you to the community instead of expanding 2023-07-14 09:22:56 -04:00
Vitor Pamplona
640b0d3174 Adds the post highlighted to the highlight display 2023-07-14 09:04:34 -04:00
Vitor Pamplona
a86cd8772f Merge remote-tracking branch 'origin/HEAD' 2023-07-14 08:38:49 -04:00
Vitor Pamplona
83be195a43 Migrates DropDownMenu to MutableState 2023-07-14 08:38:34 -04:00
Vitor Pamplona
14bf73e2f1 Merge pull request #504 from akiomik/update-i18n-ja
Update translations for ja
2023-07-14 07:43:18 -04:00
Vitor Pamplona
efb7b06e4e Merge pull request #505 from greenart7c3/main
fix crash when changing language in android < 13
2023-07-14 07:42:58 -04:00
Vitor Pamplona
8b9f37d678 Merge pull request #506 from greenart7c3/string_resources
update pt-br translations
2023-07-14 07:42:29 -04:00
greenart7c3
e83f16f5c5 update pt-br translations 2023-07-14 06:46:30 -03:00
greenart7c3
e648970b17 fix crash when changing setting in android api < 13 2023-07-14 05:51:09 -03:00
Akiomi Kamakura
44badf8c86 Update translations for ja 2023-07-14 15:21:13 +09:00
Vitor Pamplona
516ff17eed Activates NostrCheck on the Media Feed 2023-07-13 21:32:48 -04:00
Vitor Pamplona
3b1533eb82 Fixes size of custom reactions on the Stories feed. 2023-07-13 21:19:08 -04:00
108 changed files with 6626 additions and 1240 deletions

View File

@@ -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)

View File

@@ -13,8 +13,8 @@ android {
applicationId "com.vitorpamplona.amethyst"
minSdk 26
targetSdk 33
versionCode 250
versionName "0.68.2"
versionCode 256
versionName "0.69.3"
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'

View File

@@ -79,6 +79,7 @@ object ServiceManager {
NostrAccountDataSource.start()
NostrChatroomListDataSource.start()
NostrDiscoveryDataSource.start()
NostrVideoDataSource.start()
// More Info Data Sources
NostrSingleEventDataSource.start()

View File

@@ -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

View File

@@ -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)
}

View File

@@ -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)

View File

@@ -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) {

View File

@@ -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()
}
}

View File

@@ -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(

View File

@@ -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(

View File

@@ -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 }
}
}

View File

@@ -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 }

View File

@@ -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
}
}
}

View File

@@ -25,5 +25,6 @@ class BadgeProfilesEvent(
companion object {
const val kind = 30008
const val standardDTAg = "profile_badges"
}
}

View File

@@ -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())

View File

@@ -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)

View File

@@ -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
}

View File

@@ -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)
)
}
}
}

View File

@@ -20,6 +20,8 @@ class HighlightEvent(
fun author() = taggedUsers().firstOrNull()
fun quote() = content
fun inPost() = taggedAddresses().firstOrNull()
companion object {
const val kind = 9802

View File

@@ -14,8 +14,6 @@ interface LnZapEventInterface : EventInterface {
fun zappedRequestAuthor(): String?
fun taggedAddresses(): List<ATag>
fun amount(): BigDecimal?
fun containedPost(): Event?

View File

@@ -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
}

View File

@@ -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())
}
}
}

View File

@@ -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())
}
}
}

View File

@@ -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

View File

@@ -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)

View File

@@ -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))
)

View File

@@ -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()
}
}

View File

@@ -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
)
}
}
}

View File

@@ -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
)
) {

View File

@@ -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))
}

View File

@@ -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()
}
}
}

View File

@@ -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)
}
}
}

View File

@@ -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 ->

View File

@@ -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()

View File

@@ -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()

View File

@@ -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()

View File

@@ -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>
}

View File

@@ -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) }
}
}

View File

@@ -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()
}

View File

@@ -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() &&
(

View File

@@ -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()
}

View File

@@ -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 }))
}
}

View File

@@ -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
}

View File

@@ -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
}

View File

@@ -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()

View File

@@ -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
}

View File

@@ -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
}

View File

@@ -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
}

View File

@@ -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()
}

View File

@@ -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)
}
}

View File

@@ -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(

View File

@@ -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

View File

@@ -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 }
}
}

View 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),
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
)
}

View File

@@ -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
)
}
}

View File

@@ -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) {

View File

@@ -6,13 +6,9 @@ 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 +19,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.Size16Modifier
import com.vitorpamplona.amethyst.ui.theme.nip05
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
@@ -152,52 +149,31 @@ private fun DisplayNIP05(
if (user != "_") {
Text(
text = remember(nip05) { AnnotatedString(user) },
color = MaterialTheme.colors.placeholderText,
color = MaterialTheme.colors.nip05,
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 = MaterialTheme.colors.nip05),
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 +183,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 +200,7 @@ fun DisplayNip05ProfileStatus(user: User) {
if (user != "_") {
Text(
text = remember { AnnotatedString(user + "@") },
color = MaterialTheme.colors.nip05,
modifier = Modifier.padding(top = 1.dp, bottom = 1.dp, start = 5.dp),
maxLines = 1,
overflow = TextOverflow.Ellipsis
@@ -256,7 +211,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 = MaterialTheme.colors.nip05),
modifier = Modifier.padding(top = 1.dp, bottom = 1.dp, start = domainPadStart),
maxLines = 1,
overflow = TextOverflow.Ellipsis

View File

@@ -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(

View File

@@ -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,

View File

@@ -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

View File

@@ -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)
}
}

View File

@@ -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
}

View File

@@ -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)
}
)
}
}

View File

@@ -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()

View File

@@ -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

View File

@@ -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())) }
}
}
}

View File

@@ -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()
}

View File

@@ -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
)

View File

@@ -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())) }
}
}
}

View File

@@ -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,

View File

@@ -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)

View File

@@ -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

View File

@@ -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,

View File

@@ -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
)

View File

@@ -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()
}
}

View File

@@ -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
)

View File

@@ -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

View File

@@ -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()

View File

@@ -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
)
},

View File

@@ -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)
}

View File

@@ -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
)
}
}
}

View File

@@ -12,10 +12,13 @@ val BitcoinOrange = Color(0xFFF7931A)
val RoyalBlue = Color(0xFF4169E1)
val Following = Color(0xFF03DAC5)
val Nip05 = Color(0xFF01BAFF)
val FollowsFollow = Color.Yellow
val NIP05Verified = Color.Blue
val Nip05EmailColor = Color(0xFFb198ec)
val Nip05EmailColorDark = Color(0xFF6e5490)
val Nip05EmailColorLight = Color(0xFFa770f3)
val DarkerGreen = Color.Green.copy(alpha = 0.32f)
val WarningColor = Color(0xFFC62828)

View File

@@ -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)

View File

@@ -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)
@@ -263,6 +260,9 @@ val Colors.veryImportantLink: Color
val Colors.placeholderText: Color
get() = if (isLight) LightPlaceholderText else DarkPlaceholderText
val Colors.nip05: Color
get() = if (isLight) Nip05EmailColorLight else Nip05EmailColorDark
val Colors.placeholderTextColorFilter: ColorFilter
get() = if (isLight) LightPlaceholderTextColorFilter else DarkPlaceholderTextColorFilter
@@ -278,9 +278,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

View 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="#6e5490"/>
<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="#6e5490"/>
</vector>

View 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>

View 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="#a770f3"/>
<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="#a770f3"/>
</vector>

View 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>

View 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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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">(0100)%</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>

View File

@@ -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>

View File

@@ -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'

View 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

File diff suppressed because one or more lines are too long

View 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

View 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

Some files were not shown because too many files have changed in this diff Show More