Compare commits

...

45 Commits

Author SHA1 Message Date
Vitor Pamplona
4afcf48392 v0.16.2 2023-02-16 19:43:33 -05:00
Vitor Pamplona
943d4525be Removing favicons from proxy 2023-02-16 19:41:50 -05:00
Vitor Pamplona
de302eb966 Improving error count for relays to also include wrong URLs 2023-02-16 19:41:31 -05:00
Vitor Pamplona
85adc722ae v0.16.1 2023-02-16 19:04:26 -05:00
Vitor Pamplona
c8c48cca32 BugFix: User's relays not coming back after a pause of the app if they have not setup relays through the app yet. 2023-02-16 19:03:11 -05:00
Vitor Pamplona
3f68b42acd Moving Async Image Proxy to components. 2023-02-16 18:25:55 -05:00
Vitor Pamplona
da5c61038d Fixes https://github.com/vitorpamplona/amethyst/issues/129 2023-02-16 08:17:41 -05:00
Vitor Pamplona
e8949f08ae Asks reports when each user/event is seen instead. 2023-02-15 17:20:39 -05:00
Vitor Pamplona
068b5faf7d Remove logs 2023-02-15 17:19:55 -05:00
Vitor Pamplona
7dd01ae5e4 v0.16.0 2023-02-15 12:31:28 -05:00
Vitor Pamplona
3493802a1e Activating Image Proxy + CDN 2023-02-15 12:31:15 -05:00
Vitor Pamplona
9ec9feda28 v0.15.9 2023-02-14 20:34:27 -05:00
Vitor Pamplona
0277fa7364 Fixing the delay in updating the Note tab on the Profile View 2023-02-14 20:33:32 -05:00
Vitor Pamplona
af96f142cd Activating Event's check signature 2023-02-14 20:32:35 -05:00
Vitor Pamplona
f80ba653b2 v0.15.8 2023-02-14 19:23:33 -05:00
Vitor Pamplona
5947d03cb3 Follower Count bug 2023-02-14 19:22:37 -05:00
Vitor Pamplona
694cfdf322 v0.15.7 2023-02-14 18:21:57 -05:00
Vitor Pamplona
10efe5d17d Makes chat bubbles back to their contentWrap state 2023-02-14 18:15:33 -05:00
Vitor Pamplona
d964f93e67 Adds helper classes to avoid recreating master lists. 2023-02-14 17:44:03 -05:00
Vitor Pamplona
4996c5a9d8 Only adds a report if it doesn't already exist in the database 2023-02-14 17:43:19 -05:00
Vitor Pamplona
c0fe13d79c Only wait 5 seconds to update lists if contents are already been displayed. 2023-02-14 17:42:35 -05:00
Vitor Pamplona
c56d3af0fc v0.10.6 2023-02-14 16:13:11 -05:00
Vitor Pamplona
a050236d6a Increasing Delays to update lists to 5 seconds while making sure it refreshes quickly when the user switches the tab. 2023-02-14 16:02:38 -05:00
Vitor Pamplona
90bdeffc52 v0.15.5 Performance Improvements. 2023-02-14 14:26:28 -05:00
Vitor Pamplona
e3ed676624 Avoids using remember for Fixed avatars. 2023-02-14 14:25:26 -05:00
Vitor Pamplona
5e5ea32be1 Better fixed avatar handling. 2023-02-14 14:25:01 -05:00
Vitor Pamplona
1306270887 Update lists once every second, not 100ms 2023-02-14 14:24:50 -05:00
Vitor Pamplona
fbff78208e Making sure screens are refreshed from the start 2023-02-14 14:24:14 -05:00
Vitor Pamplona
8468212440 v0.15.4 2023-02-14 13:35:55 -05:00
Vitor Pamplona
7f5a173259 Adding more information to the debug info 2023-02-14 13:34:59 -05:00
Vitor Pamplona
1b8d83f41a Increasing Image disk cache size to 1GB 2023-02-14 13:34:45 -05:00
Vitor Pamplona
e381e2f05f Adjusting Post Lists to 1000 and simplifying comparisons to know when the list has changed. 2023-02-14 12:08:35 -05:00
Vitor Pamplona
6a05039bde Fixing Zap counter in the Profile Page. 2023-02-14 11:48:17 -05:00
Vitor Pamplona
76737e6b74 Adding Zaps to the profile 2023-02-14 11:37:41 -05:00
Vitor Pamplona
c3aa37534e Removing the Duplicated Observer infrastructure on User: It wasn't updating observers correctly due to the batch update we use. 2023-02-14 11:01:08 -05:00
Vitor Pamplona
8e897762dd Getting Relay icons from non SSL-secured relays 2023-02-14 10:58:42 -05:00
Vitor Pamplona
790cbb35db Moving Database updates to an invalidate UI call that only runs at every 100 ms. 2023-02-14 09:29:24 -05:00
Vitor Pamplona
743c23f3c8 Moving Incoming Payload processing to the Global Thread to release Web Threads faster. 2023-02-14 09:17:12 -05:00
Vitor Pamplona
385a1c3849 Merge pull request #122 from veer66/main
Fix issue #121
2023-02-14 08:25:00 -05:00
Vee Satayamas
a3a3f8ea99 Fix issue #121
NIP-09 is event deletion, not delegation
2023-02-14 10:49:30 +07:00
Vitor Pamplona
25b470cde6 Removing border colors. 2023-02-13 18:10:13 -05:00
Vitor Pamplona
05d71fd27e Better positioning of the User Search (always at the bottom, independent of screen size) 2023-02-13 18:08:38 -05:00
Vitor Pamplona
f5e61b9409 v0.15.3 2023-02-13 17:12:57 -05:00
Vitor Pamplona
620bd4d5a8 This might work best between smaller images and bigger images 2023-02-13 17:12:19 -05:00
Vitor Pamplona
3f671350ce Adjusting the New Post screen to show Search and Images (cropped if necessary) together with the keyboard and the text. 2023-02-13 16:48:42 -05:00
42 changed files with 586 additions and 380 deletions

View File

@@ -26,7 +26,7 @@ Amethyst brings the best social network to your Android phone. Just insert your
- [ ] Zaps
- [ ] Infinity Scroll
- [ ] Identity Verification (NIP-05)
- [ ] Event Delegation (NIP-09)
- [ ] Event Deletion (NIP-09)
- [ ] Account Creation / Backup Guidance
- [ ] Message Sent feedback

View File

@@ -11,8 +11,8 @@ android {
applicationId "com.vitorpamplona.amethyst"
minSdk 26
targetSdk 33
versionCode 45
versionName "0.15.2"
versionCode 55
versionName "0.16.2"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
vectorDrawables {

View File

@@ -3,6 +3,8 @@ package com.vitorpamplona.amethyst
import android.content.Context
import android.graphics.Bitmap
import android.util.LruCache
import androidx.compose.ui.graphics.ImageBitmap
import androidx.compose.ui.graphics.asImageBitmap
import java.util.UUID
import name.neuhalfen.projects.android.robohash.RoboHash
@@ -10,14 +12,18 @@ object RoboHashCache {
lateinit var robots: RoboHash
lateinit var defaultAvatar: ImageBitmap
@Synchronized
fun get(context: Context, hash: String): Bitmap {
fun get(context: Context, hash: String): ImageBitmap {
if (!this::robots.isInitialized) {
robots = RoboHash(context)
robots.useCache(LruCache(100));
//robots.useCache(LruCache(100));
defaultAvatar = robots.imageForHandle(robots.calculateHandleFromUUID(UUID.nameUUIDFromBytes("aaaa".toByteArray()))).asImageBitmap()
}
return robots.imageForHandle(robots.calculateHandleFromUUID(UUID.nameUUIDFromBytes("aaaa".toByteArray())))
return defaultAvatar
}
}

View File

@@ -29,7 +29,7 @@ object ServiceManager {
val myAccount = account
if (myAccount != null) {
Client.connect(myAccount.convertLocalRelays())
Client.connect(myAccount.activeRelays() ?: myAccount.convertLocalRelays())
// start services
NostrAccountDataSource.account = myAccount

View File

@@ -20,6 +20,7 @@ import java.util.Locale
import java.util.concurrent.atomic.AtomicBoolean
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.Job
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
@@ -136,9 +137,12 @@ class Account(
}
fun createZapRequestFor(user: User): LnZapRequestEvent? {
return createZapRequestFor(user.pubkeyHex)
}
fun createZapRequestFor(userPubKeyHex: String): LnZapRequestEvent? {
if (!isWriteable()) return null
return LnZapRequestEvent.create(user.pubkeyHex, userProfile().relays?.keys ?: localRelays.map { it.url }.toSet(), loggedIn.privKey!!)
return LnZapRequestEvent.create(userPubKeyHex, userProfile().relays?.keys ?: localRelays.map { it.url }.toSet(), loggedIn.privKey!!)
}
fun report(note: Note, type: ReportEvent.ReportType) {
@@ -391,13 +395,13 @@ class Account(
}
init {
userProfile().subscribe(object: User.Listener() {
override fun onRelayChange() {
userProfile().liveRelays.observeForever {
GlobalScope.launch(Dispatchers.IO) {
Client.disconnect()
Client.connect(activeRelays() ?: convertLocalRelays())
RelayPool.requestAndWatch()
}
})
}
}
// Observers line up here.

View File

@@ -22,6 +22,12 @@ import java.time.format.DateTimeFormatter
import java.util.Collections
import java.util.concurrent.ConcurrentHashMap
import com.vitorpamplona.amethyst.service.relays.Relay
import java.util.concurrent.atomic.AtomicBoolean
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import nostr.postr.events.ContactListEvent
import nostr.postr.events.DeletionEvent
import nostr.postr.events.Event
@@ -143,7 +149,7 @@ object LocalCache {
val user = getOrCreateUser(event.pubKey.toHexKey())
if (event.createdAt > user.updatedFollowsAt) {
//Log.d("CL", "AAA ${user.toBestDisplayName()} ${event.follows.size}")
Log.d("CL", "AAA ${user.toBestDisplayName()} ${event.follows.size}")
user.updateFollows(
event.follows.map {
try {
@@ -425,7 +431,7 @@ object LocalCache {
return
}
Log.d("ZP", "New ZapEvent ${event.content} (${notes.size},${users.size}) ${note.author?.toBestDisplayName()} ${formattedDateTime(event.createdAt)}")
//Log.d("ZP", "New ZapEvent ${event.content} (${notes.size},${users.size}) ${note.author?.toBestDisplayName()} ${formattedDateTime(event.createdAt)}")
// Adds notifications to users.
mentions.forEach {
@@ -455,7 +461,7 @@ object LocalCache {
note.loadEvent(event, author, mentions, repliesTo)
Log.d("ZP", "New Zap Request ${event.content} (${notes.size},${users.size}) ${note.author?.toBestDisplayName()} ${formattedDateTime(event.createdAt)}")
//Log.d("ZP", "New Zap Request ${event.content} (${notes.size},${users.size}) ${note.author?.toBestDisplayName()} ${formattedDateTime(event.createdAt)}")
// Adds notifications to users.
mentions.forEach {
@@ -502,12 +508,30 @@ object LocalCache {
val live: LocalCacheLiveData = LocalCacheLiveData(this)
private fun refreshObservers() {
live.refresh()
live.invalidateData()
}
}
class LocalCacheLiveData(val cache: LocalCache): LiveData<LocalCacheState>(LocalCacheState(cache)) {
fun refresh() {
// Refreshes observers in batches.
var handlerWaiting = AtomicBoolean()
@Synchronized
fun invalidateData() {
if (!hasActiveObservers()) return
if (handlerWaiting.getAndSet(true)) return
handlerWaiting.set(true)
val scope = CoroutineScope(Job() + Dispatchers.Main)
scope.launch {
delay(100)
refresh()
handlerWaiting.set(false)
}
}
private fun refresh() {
postValue(LocalCacheState(cache))
}
}

View File

@@ -132,7 +132,7 @@ class Note(val idHex: String) {
if (author !in reports.keys) {
reports = reports + Pair(author, setOf(note))
liveReports.invalidateData()
} else {
} else if (reports[author]?.contains(note) == false) {
reports = reports + Pair(author, (reports[author] ?: emptySet()) + note)
liveReports.invalidateData()
}

View File

@@ -1,28 +1,20 @@
package com.vitorpamplona.amethyst.model
import androidx.lifecycle.LiveData
import com.vitorpamplona.amethyst.lnurl.LnInvoiceUtil
import com.vitorpamplona.amethyst.service.NostrSingleUserDataSource
import com.vitorpamplona.amethyst.service.model.LnZapEvent
import com.vitorpamplona.amethyst.service.model.ReportEvent
import com.vitorpamplona.amethyst.service.relays.Client
import com.vitorpamplona.amethyst.service.relays.FeedType
import com.vitorpamplona.amethyst.service.relays.Relay
import com.vitorpamplona.amethyst.ui.note.toShortenHex
import fr.acinq.secp256k1.Hex
import java.math.BigDecimal
import java.util.Collections
import java.util.concurrent.ConcurrentHashMap
import java.util.concurrent.atomic.AtomicBoolean
import kotlin.time.ExperimentalTime
import kotlin.time.measureTimedValue
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import nostr.postr.events.ContactListEvent
import nostr.postr.events.Event
import nostr.postr.events.MetadataEvent
import nostr.postr.toNpub
@@ -60,10 +52,11 @@ class User(val pubkeyHex: String) {
var relaysBeingUsed = mapOf<String, RelayInfo>()
private set
var messages = mapOf<User, Set<Note>>()
data class Chatroom(var roomMessages: Set<Note>)
var privateChatrooms = mapOf<User, Chatroom>()
private set
var latestMetadataRequestEOSE: Long? = null
var latestReportRequestEOSE: Long? = null
fun toBestDisplayName(): String {
@@ -89,72 +82,50 @@ class User(val pubkeyHex: String) {
liveFollows.invalidateData()
user.liveFollows.invalidateData()
updateSubscribers {
it.onFollowsChange()
}
user.updateSubscribers {
it.onFollowsChange()
}
}
fun unfollow(user: User) {
follows = follows - user
user.followers = user.followers - this
liveFollows.invalidateData()
user.liveFollows.invalidateData()
updateSubscribers {
it.onFollowsChange()
}
user.updateSubscribers {
it.onFollowsChange()
}
}
fun follow(users: Set<User>, followedAt: Long) {
follows = follows + users
users.forEach {
it.followers = it.followers + this
it.liveFollows.invalidateData()
it.updateSubscribers {
it.onFollowsChange()
if (this !in it.followers) {
it.followers = it.followers + this
it.liveFollows.invalidateData()
}
}
liveFollows.invalidateData()
updateSubscribers {
it.onFollowsChange()
}
}
fun unfollow(users: Set<User>) {
follows = follows - users
users.forEach {
it.followers = it.followers - this
it.liveFollows.invalidateData()
it.updateSubscribers {
it.onFollowsChange()
if (this in it.followers) {
it.followers = it.followers - this
it.liveFollows.invalidateData()
}
}
liveFollows.invalidateData()
updateSubscribers {
it.onFollowsChange()
}
}
fun addTaggedPost(note: Note) {
if (note !in taggedPosts) {
taggedPosts = taggedPosts + note
updateSubscribers { it.onNewTaggedPosts() }
// No need for Listener yet
}
}
fun addNote(note: Note) {
if (note !in notes) {
notes = notes + note
updateSubscribers { it.onNewNotes() }
// No need for Listener yet
}
}
@@ -164,7 +135,7 @@ class User(val pubkeyHex: String) {
if (author !in reports.keys) {
reports = reports + Pair(author, setOf(note))
liveReports.invalidateData()
} else {
} else if (reports[author]?.contains(note) == false) {
reports = reports + Pair(author, (reports[author] ?: emptySet()) + note)
liveReports.invalidateData()
}
@@ -203,20 +174,19 @@ class User(val pubkeyHex: String) {
}
@Synchronized
fun getOrCreateChannel(user: User): Set<Note> {
return messages[user] ?: run {
val channel = setOf<Note>()
messages = messages + Pair(user, channel)
channel
fun getOrCreatePrivateChatroom(user: User): Chatroom {
return privateChatrooms[user] ?: run {
val privateChatroom = Chatroom(setOf<Note>())
privateChatrooms = privateChatrooms + Pair(user, privateChatroom)
privateChatroom
}
}
fun addMessage(user: User, msg: Note) {
val channel = getOrCreateChannel(user)
if (msg !in channel) {
messages = messages + Pair(user, channel + msg)
val privateChatroom = getOrCreatePrivateChatroom(user)
if (msg !in privateChatroom.roomMessages) {
privateChatroom.roomMessages = privateChatroom.roomMessages + msg
liveMessages.invalidateData()
updateSubscribers { it.onNewMessage() }
}
}
@@ -237,7 +207,6 @@ class User(val pubkeyHex: String) {
here.counter++
}
updateSubscribers { it.onNewRelayInfo() }
liveRelayInfo.invalidateData()
}
@@ -251,17 +220,11 @@ class User(val pubkeyHex: String) {
updatedFollowsAt = updateAt
}
data class RelayMetadata(val read: Boolean, val write: Boolean, val activeTypes: Set<FeedType>)
fun updateRelays(relayUse: Map<String, ContactListEvent.ReadWrite>) {
if (relays != relayUse) {
relays = relayUse
listeners.forEach {
it.onRelayChange()
}
liveRelays.invalidateData()
}
liveRelays.invalidateData()
}
fun updateUserInfo(newUserInfo: UserMetadata, updateAt: Long) {
@@ -276,9 +239,9 @@ class User(val pubkeyHex: String) {
}
fun hasSentMessagesTo(user: User?): Boolean {
val messagesToUser = messages[user] ?: return false
val messagesToUser = privateChatrooms[user] ?: return false
return messagesToUser.firstOrNull { this == it.author } != null
return messagesToUser.roomMessages.any { this == it.author }
}
fun hasReport(loggedIn: User, type: ReportEvent.ReportType): Boolean {
@@ -287,45 +250,6 @@ class User(val pubkeyHex: String) {
} != null
}
// Model Observers
private var listeners = setOf<Listener>()
fun subscribe(listener: Listener) {
listeners = listeners.plus(listener)
}
fun unsubscribe(listener: Listener) {
listeners = listeners.minus(listener)
}
abstract class Listener {
open fun onRelayChange() = Unit
open fun onFollowsChange() = Unit
open fun onNewTaggedPosts() = Unit
open fun onNewNotes() = Unit
open fun onNewMessage() = Unit
open fun onNewRelayInfo() = Unit
open fun onNewReports() = Unit
}
// Refreshes observers in batches.
var modelHandlerWaiting = AtomicBoolean()
@Synchronized
fun updateSubscribers(on: (Listener) -> Unit) {
if (modelHandlerWaiting.getAndSet(true)) return
modelHandlerWaiting.set(true)
val scope = CoroutineScope(Job() + Dispatchers.Default)
scope.launch {
delay(100)
listeners.forEach {
on(it)
}
modelHandlerWaiting.set(false)
}
}
// UI Observers line up here.
val liveFollows: UserLiveData = UserLiveData(this)
val liveReports: UserLiveData = UserLiveData(this)

View File

@@ -56,9 +56,9 @@ object NostrChatRoomDataSource: NostrDataSource<Note>("ChatroomFeed") {
// returns the last Note of each user.
override fun feed(): List<Note> {
val messages = account.userProfile().messages[withUser] ?: return emptyList()
val messages = account.userProfile().privateChatrooms[withUser] ?: return emptyList()
return messages.filter { account.isAcceptable(it) }.sortedBy { it.event?.createdAt }.reversed()
return messages.roomMessages.filter { account.isAcceptable(it) }.sortedBy { it.event?.createdAt }.reversed()
}
override fun updateChannelFilters() {

View File

@@ -75,11 +75,11 @@ object NostrChatroomListDataSource: NostrDataSource<Note>("MailBoxFeed") {
// returns the last Note of each user.
override fun feed(): List<Note> {
val messages = account.userProfile().messages
val messagingWith = messages.keys.filter { account.isAcceptable(it) }
val privateChatrooms = account.userProfile().privateChatrooms
val messagingWith = privateChatrooms.keys.filter { account.isAcceptable(it) }
val privateMessages = messagingWith.mapNotNull {
messages[it]?.sortedBy { it.event?.createdAt }?.lastOrNull { it.event != null }
privateChatrooms[it]?.roomMessages?.sortedBy { it.event?.createdAt }?.lastOrNull { it.event != null }
}
val publicChannels = account.followingChannels().map {

View File

@@ -3,6 +3,7 @@ package com.vitorpamplona.amethyst.service
import com.vitorpamplona.amethyst.model.LocalCache
import com.vitorpamplona.amethyst.model.Note
import com.vitorpamplona.amethyst.model.UrlCachedPreviewer
import com.vitorpamplona.amethyst.model.toHexKey
import com.vitorpamplona.amethyst.service.model.ChannelCreateEvent
import com.vitorpamplona.amethyst.service.model.ChannelHideMessageEvent
import com.vitorpamplona.amethyst.service.model.ChannelMessageEvent
@@ -16,6 +17,7 @@ import com.vitorpamplona.amethyst.service.model.RepostEvent
import com.vitorpamplona.amethyst.service.relays.Client
import com.vitorpamplona.amethyst.service.relays.Relay
import com.vitorpamplona.amethyst.service.relays.Subscription
import com.vitorpamplona.amethyst.service.relays.hasValidSignature
import java.util.Date
import java.util.UUID
import kotlinx.coroutines.CoroutineScope
@@ -33,23 +35,28 @@ import nostr.postr.events.TextNoteEvent
abstract class NostrDataSource<T>(val debugName: String) {
private var subscriptions = mapOf<String, Subscription>()
private var eventCounter = mapOf<String, Int>()
data class Counter(var counter:Int)
private var eventCounter = mapOf<String, Counter>()
fun printCounter() {
eventCounter.forEach {
println("AAA Count ${it.key}: ${it.value}")
println("AAA Count ${it.key}: ${it.value.counter}")
}
}
private val clientListener = object : Client.Listener() {
override fun onEvent(event: Event, subscriptionId: String, relay: Relay) {
if (subscriptionId in subscriptions.keys) {
if (!event.hasValidSignature()) return
val key = "${debugName} ${subscriptionId} ${event.kind}"
val keyValue = eventCounter.get(key)
if (keyValue != null) {
eventCounter = eventCounter + Pair(key, keyValue + 1)
keyValue.counter++
} else {
eventCounter = eventCounter + Pair(key, 1)
eventCounter = eventCounter + Pair(key, Counter(1))
}
try {
@@ -133,7 +140,7 @@ abstract class NostrDataSource<T>(val debugName: String) {
}
fun loadTop(): List<T> {
val returningList = feed().take(100)
val returningList = feed().take(1000)
// prepare previews
val scope = CoroutineScope(Job() + Dispatchers.IO)

View File

@@ -2,11 +2,16 @@ package com.vitorpamplona.amethyst.service
import com.vitorpamplona.amethyst.model.Account
import com.vitorpamplona.amethyst.model.LocalCache
import com.vitorpamplona.amethyst.model.LocalCacheState
import com.vitorpamplona.amethyst.model.Note
import com.vitorpamplona.amethyst.model.User
import com.vitorpamplona.amethyst.model.UserState
import com.vitorpamplona.amethyst.service.model.RepostEvent
import com.vitorpamplona.amethyst.service.relays.FeedType
import com.vitorpamplona.amethyst.service.relays.TypedFilter
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.launch
import nostr.postr.JsonFilter
import nostr.postr.events.TextNoteEvent
import nostr.postr.toHex
@@ -14,22 +19,26 @@ import nostr.postr.toHex
object NostrHomeDataSource: NostrDataSource<Note>("HomeFeed") {
lateinit var account: Account
object cacheListener: User.Listener() {
override fun onFollowsChange() {
resetFilters()
}
private val cacheListener: (UserState) -> Unit = {
invalidateFilters()
}
override fun start() {
if (this::account.isInitialized)
account.userProfile().subscribe(cacheListener)
if (this::account.isInitialized) {
GlobalScope.launch(Dispatchers.Main) {
account.userProfile().liveFollows.observeForever(cacheListener)
}
}
super.start()
}
override fun stop() {
super.stop()
if (this::account.isInitialized)
account.userProfile().unsubscribe(cacheListener)
if (this::account.isInitialized) {
GlobalScope.launch(Dispatchers.Main) {
account.userProfile().liveFollows.removeObserver(cacheListener)
}
}
}
fun createFollowAccountsFilter(): TypedFilter {
@@ -44,7 +53,7 @@ object NostrHomeDataSource: NostrDataSource<Note>("HomeFeed") {
return TypedFilter(
types = setOf(FeedType.FOLLOWS),
filter = JsonFilter(
kinds = listOf(TextNoteEvent.kind, RepostEvent.kind),
kinds = listOf(TextNoteEvent.kind),
authors = followSet,
limit = 400
)

View File

@@ -28,7 +28,7 @@ object NostrSingleEventDataSource: NostrDataSource<Note>("SingleEventFeed") {
val now = Date().time / 1000
return reactionsToWatch.filter {
val lastTime = it.lastReactionsDownloadTime;
val lastTime = it.lastReactionsDownloadTime
lastTime == null || lastTime < (now - 10)
}.map {
TypedFilter(
@@ -78,7 +78,7 @@ object NostrSingleEventDataSource: NostrDataSource<Note>("SingleEventFeed") {
)
}
val singleEventChannel = requestNewChannel() { time ->
val singleEventChannel = requestNewChannel { time ->
eventsToWatch.forEach {
LocalCache.getOrCreateNote(it).lastReactionsDownloadTime = time
}

View File

@@ -18,7 +18,6 @@ object NostrUserProfileDataSource: NostrDataSource<Note>("UserProfileFeed") {
fun loadUserProfile(userId: String) {
user = LocalCache.getOrCreateUser(userId)
resetFilters()
}
fun createUserInfoFilter(): TypedFilter {
@@ -48,8 +47,7 @@ object NostrUserProfileDataSource: NostrDataSource<Note>("UserProfileFeed") {
types = FeedType.values().toSet(),
filter = JsonFilter(
kinds = listOf(LnZapEvent.kind),
tags = mapOf("e" to listOf(user!!.pubkeyHex)),
limit = 100
tags = mapOf("p" to listOf(user!!.pubkeyHex))
)
)
}

View File

@@ -1,5 +1,6 @@
package com.vitorpamplona.amethyst.service.model
import android.util.Log
import java.util.Date
import nostr.postr.Utils
import nostr.postr.events.Event
@@ -16,10 +17,11 @@ class ChannelCreateEvent (
@Transient val channelInfo: ChannelData
init {
try {
channelInfo = MetadataEvent.gson.fromJson(content, ChannelData::class.java)
channelInfo = try {
MetadataEvent.gson.fromJson(content, ChannelData::class.java)
} catch (e: Exception) {
throw Error("can't parse $content", e)
Log.e("ChannelMetadataEvent", "Can't parse channel info $content", e)
ChannelData(null, null, null)
}
}
@@ -27,10 +29,15 @@ class ChannelCreateEvent (
const val kind = 40
fun create(channelInfo: ChannelData?, privateKey: ByteArray, createdAt: Long = Date().time / 1000): ChannelCreateEvent {
val content = if (channelInfo != null)
gson.toJson(channelInfo)
else
val content = try {
if (channelInfo != null)
gson.toJson(channelInfo)
else
""
} catch (t: Throwable) {
Log.e("ChannelCreateEvent", "Couldn't parse channel information", t)
""
}
val pubKey = Utils.pubkeyCreate(privateKey)
val tags = emptyList<List<String>>()

View File

@@ -1,5 +1,6 @@
package com.vitorpamplona.amethyst.service.model
import android.util.Log
import java.util.Date
import nostr.postr.ContactMetaData
import nostr.postr.Utils
@@ -20,21 +21,24 @@ class ChannelMetadataEvent (
init {
channel = tags.firstOrNull { it.firstOrNull() == "e" }?.getOrNull(1)
try {
channelInfo = MetadataEvent.gson.fromJson(content, ChannelCreateEvent.ChannelData::class.java)
} catch (e: Exception) {
throw Error("can't parse $content", e)
}
channelInfo =
try {
MetadataEvent.gson.fromJson(content, ChannelCreateEvent.ChannelData::class.java)
} catch (e: Exception) {
Log.e("ChannelMetadataEvent", "Can't parse channel info $content", e)
ChannelCreateEvent.ChannelData(null, null, null)
}
}
companion object {
const val kind = 41
fun create(newChannelInfo: ChannelCreateEvent.ChannelData?, originalChannelIdHex: String, privateKey: ByteArray, createdAt: Long = Date().time / 1000): ChannelMetadataEvent {
val content = if (newChannelInfo != null)
gson.toJson(newChannelInfo)
else
""
val content =
if (newChannelInfo != null)
gson.toJson(newChannelInfo)
else
""
val pubKey = Utils.pubkeyCreate(privateKey)
val tags = listOf( listOf("e", originalChannelIdHex, "", "root") )

View File

@@ -1,6 +1,8 @@
package com.vitorpamplona.amethyst.service.relays
import java.util.UUID
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.launch
import nostr.postr.events.Event
/**
@@ -50,7 +52,7 @@ object Client: RelayPool.Listener {
RelayPool.send(signedEvent)
}
fun close(subscriptionId: String){
fun close(subscriptionId: String) {
RelayPool.close(subscriptionId)
}
@@ -61,19 +63,35 @@ object Client: RelayPool.Listener {
}
override fun onEvent(event: Event, subscriptionId: String, relay: Relay) {
listeners.forEach { it.onEvent(event, subscriptionId, relay) }
// Releases the Web thread for the new payload.
// May need to add a processing queue if processing new events become too costly.
GlobalScope.launch {
listeners.forEach { it.onEvent(event, subscriptionId, relay) }
}
}
override fun onError(error: Error, subscriptionId: String, relay: Relay) {
listeners.forEach { it.onError(error, subscriptionId, relay) }
// Releases the Web thread for the new payload.
// May need to add a processing queue if processing new events become too costly.
GlobalScope.launch {
listeners.forEach { it.onError(error, subscriptionId, relay) }
}
}
override fun onRelayStateChange(type: Relay.Type, relay: Relay, channel: String?) {
listeners.forEach { it.onRelayStateChange(type, relay, channel) }
// Releases the Web thread for the new payload.
// May need to add a processing queue if processing new events become too costly.
GlobalScope.launch {
listeners.forEach { it.onRelayStateChange(type, relay, channel) }
}
}
override fun onSendResponse(eventId: String, success: Boolean, message: String, relay: Relay) {
listeners.forEach { it.onSendResponse(eventId, success, message, relay) }
// Releases the Web thread for the new payload.
// May need to add a processing queue if processing new events become too costly.
GlobalScope.launch {
listeners.forEach { it.onSendResponse(eventId, success, message, relay) }
}
}
fun subscribe(listener: Listener) {

View File

@@ -0,0 +1,16 @@
package com.vitorpamplona.amethyst.service.relays
import fr.acinq.secp256k1.Secp256k1
import nostr.postr.events.Event
import nostr.postr.events.generateId
fun Event.hasValidSignature(): Boolean {
if (!id.contentEquals(generateId())) {
return false
}
if (!Secp256k1.get().verifySchnorr(sig, id, pubKey)) {
return false
}
return true
}

View File

@@ -22,9 +22,6 @@ class Relay(
var activeTypes: Set<FeedType> = FeedType.values().toSet(),
) {
private val httpClient = OkHttpClient.Builder()
.connectTimeout(100, TimeUnit.SECONDS)
.readTimeout(100, TimeUnit.SECONDS)
.callTimeout(100, TimeUnit.SECONDS)
.followRedirects(true)
.followSslRedirects(true)
.build();
@@ -148,6 +145,8 @@ class Relay(
socket = httpClient.newWebSocket(request, listener)
} catch (e: Exception) {
errorCounter++
isReady = false
closingTime = Date().time / 1000
Log.e("Relay", "Relay Invalid $url")
e.printStackTrace()

View File

@@ -15,6 +15,7 @@ import coil.ImageLoader
import coil.decode.GifDecoder
import coil.decode.ImageDecoderDecoder
import coil.decode.SvgDecoder
import coil.disk.DiskCache
import coil.memory.MemoryCache
import coil.request.CachePolicy
import com.vitorpamplona.amethyst.EncryptedStorage

View File

@@ -5,8 +5,10 @@ import androidx.compose.foundation.border
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.itemsIndexed
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.foundation.verticalScroll
import androidx.compose.material.*
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
@@ -54,6 +56,8 @@ fun NewPostView(onClose: () -> Unit, baseReplyTo: Note? = null, account: Account
val focusRequester = remember { FocusRequester() }
val keyboardController = LocalSoftwareKeyboardController.current
val scroolState = rememberScrollState()
LaunchedEffect(Unit) {
delay(100)
focusRequester.requestFocus()
@@ -71,125 +75,133 @@ fun NewPostView(onClose: () -> Unit, baseReplyTo: Note? = null, account: Account
decorFitsSystemWindows = false
)
) {
Surface(
modifier = Modifier
.fillMaxWidth().fillMaxHeight()
) {
Column(
modifier = Modifier.padding(10.dp)
) {
Row(
modifier = Modifier
.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically
) {
CloseButton(onCancel = {
postViewModel.cancel()
onClose()
})
UploadFromGallery(
isUploading = postViewModel.isUploadingImage,
Surface(modifier = Modifier.fillMaxWidth().fillMaxHeight()) {
Column(modifier = Modifier.fillMaxWidth().fillMaxHeight()) {
Column(modifier = Modifier.padding(10.dp).imePadding().weight(1f)) {
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically
) {
postViewModel.upload(it, context)
}
PostButton(
onPost = {
postViewModel.sendPost()
CloseButton(onCancel = {
postViewModel.cancel()
onClose()
},
isActive = postViewModel.message.text.isNotBlank()
&& !postViewModel.isUploadingImage
)
}
})
if (postViewModel.replyTos != null && baseReplyTo?.event is TextNoteEvent) {
ReplyInformation(postViewModel.replyTos, postViewModel.mentions, "") {
postViewModel.removeFromReplyList(it)
UploadFromGallery(
isUploading = postViewModel.isUploadingImage,
) {
postViewModel.upload(it, context)
}
PostButton(
onPost = {
postViewModel.sendPost()
onClose()
},
isActive = postViewModel.message.text.isNotBlank()
&& !postViewModel.isUploadingImage
)
}
}
OutlinedTextField(
value = postViewModel.message,
onValueChange = {
postViewModel.updateMessage(it)
},
keyboardOptions = KeyboardOptions.Default.copy(
capitalization = KeyboardCapitalization.Sentences
),
modifier = Modifier
.fillMaxWidth()
.border(
width = 1.dp,
color = MaterialTheme.colors.surface,
shape = RoundedCornerShape(8.dp)
)
.focusRequester(focusRequester)
.onFocusChanged {
if (it.isFocused) {
keyboardController?.show()
}
}.imePadding(),
placeholder = {
Text(
text = "What's on your mind?",
color = MaterialTheme.colors.onSurface.copy(alpha = 0.32f)
)
},
colors = TextFieldDefaults
.outlinedTextFieldColors(
unfocusedBorderColor = Color.Transparent,
focusedBorderColor = Color.Transparent
),
visualTransformation = UrlUserTagTransformation(MaterialTheme.colors.primary),
textStyle = LocalTextStyle.current.copy(textDirection = TextDirection.Content)
)
val userSuggestions = postViewModel.userSuggestions
if (userSuggestions.isNotEmpty()) {
LazyColumn(
contentPadding = PaddingValues(
top = 10.dp,
bottom = 10.dp
)
Row(
modifier = Modifier.fillMaxWidth().weight(1f)
) {
itemsIndexed(userSuggestions, key = { _, item -> item.pubkeyHex }) { index, item ->
UserLine(item, account) {
postViewModel.autocompleteWithUser(item)
Column(modifier = Modifier.fillMaxWidth().verticalScroll(scroolState)) {
if (postViewModel.replyTos != null && baseReplyTo?.event is TextNoteEvent) {
ReplyInformation(postViewModel.replyTos, postViewModel.mentions, "") {
postViewModel.removeFromReplyList(it)
}
}
OutlinedTextField(
value = postViewModel.message,
onValueChange = {
postViewModel.updateMessage(it)
},
keyboardOptions = KeyboardOptions.Default.copy(
capitalization = KeyboardCapitalization.Sentences
),
modifier = Modifier
.fillMaxWidth()
.border(
width = 1.dp,
color = MaterialTheme.colors.surface,
shape = RoundedCornerShape(8.dp)
)
.focusRequester(focusRequester)
.onFocusChanged {
if (it.isFocused) {
keyboardController?.show()
}
},
placeholder = {
Text(
text = "What's on your mind?",
color = MaterialTheme.colors.onSurface.copy(alpha = 0.32f)
)
},
colors = TextFieldDefaults
.outlinedTextFieldColors(
unfocusedBorderColor = Color.Transparent,
focusedBorderColor = Color.Transparent
),
visualTransformation = UrlUserTagTransformation(MaterialTheme.colors.primary),
textStyle = LocalTextStyle.current.copy(textDirection = TextDirection.Content)
)
val myUrlPreview = postViewModel.urlPreview
if (myUrlPreview != null) {
Row(modifier = Modifier.padding(top = 5.dp)) {
if (isValidURL(myUrlPreview)) {
val removedParamsFromUrl =
myUrlPreview.split("?")[0].toLowerCase()
if (imageExtension.matcher(removedParamsFromUrl).matches()) {
AsyncImage(
model = myUrlPreview,
contentDescription = myUrlPreview,
contentScale = ContentScale.FillWidth,
modifier = Modifier
.padding(top = 4.dp)
.fillMaxWidth()
.clip(shape = RoundedCornerShape(15.dp))
.border(
1.dp,
MaterialTheme.colors.onSurface.copy(alpha = 0.12f),
RoundedCornerShape(15.dp)
)
)
} else if (videoExtension.matcher(removedParamsFromUrl)
.matches()
) {
VideoView(myUrlPreview)
} else {
UrlPreview(myUrlPreview, myUrlPreview)
}
} else if (noProtocolUrlValidator.matcher(myUrlPreview).matches()) {
UrlPreview("https://$myUrlPreview", myUrlPreview)
}
}
}
}
}
}
val myUrlPreview = postViewModel.urlPreview
if (myUrlPreview != null) {
Column(modifier = Modifier.padding(top = 5.dp)) {
if (isValidURL(myUrlPreview)) {
val removedParamsFromUrl = myUrlPreview.split("?")[0].toLowerCase()
if (imageExtension.matcher(removedParamsFromUrl).matches()) {
AsyncImage(
model = myUrlPreview,
contentDescription = myUrlPreview,
contentScale = ContentScale.FillWidth,
modifier = Modifier
.padding(top = 4.dp)
.fillMaxWidth()
.clip(shape = RoundedCornerShape(15.dp))
.border(
1.dp,
MaterialTheme.colors.onSurface.copy(alpha = 0.12f),
RoundedCornerShape(15.dp)
)
)
} else if (videoExtension.matcher(removedParamsFromUrl).matches()) {
VideoView(myUrlPreview)
} else {
UrlPreview(myUrlPreview, myUrlPreview)
}
val userSuggestions = postViewModel.userSuggestions
if (userSuggestions.isNotEmpty()) {
LazyColumn(
contentPadding = PaddingValues(
top = 10.dp
),
modifier = Modifier.heightIn(0.dp, 300.dp)
) {
itemsIndexed(
userSuggestions,
key = { _, item -> item.pubkeyHex }) { index, item ->
UserLine(item, account) {
postViewModel.autocompleteWithUser(item)
}
}
} else if (noProtocolUrlValidator.matcher(myUrlPreview).matches()) {
UrlPreview("https://$myUrlPreview", myUrlPreview)
}
}
}

View File

@@ -0,0 +1,82 @@
package com.vitorpamplona.amethyst.ui.components
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.ColorFilter
import androidx.compose.ui.graphics.DefaultAlpha
import androidx.compose.ui.graphics.FilterQuality
import androidx.compose.ui.graphics.drawscope.DrawScope
import androidx.compose.ui.graphics.painter.Painter
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
import coil.compose.AsyncImage
import coil.compose.AsyncImagePainter
import coil.compose.LocalImageLoader
import java.net.URLEncoder
import java.util.Base64
data class ResizeImage(val url: String?, val size: Dp)
@Composable
fun AsyncImageProxy(
model: ResizeImage,
contentDescription: String?,
modifier: Modifier = Modifier,
placeholder: Painter? = null,
error: Painter? = null,
fallback: Painter? = error,
onLoading: ((AsyncImagePainter.State.Loading) -> Unit)? = null,
onSuccess: ((AsyncImagePainter.State.Success) -> Unit)? = null,
onError: ((AsyncImagePainter.State.Error) -> Unit)? = null,
alignment: Alignment = Alignment.Center,
contentScale: ContentScale = ContentScale.Fit,
alpha: Float = DefaultAlpha,
colorFilter: ColorFilter? = null,
filterQuality: FilterQuality = DrawScope.DefaultFilterQuality,
) {
if (model.url == null) {
AsyncImage(
model = model.url,
contentDescription = contentDescription,
imageLoader = LocalImageLoader.current,
modifier = modifier,
placeholder = placeholder,
error = error,
fallback = fallback,
onLoading = onLoading,
onSuccess = onSuccess,
onError = onError,
alignment = alignment,
contentScale = contentScale,
alpha = alpha,
colorFilter = colorFilter,
filterQuality = filterQuality
)
} else {
val imgPx = with(LocalDensity.current) { model.size.toPx().toInt() }
val base64 = Base64.getUrlEncoder().encodeToString(model.url.toByteArray())
val extension = model.url.split(".").lastOrNull()
AsyncImage(
model = "https://d12fidohs5rlxk.cloudfront.net/preset:sharp/rs:fit:$imgPx:$imgPx:0/gravity:sm/$base64",
contentDescription = contentDescription,
imageLoader = LocalImageLoader.current,
modifier = modifier,
placeholder = placeholder,
error = error,
fallback = fallback,
onLoading = onLoading,
onSuccess = onSuccess,
onError = onError,
alignment = alignment,
contentScale = contentScale,
alpha = alpha,
colorFilter = colorFilter,
filterQuality = filterQuality
)
}
}

View File

@@ -38,10 +38,11 @@ import androidx.compose.ui.unit.sp
import androidx.core.content.ContextCompat.startActivity
import com.vitorpamplona.amethyst.R
import com.vitorpamplona.amethyst.lnurl.LightningAddressResolver
import com.vitorpamplona.amethyst.model.Account
import kotlinx.coroutines.launch
@Composable
fun InvoiceRequest(lud16: String, onClose: () -> Unit ) {
fun InvoiceRequest(lud16: String, toUserPubKeyHex: String, account: Account, onClose: () -> Unit ) {
val context = LocalContext.current
val scope = rememberCoroutineScope()
@@ -128,7 +129,9 @@ fun InvoiceRequest(lud16: String, onClose: () -> Unit ) {
Button(
modifier = Modifier.fillMaxWidth().padding(vertical = 10.dp),
onClick = {
LightningAddressResolver().lnAddressInvoice(lud16, amount * 1000, message,
val zapRequest = account.createZapRequestFor(toUserPubKeyHex)
LightningAddressResolver().lnAddressInvoice(lud16, amount * 1000, message, zapRequest?.toJson(),
onSuccess = {
runCatching {
val intent = Intent(Intent.ACTION_VIEW, Uri.parse("lightning:$it"))

View File

@@ -50,7 +50,6 @@ fun RichTextViewer(
) {
Column(
Modifier
.fillMaxWidth()
.animateContentSize()) {
// FlowRow doesn't work well with paragraphs. So we need to split them
content.split('\n').forEach { paragraph ->

View File

@@ -34,7 +34,9 @@ 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.painter.BitmapPainter
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.text.font.FontStyle
import androidx.compose.ui.text.font.FontWeight
@@ -47,6 +49,7 @@ import coil.compose.AsyncImage
import coil.compose.rememberAsyncImagePainter
import com.vitorpamplona.amethyst.R
import com.vitorpamplona.amethyst.RoboHashCache
import com.vitorpamplona.amethyst.model.LocalCache
import com.vitorpamplona.amethyst.service.NostrAccountDataSource
import com.vitorpamplona.amethyst.service.NostrChannelDataSource
import com.vitorpamplona.amethyst.service.NostrChatRoomDataSource
@@ -54,18 +57,24 @@ import com.vitorpamplona.amethyst.service.NostrChatroomListDataSource
import com.vitorpamplona.amethyst.service.NostrGlobalDataSource
import com.vitorpamplona.amethyst.service.NostrHomeDataSource
import com.vitorpamplona.amethyst.service.NostrNotificationDataSource
import com.vitorpamplona.amethyst.service.NostrSearchEventOrUserDataSource
import com.vitorpamplona.amethyst.service.NostrSingleChannelDataSource
import com.vitorpamplona.amethyst.service.NostrSingleEventDataSource
import com.vitorpamplona.amethyst.service.NostrSingleUserDataSource
import com.vitorpamplona.amethyst.service.NostrThreadDataSource
import com.vitorpamplona.amethyst.service.NostrUserProfileDataSource
import com.vitorpamplona.amethyst.service.NostrUserProfileFollowersDataSource
import com.vitorpamplona.amethyst.service.NostrUserProfileFollowsDataSource
import com.vitorpamplona.amethyst.service.NostrUserProfileZapsDataSource
import com.vitorpamplona.amethyst.service.relays.Client
import com.vitorpamplona.amethyst.service.relays.RelayPool
import com.vitorpamplona.amethyst.ui.components.AsyncImageProxy
import com.vitorpamplona.amethyst.ui.components.ResizeImage
import com.vitorpamplona.amethyst.ui.actions.NewRelayListView
import com.vitorpamplona.amethyst.ui.actions.NewUserMetadataView
import com.vitorpamplona.amethyst.ui.screen.RelayPoolViewModel
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
import java.net.URLEncoder
import kotlinx.coroutines.launch
@Composable
@@ -113,7 +122,8 @@ fun MainTopBar(scaffoldState: ScaffoldState, accountViewModel: AccountViewModel)
Box(Modifier) {
Column(
modifier = Modifier
.fillMaxWidth().fillMaxHeight()
.fillMaxWidth()
.fillMaxHeight()
.padding(start = 0.dp, end = 20.dp),
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.Center
@@ -139,18 +149,24 @@ fun MainTopBar(scaffoldState: ScaffoldState, accountViewModel: AccountViewModel)
NostrNotificationDataSource.printCounter()
NostrSingleEventDataSource.printCounter()
NostrSearchEventOrUserDataSource.printCounter()
NostrSingleChannelDataSource.printCounter()
NostrSingleUserDataSource.printCounter()
NostrThreadDataSource.printCounter()
NostrUserProfileDataSource.printCounter()
NostrUserProfileFollowersDataSource.printCounter()
NostrUserProfileFollowsDataSource.printCounter()
NostrUserProfileZapsDataSource.printCounter()
println("AAA: " + RelayPool.connectedRelays())
println("Connected Relays: " + RelayPool.connectedRelays())
val imageLoader = Coil.imageLoader(context)
println("${imageLoader.diskCache?.size}/${imageLoader.diskCache?.maxSize}")
println("${imageLoader.memoryCache?.size}/${imageLoader.memoryCache?.maxSize}")
println("Image Disk Cache ${(imageLoader.diskCache?.size ?: 0)/(1024*1024)}/${(imageLoader.diskCache?.maxSize ?: 0)/(1024*1024)} MB")
println("Image Memory Cache ${(imageLoader.memoryCache?.size ?: 0)/(1024*1024)}/${(imageLoader.memoryCache?.maxSize ?: 0)/(1024*1024)} MB")
println("Notes: " + LocalCache.notes.size)
println("Users: " + LocalCache.users.size)
}
) {
Icon(
@@ -197,11 +213,11 @@ fun MainTopBar(scaffoldState: ScaffoldState, accountViewModel: AccountViewModel)
},
modifier = Modifier
) {
AsyncImage(
model = accountUser.profilePicture(),
placeholder = rememberAsyncImagePainter(RoboHashCache.get(ctx, accountUser.pubkeyHex)),
fallback = rememberAsyncImagePainter(RoboHashCache.get(ctx, accountUser.pubkeyHex)),
error = rememberAsyncImagePainter(RoboHashCache.get(ctx, accountUser.pubkeyHex)),
AsyncImageProxy(
model = ResizeImage(accountUser.profilePicture(), 34.dp),
placeholder = BitmapPainter(RoboHashCache.get(ctx, accountUser.pubkeyHex)),
fallback = BitmapPainter(RoboHashCache.get(ctx, accountUser.pubkeyHex)),
error = BitmapPainter(RoboHashCache.get(ctx, accountUser.pubkeyHex)),
contentDescription = "Profile Image",
modifier = Modifier
.width(34.dp)

View File

@@ -71,8 +71,11 @@ import com.google.zxing.qrcode.encoder.ByteMatrix
import androidx.compose.ui.graphics.Path
import androidx.compose.ui.graphics.PathFillType
import androidx.compose.ui.graphics.painter.BitmapPainter
import androidx.compose.ui.platform.LocalContext
import com.vitorpamplona.amethyst.RoboHashCache
import com.vitorpamplona.amethyst.ui.components.AsyncImageProxy
import com.vitorpamplona.amethyst.ui.components.ResizeImage
@Composable
fun DrawerContent(navController: NavHostController,
@@ -131,8 +134,8 @@ fun ProfileContent(baseAccountUser: User, modifier: Modifier = Modifier, scaffol
Box {
val banner = accountUser.info.banner
if (banner != null && banner.isNotBlank()) {
AsyncImage(
model = banner,
AsyncImageProxy(
model = ResizeImage(banner, 150.dp),
contentDescription = "Profile Image",
contentScale = ContentScale.FillWidth,
modifier = Modifier
@@ -151,12 +154,12 @@ fun ProfileContent(baseAccountUser: User, modifier: Modifier = Modifier, scaffol
}
Column(modifier = modifier) {
AsyncImage(
model = accountUser.profilePicture(),
AsyncImageProxy(
model = ResizeImage(accountUser.profilePicture(), 100.dp),
contentDescription = "Profile Image",
placeholder = rememberAsyncImagePainter(RoboHashCache.get(ctx, accountUser.pubkeyHex)),
fallback = rememberAsyncImagePainter(RoboHashCache.get(ctx, accountUser.pubkeyHex)),
error = rememberAsyncImagePainter(RoboHashCache.get(ctx, accountUser.pubkeyHex)),
placeholder = BitmapPainter(RoboHashCache.get(ctx, accountUser.pubkeyHex)),
fallback = BitmapPainter(RoboHashCache.get(ctx, accountUser.pubkeyHex)),
error = BitmapPainter(RoboHashCache.get(ctx, accountUser.pubkeyHex)),
modifier = Modifier
.width(100.dp)
.height(100.dp)

View File

@@ -26,6 +26,7 @@ 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.painter.BitmapPainter
import androidx.compose.ui.graphics.painter.Painter
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.text.TextStyle
@@ -44,6 +45,8 @@ import com.vitorpamplona.amethyst.RoboHashCache
import com.vitorpamplona.amethyst.model.Note
import com.vitorpamplona.amethyst.service.model.ChannelCreateEvent
import com.vitorpamplona.amethyst.service.model.ChannelMetadataEvent
import com.vitorpamplona.amethyst.ui.components.AsyncImageProxy
import com.vitorpamplona.amethyst.ui.components.ResizeImage
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
@Composable
@@ -86,7 +89,7 @@ fun ChatroomCompose(baseNote: Note, accountViewModel: AccountViewModel, navContr
ChannelName(
channelPicture = channel.profilePicture(),
channelPicturePlaceholder = rememberAsyncImagePainter(RoboHashCache.get(context, channel.idHex)),
channelPicturePlaceholder = BitmapPainter(RoboHashCache.get(context, channel.idHex)),
channelTitle = {
Text(
"${channel.info.name}",
@@ -149,8 +152,8 @@ fun ChannelName(
) {
ChannelName(
channelPicture = {
AsyncImage(
model = channelPicture,
AsyncImageProxy(
model = ResizeImage(channelPicture, 55.dp),
placeholder = channelPicturePlaceholder,
fallback = channelPicturePlaceholder,
error = channelPicturePlaceholder,

View File

@@ -42,6 +42,7 @@ import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.ColorFilter
import androidx.compose.ui.graphics.ColorMatrix
import androidx.compose.ui.graphics.Shape
import androidx.compose.ui.graphics.painter.BitmapPainter
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalUriHandler
import androidx.compose.ui.text.font.FontWeight
@@ -60,6 +61,8 @@ import com.vitorpamplona.amethyst.service.model.ChannelMessageEvent
import com.vitorpamplona.amethyst.service.model.ChannelMetadataEvent
import com.vitorpamplona.amethyst.service.model.ReactionEvent
import com.vitorpamplona.amethyst.service.model.RepostEvent
import com.vitorpamplona.amethyst.ui.components.AsyncImageProxy
import com.vitorpamplona.amethyst.ui.components.ResizeImage
import com.vitorpamplona.amethyst.ui.components.RichTextViewer
import com.vitorpamplona.amethyst.ui.components.TranslateableRichTextViewer
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
@@ -160,11 +163,11 @@ fun ChatroomMessageCompose(baseNote: Note, routeForLastRead: String?, innerQuote
horizontalArrangement = alignment,
modifier = Modifier.padding(top = 5.dp)
) {
AsyncImage(
model = author.profilePicture(),
placeholder = rememberAsyncImagePainter(RoboHashCache.get(context, author.pubkeyHex)),
fallback = rememberAsyncImagePainter(RoboHashCache.get(context, author.pubkeyHex)),
error = rememberAsyncImagePainter(RoboHashCache.get(context, author.pubkeyHex)),
AsyncImageProxy(
model = ResizeImage(author.profilePicture(), 25.dp),
placeholder = BitmapPainter(RoboHashCache.get(context, author.pubkeyHex)),
fallback = BitmapPainter(RoboHashCache.get(context, author.pubkeyHex)),
error = BitmapPainter(RoboHashCache.get(context, author.pubkeyHex)),
contentDescription = "Profile Image",
modifier = Modifier
.width(25.dp)
@@ -288,9 +291,9 @@ private fun RelayBadges(baseNote: Note) {
Box(Modifier.size(15.dp).padding(1.dp)) {
AsyncImage(
model = "https://${url}/favicon.ico",
placeholder = rememberAsyncImagePainter(RoboHashCache.get(ctx, url)),
fallback = rememberAsyncImagePainter(RoboHashCache.get(ctx, url)),
error = rememberAsyncImagePainter(RoboHashCache.get(ctx, url)),
placeholder = BitmapPainter(RoboHashCache.get(ctx, url)),
fallback = BitmapPainter(RoboHashCache.get(ctx, url)),
error = BitmapPainter(RoboHashCache.get(ctx, url)),
contentDescription = "Relay Icon",
colorFilter = ColorFilter.colorMatrix(ColorMatrix().apply { setToSaturation(0f) }),
modifier = Modifier

View File

@@ -43,6 +43,7 @@ import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.ColorFilter
import androidx.compose.ui.graphics.ColorMatrix
import androidx.compose.ui.graphics.Shape
import androidx.compose.ui.graphics.painter.BitmapPainter
import androidx.compose.ui.platform.LocalClipboardManager
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalUriHandler
@@ -68,6 +69,8 @@ import com.vitorpamplona.amethyst.service.model.LnZapEvent
import com.vitorpamplona.amethyst.service.model.ReactionEvent
import com.vitorpamplona.amethyst.service.model.ReportEvent
import com.vitorpamplona.amethyst.service.model.RepostEvent
import com.vitorpamplona.amethyst.ui.components.AsyncImageProxy
import com.vitorpamplona.amethyst.ui.components.ResizeImage
import com.vitorpamplona.amethyst.ui.components.RichTextViewer
import com.vitorpamplona.amethyst.ui.components.TranslateableRichTextViewer
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
@@ -194,11 +197,11 @@ fun NoteCompose(
.width(30.dp)
.height(30.dp)
.align(Alignment.BottomEnd)) {
AsyncImage(
model = channel.profilePicture(),
placeholder = rememberAsyncImagePainter(RoboHashCache.get(context, channel.idHex)),
fallback = rememberAsyncImagePainter(RoboHashCache.get(context, channel.idHex)),
error = rememberAsyncImagePainter(RoboHashCache.get(context, channel.idHex)),
AsyncImageProxy(
model = ResizeImage(channel.profilePicture(), 30.dp),
placeholder = BitmapPainter(RoboHashCache.get(context, channel.idHex)),
fallback = BitmapPainter(RoboHashCache.get(context, channel.idHex)),
error = BitmapPainter(RoboHashCache.get(context, channel.idHex)),
contentDescription = "Group Picture",
modifier = Modifier
.width(30.dp)
@@ -332,16 +335,16 @@ private fun RelayBadges(baseNote: Note) {
FlowRow(Modifier.padding(top = 10.dp, start = 5.dp, end = 4.dp)) {
relaysToDisplay.forEach {
val url = it.removePrefix("wss://")
val url = it.removePrefix("wss://").removePrefix("ws://")
Box(
Modifier
.size(15.dp)
.padding(1.dp)) {
AsyncImage(
model = "https://${url}/favicon.ico",
placeholder = rememberAsyncImagePainter(RoboHashCache.get(ctx, url)),
fallback = rememberAsyncImagePainter(RoboHashCache.get(ctx, url)),
error = rememberAsyncImagePainter(RoboHashCache.get(ctx, url)),
placeholder = BitmapPainter(RoboHashCache.get(ctx, url)),
fallback = BitmapPainter(RoboHashCache.get(ctx, url)),
error = BitmapPainter(RoboHashCache.get(ctx, url)),
contentDescription = "Relay Icon",
colorFilter = ColorFilter.colorMatrix(ColorMatrix().apply { setToSaturation(0f) }),
modifier = Modifier
@@ -410,7 +413,7 @@ fun NoteAuthorPicture(
.height(size)) {
if (author == null) {
Image(
painter = rememberAsyncImagePainter(RoboHashCache.get(ctx, "ohnothisauthorisnotfound")),
painter = BitmapPainter(RoboHashCache.get(ctx, "ohnothisauthorisnotfound")),
contentDescription = "Unknown Author",
modifier = pictureModifier
.fillMaxSize(1f)
@@ -454,12 +457,12 @@ fun UserPicture(
.width(size)
.height(size)) {
AsyncImage(
model = user.profilePicture(),
AsyncImageProxy(
model = ResizeImage(user.profilePicture(), size),
contentDescription = "Profile Image",
placeholder = rememberAsyncImagePainter(RoboHashCache.get(ctx, user.pubkeyHex)),
fallback = rememberAsyncImagePainter(RoboHashCache.get(ctx, user.pubkeyHex)),
error = rememberAsyncImagePainter(RoboHashCache.get(ctx, user.pubkeyHex)),
placeholder = BitmapPainter(RoboHashCache.get(ctx, user.pubkeyHex)),
fallback = BitmapPainter(RoboHashCache.get(ctx, user.pubkeyHex)),
error = BitmapPainter(RoboHashCache.get(ctx, user.pubkeyHex)),
modifier = pictureModifier
.fillMaxSize(1f)
.clip(shape = CircleShape)

View File

@@ -27,6 +27,7 @@ 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.painter.BitmapPainter
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp
@@ -37,6 +38,8 @@ import coil.compose.AsyncImage
import coil.compose.rememberAsyncImagePainter
import com.vitorpamplona.amethyst.RoboHashCache
import com.vitorpamplona.amethyst.model.User
import com.vitorpamplona.amethyst.ui.components.AsyncImageProxy
import com.vitorpamplona.amethyst.ui.components.ResizeImage
import com.vitorpamplona.amethyst.ui.actions.CloseButton
import com.vitorpamplona.amethyst.ui.qrcode.QrCodeScanner
import nostr.postr.toNpub
@@ -79,11 +82,11 @@ fun ShowQRDialog(user: User, onScan: (String) -> Unit, onClose: () -> Unit) {
Column(modifier = Modifier.fillMaxWidth()) {
Row(horizontalArrangement = Arrangement.Center, modifier = Modifier.fillMaxWidth()) {
AsyncImage(
model = user.profilePicture(),
placeholder = rememberAsyncImagePainter(RoboHashCache.get(ctx, user.pubkeyHex)),
fallback = rememberAsyncImagePainter(RoboHashCache.get(ctx, user.pubkeyHex)),
error = rememberAsyncImagePainter(RoboHashCache.get(ctx, user.pubkeyHex)),
AsyncImageProxy(
model = ResizeImage(user.profilePicture(), 100.dp),
placeholder = BitmapPainter(RoboHashCache.get(ctx, user.pubkeyHex)),
fallback = BitmapPainter(RoboHashCache.get(ctx, user.pubkeyHex)),
error = BitmapPainter(RoboHashCache.get(ctx, user.pubkeyHex)),
contentDescription = "Profile Image",
modifier = Modifier
.width(100.dp)

View File

@@ -125,7 +125,8 @@ class CardFeedViewModel(val dataSource: NostrDataSource<Note>): ViewModel() {
handlerWaiting.set(true)
val scope = CoroutineScope(Job() + Dispatchers.Default)
scope.launch {
delay(100)
if (feedContent.value is CardFeedState.Loaded)
delay(5000)
refresh()
handlerWaiting.set(false)
}

View File

@@ -53,14 +53,14 @@ class NostrChatroomListNewFeedViewModel: FeedViewModel(NostrChatroomListDataSour
class NostrHomeFeedViewModel: FeedViewModel(NostrHomeDataSource) {
override fun newListFromDataSource(): List<Note> {
// Filter: no replies
return dataSource.feed().filter { it.isNewThread() }.take(100)
return dataSource.feed().filter { it.isNewThread() }.take(1000)
}
}
class NostrHomeRepliesFeedViewModel: FeedViewModel(NostrHomeDataSource) {
override fun newListFromDataSource(): List<Note> {
// Filter: only replies
return dataSource.feed().filter {! it.isNewThread() }.take(100)
return dataSource.feed().filter {! it.isNewThread() }.take(1000)
}
}
@@ -78,34 +78,39 @@ abstract class FeedViewModel(val dataSource: NostrDataSource<Note>): ViewModel()
}
fun refresh() {
viewModelScope.launch(Dispatchers.Default) {
val notes = newListFromDataSource()
val scope = CoroutineScope(Job() + Dispatchers.Default)
scope.launch {
refreshSuspended()
}
}
val oldNotesState = feedContent.value
if (oldNotesState is FeedState.Loaded) {
if (notes != oldNotesState.feed) {
withContext(Dispatchers.Main) {
updateFeed(notes)
}
}
} else {
withContext(Dispatchers.Main) {
updateFeed(notes)
}
fun refreshSuspended() {
val notes = newListFromDataSource()
val oldNotesState = feedContent.value
if (oldNotesState is FeedState.Loaded) {
// Using size as a proxy for has changed.
if (notes.size != oldNotesState.feed.value.size && notes.firstOrNull() != oldNotesState.feed.value.firstOrNull()) {
updateFeed(notes)
}
} else {
updateFeed(notes)
}
}
private fun updateFeed(notes: List<Note>) {
val currentState = feedContent.value
val scope = CoroutineScope(Job() + Dispatchers.Main)
scope.launch {
val currentState = feedContent.value
if (notes.isEmpty()) {
_feedContent.update { FeedState.Empty }
} else if (currentState is FeedState.Loaded) {
// updates the current list
currentState.feed.value = notes
} else {
_feedContent.update { FeedState.Loaded(mutableStateOf(notes)) }
if (notes.isEmpty()) {
_feedContent.update { FeedState.Empty }
} else if (currentState is FeedState.Loaded) {
// updates the current list
currentState.feed.value = notes
} else {
_feedContent.update { FeedState.Loaded(mutableStateOf(notes)) }
}
}
}
@@ -117,7 +122,8 @@ abstract class FeedViewModel(val dataSource: NostrDataSource<Note>): ViewModel()
handlerWaiting.set(true)
val scope = CoroutineScope(Job() + Dispatchers.Default)
scope.launch {
delay(100)
if (feedContent.value is FeedState.Loaded)
delay(5000)
refresh()
handlerWaiting.set(false)
}

View File

@@ -39,7 +39,8 @@ open class LnZapFeedViewModel(val dataSource: NostrDataSource<Pair<Note, Note>>)
val oldNotesState = feedContent.value
if (oldNotesState is LnZapFeedState.Loaded) {
if (notes != oldNotesState.feed) {
// Using size as a proxy for has changed.
if (notes.size != oldNotesState.feed.value.size && notes.firstOrNull() != oldNotesState.feed.value.firstOrNull()) {
updateFeed(notes)
}
} else {
@@ -71,7 +72,7 @@ open class LnZapFeedViewModel(val dataSource: NostrDataSource<Pair<Note, Note>>)
handlerWaiting.set(true)
val scope = CoroutineScope(Job() + Dispatchers.Default)
scope.launch {
delay(100)
delay(1000)
refresh()
handlerWaiting.set(false)
}

View File

@@ -20,6 +20,8 @@ import androidx.navigation.NavController
import com.google.accompanist.swiperefresh.SwipeRefresh
import com.google.accompanist.swiperefresh.rememberSwipeRefreshState
import com.vitorpamplona.amethyst.model.User
import com.vitorpamplona.amethyst.model.UserState
import com.vitorpamplona.amethyst.service.NostrHomeDataSource
import com.vitorpamplona.amethyst.ui.actions.NewRelayListView
import com.vitorpamplona.amethyst.ui.note.RelayCompose
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
@@ -59,21 +61,20 @@ class RelayFeedViewModel: ViewModel() {
}
}
inner class CacheListener: User.Listener() {
override fun onNewRelayInfo() { invalidateData() }
override fun onRelayChange() { invalidateData() }
val listener: (UserState) -> Unit = {
invalidateData()
}
val listener = CacheListener()
fun subscribeTo(user: User) {
currentUser = user
user.subscribe(listener)
user.liveRelays.observeForever(listener)
user.liveRelayInfo.observeForever(listener)
invalidateData()
}
fun unsubscribeTo(user: User) {
user.unsubscribe(listener)
user.liveRelays.removeObserver(listener)
user.liveRelayInfo.removeObserver(listener)
currentUser = null
}

View File

@@ -54,7 +54,8 @@ open class UserFeedViewModel(val dataSource: NostrDataSource<User>): ViewModel()
val oldNotesState = feedContent.value
if (oldNotesState is UserFeedState.Loaded) {
if (notes != oldNotesState.feed) {
// Using size as a proxy for has changed.
if (notes.size != oldNotesState.feed.value.size && notes.firstOrNull() != oldNotesState.feed.value.firstOrNull()) {
updateFeed(notes)
}
} else {
@@ -86,7 +87,8 @@ open class UserFeedViewModel(val dataSource: NostrDataSource<User>): ViewModel()
handlerWaiting.set(true)
val scope = CoroutineScope(Job() + Dispatchers.Default)
scope.launch {
delay(100)
if (feedContent.value is UserFeedState.Loaded)
delay(5000)
refresh()
handlerWaiting.set(false)
}

View File

@@ -38,6 +38,7 @@ 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.painter.BitmapPainter
import androidx.compose.ui.platform.ClipboardManager
import androidx.compose.ui.platform.LocalClipboardManager
import androidx.compose.ui.platform.LocalContext
@@ -61,6 +62,8 @@ import com.vitorpamplona.amethyst.model.Channel
import com.vitorpamplona.amethyst.model.Note
import com.vitorpamplona.amethyst.model.toNote
import com.vitorpamplona.amethyst.service.NostrChannelDataSource
import com.vitorpamplona.amethyst.ui.components.AsyncImageProxy
import com.vitorpamplona.amethyst.ui.components.ResizeImage
import com.vitorpamplona.amethyst.ui.actions.NewChannelView
import com.vitorpamplona.amethyst.ui.actions.NewPostView
import com.vitorpamplona.amethyst.ui.actions.PostButton
@@ -130,6 +133,7 @@ fun ChannelScreen(channelId: String?, accountViewModel: AccountViewModel, accoun
onPost = {
account.sendChannelMeesage(newPost.value.text, channel.idHex, null, null)
newPost.value = TextFieldValue("")
feedViewModel.refresh() // Don't wait a full second before updating
},
newPost.value.text.isNotBlank(),
modifier = Modifier.padding(end = 10.dp)
@@ -156,11 +160,11 @@ fun ChannelHeader(baseChannel: Channel, account: Account, accountStateViewModel:
Column(modifier = Modifier.padding(12.dp)) {
Row(verticalAlignment = Alignment.CenterVertically) {
AsyncImage(
model = channel.profilePicture(),
placeholder = rememberAsyncImagePainter(RoboHashCache.get(context, channel.idHex)),
fallback = rememberAsyncImagePainter(RoboHashCache.get(context, channel.idHex)),
error = rememberAsyncImagePainter(RoboHashCache.get(context, channel.idHex)),
AsyncImageProxy(
model = ResizeImage(channel.profilePicture(), 35.dp),
placeholder = BitmapPainter(RoboHashCache.get(context, channel.idHex)),
fallback = BitmapPainter(RoboHashCache.get(context, channel.idHex)),
error = BitmapPainter(RoboHashCache.get(context, channel.idHex)),
contentDescription = "Profile Image",
modifier = Modifier
.width(35.dp)

View File

@@ -74,6 +74,10 @@ fun ChatroomListScreen(accountViewModel: AccountViewModel, navController: NavCon
fun TabKnown(accountViewModel: AccountViewModel, navController: NavController) {
val feedViewModel: NostrChatroomListKnownFeedViewModel = viewModel()
LaunchedEffect(Unit) {
feedViewModel.hardRefresh() // refresh filters
feedViewModel.refresh() // refresh view
}
Column(Modifier.fillMaxHeight()) {
Column(
@@ -88,6 +92,11 @@ fun TabKnown(accountViewModel: AccountViewModel, navController: NavController) {
fun TabNew(accountViewModel: AccountViewModel, navController: NavController) {
val feedViewModel: NostrChatroomListNewFeedViewModel = viewModel()
LaunchedEffect(Unit) {
feedViewModel.hardRefresh() // refresh filters
feedViewModel.refresh() // refresh view
}
Column(Modifier.fillMaxHeight()) {
Column(
modifier = Modifier.padding(vertical = 0.dp)

View File

@@ -29,6 +29,7 @@ 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.painter.BitmapPainter
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.input.KeyboardCapitalization
@@ -43,6 +44,8 @@ import coil.compose.rememberAsyncImagePainter
import com.vitorpamplona.amethyst.RoboHashCache
import com.vitorpamplona.amethyst.model.User
import com.vitorpamplona.amethyst.service.NostrChatRoomDataSource
import com.vitorpamplona.amethyst.ui.components.AsyncImageProxy
import com.vitorpamplona.amethyst.ui.components.ResizeImage
import com.vitorpamplona.amethyst.ui.actions.PostButton
import com.vitorpamplona.amethyst.ui.note.UserPicture
import com.vitorpamplona.amethyst.ui.note.UsernameDisplay
@@ -109,6 +112,7 @@ fun ChatroomScreen(userId: String?, accountViewModel: AccountViewModel, navContr
onPost = {
account.sendPrivateMeesage(newPost.value.text, userId)
newPost.value = TextFieldValue("")
feedViewModel.refresh() // Don't wait a full second before updating
},
newPost.value.text.isNotBlank(),
modifier = Modifier.padding(end = 10.dp)
@@ -139,11 +143,11 @@ fun ChatroomHeader(baseUser: User, accountViewModel: AccountViewModel, navContro
val authorState by baseUser.liveMetadata.observeAsState()
val author = authorState?.user!!
AsyncImage(
model = author.profilePicture(),
placeholder = rememberAsyncImagePainter(RoboHashCache.get(ctx, author.pubkeyHex)),
fallback = rememberAsyncImagePainter(RoboHashCache.get(ctx, author.pubkeyHex)),
error = rememberAsyncImagePainter(RoboHashCache.get(ctx, author.pubkeyHex)),
AsyncImageProxy(
model = ResizeImage(author.profilePicture(), 35.dp),
placeholder = BitmapPainter(RoboHashCache.get(ctx, author.pubkeyHex)),
fallback = BitmapPainter(RoboHashCache.get(ctx, author.pubkeyHex)),
error = BitmapPainter(RoboHashCache.get(ctx, author.pubkeyHex)),
contentDescription = "Profile Image",
modifier = Modifier
.width(35.dp)

View File

@@ -34,6 +34,7 @@ fun HomeScreen(accountViewModel: AccountViewModel, navController: NavController)
LaunchedEffect(Unit) {
feedViewModel.refresh()
feedViewModelReplies.refresh()
}
Column(Modifier.fillMaxHeight()) {

View File

@@ -61,6 +61,8 @@ import com.vitorpamplona.amethyst.service.NostrUserProfileFollowersDataSource
import com.vitorpamplona.amethyst.service.NostrUserProfileFollowsDataSource
import com.vitorpamplona.amethyst.service.NostrUserProfileZapsDataSource
import com.vitorpamplona.amethyst.service.model.ReportEvent
import com.vitorpamplona.amethyst.ui.components.AsyncImageProxy
import com.vitorpamplona.amethyst.ui.components.ResizeImage
import com.vitorpamplona.amethyst.ui.actions.NewUserMetadataView
import com.vitorpamplona.amethyst.ui.components.InvoiceRequest
import com.vitorpamplona.amethyst.ui.note.UserPicture
@@ -79,16 +81,21 @@ fun ProfileScreen(userId: String?, accountViewModel: AccountViewModel, navContro
if (userId == null) return
NostrUserProfileDataSource.loadUserProfile(userId)
NostrUserProfileFollowersDataSource.loadUserProfile(userId)
NostrUserProfileFollowsDataSource.loadUserProfile(userId)
NostrUserProfileZapsDataSource.loadUserProfile(userId)
val lifeCycleOwner = LocalLifecycleOwner.current
DisposableEffect(accountViewModel) {
val observer = LifecycleEventObserver { source, event ->
if (event == Lifecycle.Event.ON_RESUME) {
println("Profile Start")
NostrUserProfileDataSource.loadUserProfile(userId)
NostrUserProfileFollowersDataSource.loadUserProfile(userId)
NostrUserProfileFollowsDataSource.loadUserProfile(userId)
NostrUserProfileZapsDataSource.loadUserProfile(userId)
NostrUserProfileDataSource.start()
NostrUserProfileFollowersDataSource.start()
NostrUserProfileFollowsDataSource.start()
NostrUserProfileZapsDataSource.start()
}
if (event == Lifecycle.Event.ON_PAUSE) {
println("Profile Stop")
@@ -332,7 +339,7 @@ private fun ProfileHeader(
}
}
DrawAdditionalInfo(baseUser)
DrawAdditionalInfo(baseUser, account)
Divider(modifier = Modifier.padding(top = 6.dp))
}
@@ -340,7 +347,7 @@ private fun ProfileHeader(
}
@Composable
private fun DrawAdditionalInfo(baseUser: User) {
private fun DrawAdditionalInfo(baseUser: User, account: Account) {
val userState by baseUser.liveMetadata.observeAsState()
val user = userState?.user ?: return
@@ -400,7 +407,7 @@ private fun DrawAdditionalInfo(baseUser: User) {
if (ZapExpanded) {
Row(verticalAlignment = Alignment.CenterVertically, modifier = Modifier.padding(vertical = 5.dp)) {
InvoiceRequest(lud16) {
InvoiceRequest(lud16, baseUser.pubkeyHex, account) {
ZapExpanded = false
}
}
@@ -422,8 +429,8 @@ private fun DrawBanner(baseUser: User) {
val banner = user.info.banner
if (banner != null && banner.isNotBlank()) {
AsyncImage(
model = banner,
AsyncImageProxy(
model = ResizeImage(banner, 125.dp),
contentDescription = "Profile Image",
contentScale = ContentScale.FillWidth,
modifier = Modifier
@@ -448,6 +455,10 @@ fun TabNotes(user: User, accountViewModel: AccountViewModel, navController: NavC
if (accountState != null) {
val feedViewModel: NostrUserProfileFeedViewModel = viewModel()
LaunchedEffect(Unit) {
feedViewModel.refresh()
}
Column(Modifier.fillMaxHeight()) {
Column(
modifier = Modifier.padding(vertical = 0.dp)
@@ -462,6 +473,10 @@ fun TabNotes(user: User, accountViewModel: AccountViewModel, navController: NavC
fun TabFollows(user: User, accountViewModel: AccountViewModel, navController: NavController) {
val feedViewModel: NostrUserProfileFollowsUserFeedViewModel = viewModel()
LaunchedEffect(Unit) {
feedViewModel.refresh()
}
Column(Modifier.fillMaxHeight()) {
Column(
modifier = Modifier.padding(vertical = 0.dp)
@@ -475,6 +490,10 @@ fun TabFollows(user: User, accountViewModel: AccountViewModel, navController: Na
fun TabFollowers(user: User, accountViewModel: AccountViewModel, navController: NavController) {
val feedViewModel: NostrUserProfileFollowersUserFeedViewModel = viewModel()
LaunchedEffect(Unit) {
feedViewModel.refresh()
}
Column(Modifier.fillMaxHeight()) {
Column(
modifier = Modifier.padding(vertical = 0.dp)
@@ -490,6 +509,10 @@ fun TabReceivedZaps(user: User, accountViewModel: AccountViewModel, navControlle
if (accountState != null) {
val feedViewModel: NostrUserProfileZapsFeedViewModel = viewModel()
LaunchedEffect(Unit) {
feedViewModel.refresh()
}
Column(Modifier.fillMaxHeight()) {
Column(
modifier = Modifier.padding(vertical = 0.dp)

View File

@@ -36,6 +36,7 @@ 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.graphics.painter.BitmapPainter
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalLifecycleOwner
import androidx.compose.ui.res.painterResource
@@ -78,6 +79,10 @@ fun SearchScreen(accountViewModel: AccountViewModel, navController: NavControlle
val feedViewModel: NostrGlobalFeedViewModel = viewModel()
val lifeCycleOwner = LocalLifecycleOwner.current
LaunchedEffect(Unit) {
feedViewModel.refresh()
}
DisposableEffect(accountViewModel) {
val observer = LifecycleEventObserver { source, event ->
if (event == Lifecycle.Event.ON_RESUME) {
@@ -234,7 +239,7 @@ private fun SearchBar(accountViewModel: AccountViewModel, navController: NavCont
itemsIndexed(searchResultsChannels.value, key = { _, item -> "c"+item.idHex }) { index, item ->
ChannelName(
channelPicture = item.profilePicture(),
channelPicturePlaceholder = rememberAsyncImagePainter(RoboHashCache.get(ctx, item.idHex)),
channelPicturePlaceholder = BitmapPainter(RoboHashCache.get(ctx, item.idHex)),
channelTitle = {
Text(
"${item.info.name}",

View File

@@ -5,6 +5,7 @@ import androidx.compose.foundation.layout.fillMaxHeight
import androidx.compose.foundation.layout.padding
import androidx.compose.runtime.Composable
import androidx.compose.runtime.DisposableEffect
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.livedata.observeAsState
import androidx.compose.ui.Modifier
@@ -50,6 +51,10 @@ fun ThreadScreen(noteId: String?, accountViewModel: AccountViewModel, navControl
val feedViewModel: NostrThreadFeedViewModel = viewModel()
LaunchedEffect(Unit) {
feedViewModel.refresh()
}
Column(Modifier.fillMaxHeight()) {
Column(
modifier = Modifier.padding(vertical = 0.dp)