mirror of
https://github.com/vitorpamplona/amethyst.git
synced 2025-04-12 05:49:30 +02:00
Merge branch 'main' into amber
This commit is contained in:
commit
dfbc5fa556
@ -83,7 +83,7 @@ height="80">](https://github.com/vitorpamplona/amethyst/releases)
|
||||
- [x] Gift Wraps & Seals (NIP-59)
|
||||
- [x] Versioned Encrypted Payloads (NIP-44)
|
||||
- [x] Expiration Support (NIP-40)
|
||||
- [x] Status Event (NIP-315)
|
||||
- [x] Status Event (NIP-38)
|
||||
- [ ] Marketplace (NIP-15)
|
||||
- [ ] Image/Video Capture in the app
|
||||
- [ ] Local Database
|
||||
|
@ -13,8 +13,8 @@ android {
|
||||
applicationId "com.vitorpamplona.amethyst"
|
||||
minSdk 26
|
||||
targetSdk 34
|
||||
versionCode 281
|
||||
versionName "0.75.2"
|
||||
versionCode 287
|
||||
versionName "0.75.8"
|
||||
|
||||
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
||||
vectorDrawables {
|
||||
@ -171,7 +171,7 @@ dependencies {
|
||||
playImplementation 'com.google.mlkit:translate:17.0.1'
|
||||
|
||||
// PushNotifications
|
||||
playImplementation platform('com.google.firebase:firebase-bom:32.2.2')
|
||||
playImplementation platform('com.google.firebase:firebase-bom:32.2.3')
|
||||
playImplementation 'com.google.firebase:firebase-messaging-ktx'
|
||||
|
||||
// Charts
|
||||
|
@ -122,7 +122,11 @@ object ServiceManager {
|
||||
isStarted = false
|
||||
}
|
||||
|
||||
fun cleanUp() {
|
||||
fun cleanObservers() {
|
||||
LocalCache.cleanObservers()
|
||||
}
|
||||
|
||||
fun trimMemory() {
|
||||
LocalCache.cleanObservers()
|
||||
|
||||
val accounts = LocalPreferences.allLocalAccountNPubs().mapNotNull { decodePublicKeyAsHexOrNull(it) }.toSet()
|
||||
|
@ -103,14 +103,16 @@ class Account(
|
||||
val showSensitiveContent: Boolean?
|
||||
)
|
||||
|
||||
val liveHiddenUsers: LiveData<LiveHiddenUsers> = live.combineWith(getBlockListNote().live().metadata) { localLive, liveMuteListEvent ->
|
||||
val liveBlockedUsers = (liveMuteListEvent?.note?.event as? PeopleListEvent)?.publicAndPrivateUsers(keyPair.privKey)
|
||||
LiveHiddenUsers(
|
||||
hiddenUsers = liveBlockedUsers ?: persistentSetOf(),
|
||||
spammers = localLive?.account?.transientHiddenUsers ?: persistentSetOf(),
|
||||
showSensitiveContent = showSensitiveContent
|
||||
)
|
||||
}.distinctUntilChanged()
|
||||
val liveHiddenUsers: LiveData<LiveHiddenUsers> by lazy {
|
||||
live.combineWith(getBlockListNote().live().metadata) { localLive, liveMuteListEvent ->
|
||||
val liveBlockedUsers = (liveMuteListEvent?.note?.event as? PeopleListEvent)?.publicAndPrivateUsers(keyPair.privKey)
|
||||
LiveHiddenUsers(
|
||||
hiddenUsers = liveBlockedUsers ?: persistentSetOf(),
|
||||
spammers = localLive?.account?.transientHiddenUsers ?: persistentSetOf(),
|
||||
showSensitiveContent = showSensitiveContent
|
||||
)
|
||||
}.distinctUntilChanged()
|
||||
}
|
||||
|
||||
var userProfileCache: User? = null
|
||||
|
||||
@ -1192,6 +1194,21 @@ class Account(
|
||||
LocalCache.consume(event, null)
|
||||
}
|
||||
|
||||
fun deleteStatus(oldStatus: AddressableNote) {
|
||||
if (!isWriteable()) return
|
||||
val oldEvent = oldStatus.event as? StatusEvent ?: return
|
||||
|
||||
val event = StatusEvent.clear(oldEvent, keyPair.privKey!!)
|
||||
|
||||
Client.send(event)
|
||||
LocalCache.consume(event, null)
|
||||
|
||||
val event2 = DeletionEvent.create(listOf(event.id), keyPair.privKey!!)
|
||||
|
||||
Client.send(event2)
|
||||
LocalCache.consume(event2)
|
||||
}
|
||||
|
||||
fun removeEmojiPack(usersEmojiList: Note, emojiList: Note) {
|
||||
if (!isWriteable()) return
|
||||
|
||||
|
@ -164,20 +164,12 @@ class ChannelLiveData(val channel: Channel) : LiveData<ChannelState>(ChannelStat
|
||||
|
||||
override fun onActive() {
|
||||
super.onActive()
|
||||
if (channel is PublicChatChannel) {
|
||||
NostrSingleChannelDataSource.add(channel.idHex)
|
||||
} else {
|
||||
NostrSingleChannelDataSource.add(channel.idHex)
|
||||
}
|
||||
NostrSingleChannelDataSource.add(channel)
|
||||
}
|
||||
|
||||
override fun onInactive() {
|
||||
super.onInactive()
|
||||
if (channel is PublicChatChannel) {
|
||||
NostrSingleChannelDataSource.remove(channel.idHex)
|
||||
} else {
|
||||
NostrSingleChannelDataSource.remove(channel.idHex)
|
||||
}
|
||||
NostrSingleChannelDataSource.remove(channel)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -0,0 +1,63 @@
|
||||
package com.vitorpamplona.amethyst.model
|
||||
|
||||
import androidx.compose.runtime.Stable
|
||||
import com.vitorpamplona.amethyst.service.checkNotInMainThread
|
||||
import com.vitorpamplona.quartz.encoders.HexKey
|
||||
import com.vitorpamplona.quartz.utils.TimeUtils
|
||||
|
||||
@Stable
|
||||
class Chatroom() {
|
||||
var roomMessages: Set<Note> = setOf()
|
||||
var subject: String? = null
|
||||
var subjectCreatedAt: Long? = null
|
||||
|
||||
@Synchronized
|
||||
fun addMessageSync(msg: Note) {
|
||||
checkNotInMainThread()
|
||||
|
||||
if (msg !in roomMessages) {
|
||||
roomMessages = roomMessages + msg
|
||||
|
||||
val newSubject = msg.event?.subject()
|
||||
|
||||
if (newSubject != null && (msg.createdAt() ?: 0) > (subjectCreatedAt ?: 0)) {
|
||||
subject = newSubject
|
||||
subjectCreatedAt = msg.createdAt()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Synchronized
|
||||
fun removeMessageSync(msg: Note) {
|
||||
checkNotInMainThread()
|
||||
|
||||
if (msg !in roomMessages) {
|
||||
roomMessages = roomMessages + msg
|
||||
|
||||
roomMessages.filter { it.event?.subject() != null }.sortedBy { it.createdAt() }.lastOrNull()?.let {
|
||||
subject = it.event?.subject()
|
||||
subjectCreatedAt = it.createdAt()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun senderIntersects(keySet: Set<HexKey>): Boolean {
|
||||
return roomMessages.any { it.author?.pubkeyHex in keySet }
|
||||
}
|
||||
|
||||
fun pruneMessagesToTheLatestOnly(): Set<Note> {
|
||||
val sorted = roomMessages.sortedWith(compareBy({ it.createdAt() }, { it.idHex })).reversed()
|
||||
|
||||
val toKeep = if ((sorted.firstOrNull()?.createdAt() ?: 0) > TimeUtils.oneWeekAgo()) {
|
||||
// Recent messages, keep last 100
|
||||
sorted.take(100).toSet()
|
||||
} else {
|
||||
// Old messages, keep the last one.
|
||||
sorted.take(1).toSet()
|
||||
} + sorted.filter { it.liveSet?.isInUse() ?: false }
|
||||
|
||||
val toRemove = roomMessages.minus(toKeep)
|
||||
roomMessages = toKeep
|
||||
return toRemove
|
||||
}
|
||||
}
|
@ -17,7 +17,6 @@ import com.vitorpamplona.quartz.encoders.bechToBytes
|
||||
import com.vitorpamplona.quartz.encoders.decodePublicKeyAsHexOrNull
|
||||
import com.vitorpamplona.quartz.encoders.toHexKey
|
||||
import com.vitorpamplona.quartz.events.*
|
||||
import com.vitorpamplona.quartz.utils.TimeUtils
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
import kotlinx.collections.immutable.persistentSetOf
|
||||
import kotlinx.collections.immutable.toImmutableList
|
||||
@ -372,7 +371,7 @@ object LocalCache {
|
||||
if (event.createdAt > (note.createdAt() ?: 0)) {
|
||||
note.loadEvent(event, author, emptyList())
|
||||
|
||||
author.liveSet?.statuses?.invalidateData()
|
||||
author.liveSet?.innerStatuses?.invalidateData()
|
||||
|
||||
refreshObservers(note)
|
||||
}
|
||||
@ -662,7 +661,7 @@ object LocalCache {
|
||||
|
||||
mentions.forEach {
|
||||
// doesn't add to reports, but triggers recounts
|
||||
it.liveSet?.reports?.invalidateData()
|
||||
it.liveSet?.innerReports?.invalidateData()
|
||||
}
|
||||
}
|
||||
|
||||
@ -1318,8 +1317,6 @@ object LocalCache {
|
||||
fun pruneExpiredEvents() {
|
||||
checkNotInMainThread()
|
||||
|
||||
val now = TimeUtils.now()
|
||||
|
||||
val toBeRemoved = notes.filter {
|
||||
it.value.event?.isExpired() == true
|
||||
}.values
|
||||
|
@ -3,8 +3,8 @@ package com.vitorpamplona.amethyst.model
|
||||
import androidx.compose.runtime.Immutable
|
||||
import androidx.compose.runtime.Stable
|
||||
import androidx.lifecycle.LiveData
|
||||
import androidx.lifecycle.MediatorLiveData
|
||||
import androidx.lifecycle.distinctUntilChanged
|
||||
import androidx.lifecycle.map
|
||||
import com.vitorpamplona.amethyst.service.NostrSingleEventDataSource
|
||||
import com.vitorpamplona.amethyst.service.checkNotInMainThread
|
||||
import com.vitorpamplona.amethyst.service.firstFullCharOrEmoji
|
||||
@ -22,6 +22,7 @@ import com.vitorpamplona.quartz.encoders.Nip19
|
||||
import com.vitorpamplona.quartz.encoders.toNote
|
||||
import com.vitorpamplona.quartz.events.*
|
||||
import com.vitorpamplona.quartz.utils.TimeUtils
|
||||
import kotlinx.collections.immutable.toImmutableList
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import java.math.BigDecimal
|
||||
import java.time.Instant
|
||||
@ -126,7 +127,7 @@ open class Note(val idHex: String) {
|
||||
this.author = author
|
||||
this.replyTo = replyTo
|
||||
|
||||
liveSet?.metadata?.invalidateData()
|
||||
liveSet?.innerMetadata?.invalidateData()
|
||||
}
|
||||
}
|
||||
|
||||
@ -174,21 +175,21 @@ open class Note(val idHex: String) {
|
||||
fun addReply(note: Note) {
|
||||
if (note !in replies) {
|
||||
replies = replies + note
|
||||
liveSet?.replies?.invalidateData()
|
||||
liveSet?.innerReplies?.invalidateData()
|
||||
}
|
||||
}
|
||||
|
||||
fun removeReply(note: Note) {
|
||||
if (note in replies) {
|
||||
replies = replies - note
|
||||
liveSet?.replies?.invalidateData()
|
||||
liveSet?.innerReplies?.invalidateData()
|
||||
}
|
||||
}
|
||||
|
||||
fun removeBoost(note: Note) {
|
||||
if (note in boosts) {
|
||||
boosts = boosts - note
|
||||
liveSet?.boosts?.invalidateData()
|
||||
liveSet?.innerBoosts?.invalidateData()
|
||||
}
|
||||
}
|
||||
|
||||
@ -211,11 +212,11 @@ open class Note(val idHex: String) {
|
||||
relays = listOf<String>()
|
||||
lastReactionsDownloadTime = emptyMap()
|
||||
|
||||
liveSet?.replies?.invalidateData()
|
||||
liveSet?.reactions?.invalidateData()
|
||||
liveSet?.boosts?.invalidateData()
|
||||
liveSet?.reports?.invalidateData()
|
||||
liveSet?.zaps?.invalidateData()
|
||||
liveSet?.innerReplies?.invalidateData()
|
||||
liveSet?.innerReactions?.invalidateData()
|
||||
liveSet?.innerBoosts?.invalidateData()
|
||||
liveSet?.innerReports?.invalidateData()
|
||||
liveSet?.innerZaps?.invalidateData()
|
||||
|
||||
return toBeRemoved
|
||||
}
|
||||
@ -234,7 +235,7 @@ open class Note(val idHex: String) {
|
||||
reactions = reactions + Pair(reaction, newList)
|
||||
}
|
||||
|
||||
liveSet?.reactions?.invalidateData()
|
||||
liveSet?.innerReactions?.invalidateData()
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -246,7 +247,7 @@ open class Note(val idHex: String) {
|
||||
if (author in reports.keys && reports[author]?.contains(deleteNote) == true) {
|
||||
reports[author]?.let {
|
||||
reports = reports + Pair(author, it.minus(deleteNote))
|
||||
liveSet?.reports?.invalidateData()
|
||||
liveSet?.innerReports?.invalidateData()
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -254,28 +255,28 @@ open class Note(val idHex: String) {
|
||||
fun removeZap(note: Note) {
|
||||
if (zaps[note] != null) {
|
||||
zaps = zaps.minus(note)
|
||||
liveSet?.zaps?.invalidateData()
|
||||
liveSet?.innerZaps?.invalidateData()
|
||||
} else if (zaps.containsValue(note)) {
|
||||
zaps = zaps.filterValues { it != note }
|
||||
liveSet?.zaps?.invalidateData()
|
||||
liveSet?.innerZaps?.invalidateData()
|
||||
}
|
||||
}
|
||||
|
||||
fun removeZapPayment(note: Note) {
|
||||
if (zapPayments[note] != null) {
|
||||
zapPayments = zapPayments.minus(note)
|
||||
liveSet?.zaps?.invalidateData()
|
||||
liveSet?.innerZaps?.invalidateData()
|
||||
} else if (zapPayments.containsValue(note)) {
|
||||
val toRemove = zapPayments.filterValues { it == note }
|
||||
zapPayments = zapPayments.minus(toRemove.keys)
|
||||
liveSet?.zaps?.invalidateData()
|
||||
liveSet?.innerZaps?.invalidateData()
|
||||
}
|
||||
}
|
||||
|
||||
fun addBoost(note: Note) {
|
||||
if (note !in boosts) {
|
||||
boosts = boosts + note
|
||||
liveSet?.boosts?.invalidateData()
|
||||
liveSet?.innerBoosts?.invalidateData()
|
||||
}
|
||||
}
|
||||
|
||||
@ -297,12 +298,12 @@ open class Note(val idHex: String) {
|
||||
if (zapRequest !in zaps.keys) {
|
||||
val inserted = innerAddZap(zapRequest, zap)
|
||||
if (inserted) {
|
||||
liveSet?.zaps?.invalidateData()
|
||||
liveSet?.innerZaps?.invalidateData()
|
||||
}
|
||||
} else if (zaps[zapRequest] == null) {
|
||||
val inserted = innerAddZap(zapRequest, zap)
|
||||
if (inserted) {
|
||||
liveSet?.zaps?.invalidateData()
|
||||
liveSet?.innerZaps?.invalidateData()
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -325,12 +326,12 @@ open class Note(val idHex: String) {
|
||||
if (zapPaymentRequest !in zapPayments.keys) {
|
||||
val inserted = innerAddZapPayment(zapPaymentRequest, zapPayment)
|
||||
if (inserted) {
|
||||
liveSet?.zaps?.invalidateData()
|
||||
liveSet?.innerZaps?.invalidateData()
|
||||
}
|
||||
} else if (zapPayments[zapPaymentRequest] == null) {
|
||||
val inserted = innerAddZapPayment(zapPaymentRequest, zapPayment)
|
||||
if (inserted) {
|
||||
liveSet?.zaps?.invalidateData()
|
||||
liveSet?.innerZaps?.invalidateData()
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -341,10 +342,10 @@ open class Note(val idHex: String) {
|
||||
|
||||
if (reaction !in reactions.keys) {
|
||||
reactions = reactions + Pair(reaction, listOf(note))
|
||||
liveSet?.reactions?.invalidateData()
|
||||
liveSet?.innerReactions?.invalidateData()
|
||||
} else if (reactions[reaction]?.contains(note) == false) {
|
||||
reactions = reactions + Pair(reaction, (reactions[reaction] ?: emptySet()) + note)
|
||||
liveSet?.reactions?.invalidateData()
|
||||
liveSet?.innerReactions?.invalidateData()
|
||||
}
|
||||
}
|
||||
|
||||
@ -353,17 +354,17 @@ open class Note(val idHex: String) {
|
||||
|
||||
if (author !in reports.keys) {
|
||||
reports = reports + Pair(author, listOf(note))
|
||||
liveSet?.reports?.invalidateData()
|
||||
liveSet?.innerReports?.invalidateData()
|
||||
} else if (reports[author]?.contains(note) == false) {
|
||||
reports = reports + Pair(author, (reports[author] ?: emptySet()) + note)
|
||||
liveSet?.reports?.invalidateData()
|
||||
liveSet?.innerReports?.invalidateData()
|
||||
}
|
||||
}
|
||||
|
||||
fun addRelay(relay: Relay) {
|
||||
if (relay.url !in relays) {
|
||||
relays = relays + relay.url
|
||||
liveSet?.relays?.invalidateData()
|
||||
liveSet?.innerRelays?.invalidateData()
|
||||
}
|
||||
}
|
||||
|
||||
@ -463,10 +464,6 @@ open class Note(val idHex: String) {
|
||||
}.flatten()
|
||||
}
|
||||
|
||||
fun countReactions(): Int {
|
||||
return reactions.values.sumOf { it.size }
|
||||
}
|
||||
|
||||
fun zappedAmount(privKey: ByteArray?, walletServicePubkey: ByteArray?): BigDecimal {
|
||||
// Regular Zap Receipts
|
||||
val completedZaps = zaps.asSequence()
|
||||
@ -657,36 +654,52 @@ open class Note(val idHex: String) {
|
||||
@Stable
|
||||
class NoteLiveSet(u: Note) {
|
||||
// Observers line up here.
|
||||
val metadata: NoteLiveData = NoteLiveData(u)
|
||||
val innerMetadata = NoteBundledRefresherLiveData(u)
|
||||
val innerReactions = NoteBundledRefresherLiveData(u)
|
||||
val innerBoosts = NoteBundledRefresherLiveData(u)
|
||||
val innerReplies = NoteBundledRefresherLiveData(u)
|
||||
val innerReports = NoteBundledRefresherLiveData(u)
|
||||
val innerRelays = NoteBundledRefresherLiveData(u)
|
||||
val innerZaps = NoteBundledRefresherLiveData(u)
|
||||
|
||||
val authorChanges = metadata.map {
|
||||
val metadata = innerMetadata.map { it }
|
||||
val reactions = innerReactions.map { it }
|
||||
val boosts = innerBoosts.map { it }
|
||||
val replies = innerReplies.map { it }
|
||||
val reports = innerReports.map { it }
|
||||
val relays = innerRelays.map { it }
|
||||
val zaps = innerZaps.map { it }
|
||||
|
||||
val authorChanges = innerMetadata.map {
|
||||
it.note.author
|
||||
}
|
||||
val hasEvent = metadata.map {
|
||||
|
||||
val hasEvent = innerMetadata.map {
|
||||
it.note.event != null
|
||||
}.distinctUntilChanged()
|
||||
|
||||
val reactions: NoteLiveData = NoteLiveData(u)
|
||||
val boosts: NoteLiveData = NoteLiveData(u)
|
||||
val replies: NoteLiveData = NoteLiveData(u)
|
||||
val reports: NoteLiveData = NoteLiveData(u)
|
||||
val relays: NoteLiveData = NoteLiveData(u)
|
||||
val zaps: NoteLiveData = NoteLiveData(u)
|
||||
|
||||
val hasReactions = zaps.combineWith(boosts, reactions) { zapState, boostState, reactionState ->
|
||||
val hasReactions = innerZaps.combineWith(innerBoosts, innerReactions) { zapState, boostState, reactionState ->
|
||||
zapState?.note?.zaps?.isNotEmpty() ?: false ||
|
||||
boostState?.note?.boosts?.isNotEmpty() ?: false ||
|
||||
reactionState?.note?.reactions?.isNotEmpty() ?: false
|
||||
}.distinctUntilChanged()
|
||||
|
||||
val replyCount = replies.map {
|
||||
val replyCount = innerReplies.map {
|
||||
it.note.replies.size
|
||||
}.distinctUntilChanged()
|
||||
|
||||
val boostCount = boosts.map {
|
||||
val reactionCount = innerReactions.map {
|
||||
it.note.reactions.values.sumOf { it.size }
|
||||
}.distinctUntilChanged()
|
||||
|
||||
val boostCount = innerBoosts.map {
|
||||
it.note.boosts.size
|
||||
}.distinctUntilChanged()
|
||||
|
||||
val boostList = innerBoosts.map {
|
||||
it.note.boosts.toImmutableList()
|
||||
}.distinctUntilChanged()
|
||||
|
||||
fun isInUse(): Boolean {
|
||||
return metadata.hasObservers() ||
|
||||
reactions.hasObservers() ||
|
||||
@ -694,21 +707,28 @@ class NoteLiveSet(u: Note) {
|
||||
replies.hasObservers() ||
|
||||
reports.hasObservers() ||
|
||||
relays.hasObservers() ||
|
||||
zaps.hasObservers()
|
||||
zaps.hasObservers() ||
|
||||
authorChanges.hasObservers() ||
|
||||
hasEvent.hasObservers() ||
|
||||
hasReactions.hasObservers() ||
|
||||
replyCount.hasObservers() ||
|
||||
reactionCount.hasObservers() ||
|
||||
boostCount.hasObservers() ||
|
||||
boostList.hasObservers()
|
||||
}
|
||||
|
||||
fun destroy() {
|
||||
metadata.destroy()
|
||||
reactions.destroy()
|
||||
boosts.destroy()
|
||||
replies.destroy()
|
||||
reports.destroy()
|
||||
relays.destroy()
|
||||
zaps.destroy()
|
||||
innerMetadata.destroy()
|
||||
innerReactions.destroy()
|
||||
innerBoosts.destroy()
|
||||
innerReplies.destroy()
|
||||
innerReports.destroy()
|
||||
innerRelays.destroy()
|
||||
innerZaps.destroy()
|
||||
}
|
||||
}
|
||||
|
||||
class NoteLiveData(val note: Note) : LiveData<NoteState>(NoteState(note)) {
|
||||
class NoteBundledRefresherLiveData(val note: Note) : LiveData<NoteState>(NoteState(note)) {
|
||||
// Refreshes observers in batches.
|
||||
private val bundler = BundledUpdate(500, Dispatchers.IO)
|
||||
|
||||
@ -730,6 +750,17 @@ class NoteLiveData(val note: Note) : LiveData<NoteState>(NoteState(note)) {
|
||||
}
|
||||
}
|
||||
|
||||
fun <Y> map(
|
||||
transform: (NoteState) -> Y
|
||||
): NoteLoadingLiveData<Y> {
|
||||
val initialValue = this.value?.let { transform(it) }
|
||||
val result = NoteLoadingLiveData(note, initialValue)
|
||||
result.addSource(this) { x -> result.value = transform(x) }
|
||||
return result
|
||||
}
|
||||
}
|
||||
|
||||
class NoteLoadingLiveData<Y>(val note: Note, initialValue: Y?) : MediatorLiveData<Y>(initialValue) {
|
||||
override fun onActive() {
|
||||
super.onActive()
|
||||
if (note is AddressableNote) {
|
||||
|
@ -3,6 +3,7 @@ package com.vitorpamplona.amethyst.model
|
||||
import androidx.compose.runtime.Immutable
|
||||
import androidx.compose.runtime.Stable
|
||||
import androidx.lifecycle.LiveData
|
||||
import androidx.lifecycle.MediatorLiveData
|
||||
import androidx.lifecycle.distinctUntilChanged
|
||||
import androidx.lifecycle.map
|
||||
import com.vitorpamplona.amethyst.service.NostrSingleUserDataSource
|
||||
@ -23,7 +24,6 @@ import com.vitorpamplona.quartz.events.MetadataEvent
|
||||
import com.vitorpamplona.quartz.events.ReportEvent
|
||||
import com.vitorpamplona.quartz.events.UserMetadata
|
||||
import com.vitorpamplona.quartz.events.toImmutableListOfLists
|
||||
import com.vitorpamplona.quartz.utils.TimeUtils
|
||||
import kotlinx.collections.immutable.persistentSetOf
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import java.math.BigDecimal
|
||||
@ -101,7 +101,7 @@ class User(val pubkeyHex: String) {
|
||||
if (event.id == latestBookmarkList?.id) return
|
||||
|
||||
latestBookmarkList = event
|
||||
liveSet?.bookmarks?.invalidateData()
|
||||
liveSet?.innerBookmarks?.invalidateData()
|
||||
}
|
||||
|
||||
fun updateContactList(event: ContactListEvent) {
|
||||
@ -111,18 +111,18 @@ class User(val pubkeyHex: String) {
|
||||
latestContactList = event
|
||||
|
||||
// Update following of the current user
|
||||
liveSet?.follows?.invalidateData()
|
||||
liveSet?.innerFollows?.invalidateData()
|
||||
|
||||
// Update Followers of the past user list
|
||||
// Update Followers of the new contact list
|
||||
(oldContactListEvent)?.unverifiedFollowKeySet()?.forEach {
|
||||
LocalCache.users[it]?.liveSet?.followers?.invalidateData()
|
||||
LocalCache.users[it]?.liveSet?.innerFollowers?.invalidateData()
|
||||
}
|
||||
(latestContactList)?.unverifiedFollowKeySet()?.forEach {
|
||||
LocalCache.users[it]?.liveSet?.followers?.invalidateData()
|
||||
LocalCache.users[it]?.liveSet?.innerFollowers?.invalidateData()
|
||||
}
|
||||
|
||||
liveSet?.relays?.invalidateData()
|
||||
liveSet?.innerRelays?.invalidateData()
|
||||
}
|
||||
|
||||
fun addReport(note: Note) {
|
||||
@ -130,10 +130,10 @@ class User(val pubkeyHex: String) {
|
||||
|
||||
if (author !in reports.keys) {
|
||||
reports = reports + Pair(author, setOf(note))
|
||||
liveSet?.reports?.invalidateData()
|
||||
liveSet?.innerReports?.invalidateData()
|
||||
} else if (reports[author]?.contains(note) == false) {
|
||||
reports = reports + Pair(author, (reports[author] ?: emptySet()) + note)
|
||||
liveSet?.reports?.invalidateData()
|
||||
liveSet?.innerReports?.invalidateData()
|
||||
}
|
||||
}
|
||||
|
||||
@ -143,7 +143,7 @@ class User(val pubkeyHex: String) {
|
||||
if (author in reports.keys && reports[author]?.contains(deleteNote) == true) {
|
||||
reports[author]?.let {
|
||||
reports = reports + Pair(author, it.minus(deleteNote))
|
||||
liveSet?.reports?.invalidateData()
|
||||
liveSet?.innerReports?.invalidateData()
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -151,20 +151,20 @@ class User(val pubkeyHex: String) {
|
||||
fun addZap(zapRequest: Note, zap: Note?) {
|
||||
if (zapRequest !in zaps.keys) {
|
||||
zaps = zaps + Pair(zapRequest, zap)
|
||||
liveSet?.zaps?.invalidateData()
|
||||
liveSet?.innerZaps?.invalidateData()
|
||||
} else if (zapRequest in zaps.keys && zaps[zapRequest] == null) {
|
||||
zaps = zaps + Pair(zapRequest, zap)
|
||||
liveSet?.zaps?.invalidateData()
|
||||
liveSet?.innerZaps?.invalidateData()
|
||||
}
|
||||
}
|
||||
|
||||
fun removeZap(zapRequestOrZapEvent: Note) {
|
||||
if (zapRequestOrZapEvent in zaps.keys) {
|
||||
zaps = zaps.minus(zapRequestOrZapEvent)
|
||||
liveSet?.zaps?.invalidateData()
|
||||
liveSet?.innerZaps?.invalidateData()
|
||||
} else if (zapRequestOrZapEvent in zaps.values) {
|
||||
zaps = zaps.filter { it.value != zapRequestOrZapEvent }
|
||||
liveSet?.zaps?.invalidateData()
|
||||
liveSet?.innerZaps?.invalidateData()
|
||||
}
|
||||
}
|
||||
|
||||
@ -218,7 +218,7 @@ class User(val pubkeyHex: String) {
|
||||
val privateChatroom = getOrCreatePrivateChatroom(room)
|
||||
if (msg !in privateChatroom.roomMessages) {
|
||||
privateChatroom.addMessageSync(msg)
|
||||
liveSet?.messages?.invalidateData()
|
||||
liveSet?.innerMessages?.invalidateData()
|
||||
}
|
||||
}
|
||||
|
||||
@ -226,7 +226,7 @@ class User(val pubkeyHex: String) {
|
||||
val privateChatroom = getOrCreatePrivateChatroom(user)
|
||||
if (msg !in privateChatroom.roomMessages) {
|
||||
privateChatroom.addMessageSync(msg)
|
||||
liveSet?.messages?.invalidateData()
|
||||
liveSet?.innerMessages?.invalidateData()
|
||||
}
|
||||
}
|
||||
|
||||
@ -240,7 +240,7 @@ class User(val pubkeyHex: String) {
|
||||
val privateChatroom = getOrCreatePrivateChatroom(user)
|
||||
if (msg in privateChatroom.roomMessages) {
|
||||
privateChatroom.removeMessageSync(msg)
|
||||
liveSet?.messages?.invalidateData()
|
||||
liveSet?.innerMessages?.invalidateData()
|
||||
}
|
||||
}
|
||||
|
||||
@ -255,7 +255,7 @@ class User(val pubkeyHex: String) {
|
||||
here.counter++
|
||||
}
|
||||
|
||||
liveSet?.relayInfo?.invalidateData()
|
||||
liveSet?.innerRelayInfo?.invalidateData()
|
||||
}
|
||||
|
||||
fun updateUserInfo(newUserInfo: UserMetadata, latestMetadata: MetadataEvent) {
|
||||
@ -280,7 +280,7 @@ class User(val pubkeyHex: String) {
|
||||
}
|
||||
}
|
||||
|
||||
liveSet?.metadata?.invalidateData()
|
||||
liveSet?.innerMetadata?.invalidateData()
|
||||
}
|
||||
|
||||
fun isFollowing(user: User): Boolean {
|
||||
@ -382,54 +382,70 @@ class User(val pubkeyHex: String) {
|
||||
|
||||
@Stable
|
||||
class UserLiveSet(u: User) {
|
||||
// UI Observers line up here.
|
||||
val follows: UserLiveData = UserLiveData(u)
|
||||
val followers: UserLiveData = UserLiveData(u)
|
||||
val reports: UserLiveData = UserLiveData(u)
|
||||
val messages: UserLiveData = UserLiveData(u)
|
||||
val relays: UserLiveData = UserLiveData(u)
|
||||
val relayInfo: UserLiveData = UserLiveData(u)
|
||||
val metadata: UserLiveData = UserLiveData(u)
|
||||
val zaps: UserLiveData = UserLiveData(u)
|
||||
val bookmarks: UserLiveData = UserLiveData(u)
|
||||
val statuses: UserLiveData = UserLiveData(u)
|
||||
val innerMetadata = UserBundledRefresherLiveData(u)
|
||||
|
||||
val profilePictureChanges = metadata.map {
|
||||
// UI Observers line up here.
|
||||
val innerFollows = UserBundledRefresherLiveData(u)
|
||||
val innerFollowers = UserBundledRefresherLiveData(u)
|
||||
val innerReports = UserBundledRefresherLiveData(u)
|
||||
val innerMessages = UserBundledRefresherLiveData(u)
|
||||
val innerRelays = UserBundledRefresherLiveData(u)
|
||||
val innerRelayInfo = UserBundledRefresherLiveData(u)
|
||||
val innerZaps = UserBundledRefresherLiveData(u)
|
||||
val innerBookmarks = UserBundledRefresherLiveData(u)
|
||||
val innerStatuses = UserBundledRefresherLiveData(u)
|
||||
|
||||
// UI Observers line up here.
|
||||
val metadata = innerMetadata.map { it }
|
||||
val follows = innerFollows.map { it }
|
||||
val followers = innerFollowers.map { it }
|
||||
val reports = innerReports.map { it }
|
||||
val messages = innerMessages.map { it }
|
||||
val relays = innerRelays.map { it }
|
||||
val relayInfo = innerRelayInfo.map { it }
|
||||
val zaps = innerZaps.map { it }
|
||||
val bookmarks = innerBookmarks.map { it }
|
||||
val statuses = innerStatuses.map { it }
|
||||
|
||||
val profilePictureChanges = innerMetadata.map {
|
||||
it.user.profilePicture()
|
||||
}.distinctUntilChanged()
|
||||
|
||||
val nip05Changes = metadata.map {
|
||||
val nip05Changes = innerMetadata.map {
|
||||
it.user.nip05()
|
||||
}.distinctUntilChanged()
|
||||
|
||||
val userMetadataInfo = metadata.map {
|
||||
val userMetadataInfo = innerMetadata.map {
|
||||
it.user.info
|
||||
}.distinctUntilChanged()
|
||||
|
||||
fun isInUse(): Boolean {
|
||||
return follows.hasObservers() ||
|
||||
return metadata.hasObservers() ||
|
||||
follows.hasObservers() ||
|
||||
followers.hasObservers() ||
|
||||
reports.hasObservers() ||
|
||||
messages.hasObservers() ||
|
||||
relays.hasObservers() ||
|
||||
relayInfo.hasObservers() ||
|
||||
metadata.hasObservers() ||
|
||||
zaps.hasObservers() ||
|
||||
bookmarks.hasObservers() ||
|
||||
statuses.hasObservers() ||
|
||||
bookmarks.hasObservers()
|
||||
profilePictureChanges.hasObservers() ||
|
||||
nip05Changes.hasObservers() ||
|
||||
userMetadataInfo.hasObservers()
|
||||
}
|
||||
|
||||
fun destroy() {
|
||||
follows.destroy()
|
||||
followers.destroy()
|
||||
reports.destroy()
|
||||
messages.destroy()
|
||||
relays.destroy()
|
||||
relayInfo.destroy()
|
||||
metadata.destroy()
|
||||
zaps.destroy()
|
||||
bookmarks.destroy()
|
||||
statuses.destroy()
|
||||
innerMetadata.destroy()
|
||||
innerFollows.destroy()
|
||||
innerFollowers.destroy()
|
||||
innerReports.destroy()
|
||||
innerMessages.destroy()
|
||||
innerRelays.destroy()
|
||||
innerRelayInfo.destroy()
|
||||
innerZaps.destroy()
|
||||
innerBookmarks.destroy()
|
||||
innerStatuses.destroy()
|
||||
}
|
||||
}
|
||||
|
||||
@ -440,64 +456,7 @@ data class RelayInfo(
|
||||
var counter: Long
|
||||
)
|
||||
|
||||
@Stable
|
||||
class Chatroom() {
|
||||
var roomMessages: Set<Note> = setOf()
|
||||
var subject: String? = null
|
||||
var subjectCreatedAt: Long? = null
|
||||
|
||||
@Synchronized
|
||||
fun addMessageSync(msg: Note) {
|
||||
checkNotInMainThread()
|
||||
|
||||
if (msg !in roomMessages) {
|
||||
roomMessages = roomMessages + msg
|
||||
|
||||
val newSubject = msg.event?.subject()
|
||||
|
||||
if (newSubject != null && (msg.createdAt() ?: 0) > (subjectCreatedAt ?: 0)) {
|
||||
subject = newSubject
|
||||
subjectCreatedAt = msg.createdAt()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Synchronized
|
||||
fun removeMessageSync(msg: Note) {
|
||||
checkNotInMainThread()
|
||||
|
||||
if (msg !in roomMessages) {
|
||||
roomMessages = roomMessages + msg
|
||||
|
||||
roomMessages.filter { it.event?.subject() != null }.sortedBy { it.createdAt() }.lastOrNull()?.let {
|
||||
subject = it.event?.subject()
|
||||
subjectCreatedAt = it.createdAt()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun senderIntersects(keySet: Set<HexKey>): Boolean {
|
||||
return roomMessages.any { it.author?.pubkeyHex in keySet }
|
||||
}
|
||||
|
||||
fun pruneMessagesToTheLatestOnly(): Set<Note> {
|
||||
val sorted = roomMessages.sortedWith(compareBy({ it.createdAt() }, { it.idHex })).reversed()
|
||||
|
||||
val toKeep = if ((sorted.firstOrNull()?.createdAt() ?: 0) > TimeUtils.oneWeekAgo()) {
|
||||
// Recent messages, keep last 100
|
||||
sorted.take(100).toSet()
|
||||
} else {
|
||||
// Old messages, keep the last one.
|
||||
sorted.take(1).toSet()
|
||||
} + sorted.filter { it.liveSet?.isInUse() ?: false }
|
||||
|
||||
val toRemove = roomMessages.minus(toKeep)
|
||||
roomMessages = toKeep
|
||||
return toRemove
|
||||
}
|
||||
}
|
||||
|
||||
class UserLiveData(val user: User) : LiveData<UserState>(UserState(user)) {
|
||||
class UserBundledRefresherLiveData(val user: User) : LiveData<UserState>(UserState(user)) {
|
||||
// Refreshes observers in batches.
|
||||
private val bundler = BundledUpdate(500, Dispatchers.IO)
|
||||
|
||||
@ -518,6 +477,17 @@ class UserLiveData(val user: User) : LiveData<UserState>(UserState(user)) {
|
||||
}
|
||||
}
|
||||
|
||||
fun <Y> map(
|
||||
transform: (UserState) -> Y
|
||||
): UserLoadingLiveData<Y> {
|
||||
val initialValue = this.value?.let { transform(it) }
|
||||
val result = UserLoadingLiveData(user, initialValue)
|
||||
result.addSource(this) { x -> result.value = transform(x) }
|
||||
return result
|
||||
}
|
||||
}
|
||||
|
||||
class UserLoadingLiveData<Y>(val user: User, initialValue: Y?) : MediatorLiveData<Y>(initialValue) {
|
||||
override fun onActive() {
|
||||
super.onActive()
|
||||
NostrSingleUserDataSource.add(user)
|
||||
|
@ -111,12 +111,14 @@ abstract class NostrDataSource(val debugName: String) {
|
||||
private val bundler = BundledUpdate(300, Dispatchers.IO)
|
||||
|
||||
fun invalidateFilters() {
|
||||
bundler.invalidate() {
|
||||
// println("DataSource: ${this.javaClass.simpleName} InvalidateFilters")
|
||||
scope.launch(Dispatchers.IO) {
|
||||
bundler.invalidate() {
|
||||
// println("DataSource: ${this.javaClass.simpleName} InvalidateFilters")
|
||||
|
||||
// adds the time to perform the refresh into this delay
|
||||
// holding off new updates in case of heavy refresh routines.
|
||||
resetFiltersSuspend()
|
||||
// adds the time to perform the refresh into this delay
|
||||
// holding off new updates in case of heavy refresh routines.
|
||||
resetFiltersSuspend()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -18,53 +18,66 @@ object NostrDiscoveryDataSource : NostrDataSource("DiscoveryFeed") {
|
||||
|
||||
val latestEOSEs = EOSEAccount()
|
||||
|
||||
fun createLiveStreamFilter(): TypedFilter {
|
||||
val follows = account.selectedUsersFollowList(account.defaultDiscoveryFollowList)
|
||||
fun createLiveStreamFilter(): List<TypedFilter> {
|
||||
val follows = account.selectedUsersFollowList(account.defaultDiscoveryFollowList)?.toList()
|
||||
|
||||
val followKeys = follows?.map {
|
||||
it.substring(0, 8)
|
||||
}
|
||||
|
||||
return TypedFilter(
|
||||
types = setOf(FeedType.GLOBAL),
|
||||
filter = JsonFilter(
|
||||
authors = followKeys,
|
||||
kinds = listOf(LiveActivitiesChatMessageEvent.kind, LiveActivitiesEvent.kind),
|
||||
limit = 300,
|
||||
since = latestEOSEs.users[account.userProfile()]?.followList?.get(account.defaultDiscoveryFollowList)?.relayList
|
||||
)
|
||||
return listOfNotNull(
|
||||
TypedFilter(
|
||||
types = setOf(FeedType.GLOBAL),
|
||||
filter = JsonFilter(
|
||||
authors = follows,
|
||||
kinds = listOf(LiveActivitiesChatMessageEvent.kind, LiveActivitiesEvent.kind),
|
||||
limit = 300,
|
||||
since = latestEOSEs.users[account.userProfile()]?.followList?.get(account.defaultDiscoveryFollowList)?.relayList
|
||||
)
|
||||
),
|
||||
follows?.let {
|
||||
TypedFilter(
|
||||
types = setOf(FeedType.GLOBAL),
|
||||
filter = JsonFilter(
|
||||
tags = mapOf("p" to it),
|
||||
kinds = listOf(LiveActivitiesEvent.kind),
|
||||
limit = 100,
|
||||
since = latestEOSEs.users[account.userProfile()]?.followList?.get(account.defaultDiscoveryFollowList)?.relayList
|
||||
)
|
||||
)
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
fun createPublicChatFilter(): TypedFilter {
|
||||
val follows = account.selectedUsersFollowList(account.defaultDiscoveryFollowList)
|
||||
fun createPublicChatFilter(): List<TypedFilter> {
|
||||
val follows = account.selectedUsersFollowList(account.defaultDiscoveryFollowList)?.toList()
|
||||
val followChats = account.selectedChatsFollowList().toList()
|
||||
|
||||
val followKeys = follows?.map {
|
||||
it.substring(0, 8)
|
||||
}
|
||||
|
||||
return TypedFilter(
|
||||
types = setOf(FeedType.PUBLIC_CHATS),
|
||||
filter = JsonFilter(
|
||||
authors = followKeys,
|
||||
kinds = listOf(ChannelCreateEvent.kind, ChannelMetadataEvent.kind, ChannelMessageEvent.kind),
|
||||
limit = 300,
|
||||
since = latestEOSEs.users[account.userProfile()]?.followList?.get(account.defaultDiscoveryFollowList)?.relayList
|
||||
return listOf(
|
||||
TypedFilter(
|
||||
types = setOf(FeedType.PUBLIC_CHATS),
|
||||
filter = JsonFilter(
|
||||
authors = follows,
|
||||
kinds = listOf(ChannelCreateEvent.kind, ChannelMetadataEvent.kind, ChannelMessageEvent.kind),
|
||||
limit = 300,
|
||||
since = latestEOSEs.users[account.userProfile()]?.followList?.get(account.defaultDiscoveryFollowList)?.relayList
|
||||
)
|
||||
),
|
||||
TypedFilter(
|
||||
types = setOf(FeedType.PUBLIC_CHATS),
|
||||
filter = JsonFilter(
|
||||
ids = followChats,
|
||||
kinds = listOf(ChannelCreateEvent.kind),
|
||||
limit = 300,
|
||||
since = latestEOSEs.users[account.userProfile()]?.followList?.get(account.defaultDiscoveryFollowList)?.relayList
|
||||
)
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
fun createCommunitiesFilter(): TypedFilter {
|
||||
val follows = account.selectedUsersFollowList(account.defaultDiscoveryFollowList)
|
||||
|
||||
val followKeys = follows?.map {
|
||||
it.substring(0, 8)
|
||||
}
|
||||
val follows = account.selectedUsersFollowList(account.defaultDiscoveryFollowList)?.toList()
|
||||
|
||||
return TypedFilter(
|
||||
types = setOf(FeedType.GLOBAL),
|
||||
filter = JsonFilter(
|
||||
authors = followKeys,
|
||||
authors = follows,
|
||||
kinds = listOf(CommunityDefinitionEvent.kind, CommunityPostApprovalEvent.kind),
|
||||
limit = 300,
|
||||
since = latestEOSEs.users[account.userProfile()]?.followList?.get(account.defaultDiscoveryFollowList)?.relayList
|
||||
@ -197,16 +210,16 @@ object NostrDiscoveryDataSource : NostrDataSource("DiscoveryFeed") {
|
||||
}
|
||||
|
||||
override fun updateChannelFilters() {
|
||||
discoveryFeedChannel.typedFilters = listOfNotNull(
|
||||
createLiveStreamFilter(),
|
||||
createPublicChatFilter(),
|
||||
createCommunitiesFilter(),
|
||||
createLiveStreamTagsFilter(),
|
||||
createPublicChatsTagsFilter(),
|
||||
createCommunitiesTagsFilter(),
|
||||
createCommunitiesGeohashesFilter(),
|
||||
createPublicChatsGeohashesFilter(),
|
||||
createLiveStreamGeohashesFilter()
|
||||
discoveryFeedChannel.typedFilters = createLiveStreamFilter().plus(createPublicChatFilter()).plus(
|
||||
listOfNotNull(
|
||||
createLiveStreamTagsFilter(),
|
||||
createLiveStreamGeohashesFilter(),
|
||||
createCommunitiesFilter(),
|
||||
createPublicChatsTagsFilter(),
|
||||
createCommunitiesTagsFilter(),
|
||||
createCommunitiesGeohashesFilter(),
|
||||
createPublicChatsGeohashesFilter()
|
||||
)
|
||||
).ifEmpty { null }
|
||||
}
|
||||
}
|
||||
|
@ -54,13 +54,8 @@ object NostrHomeDataSource : NostrDataSource("HomeFeed") {
|
||||
}
|
||||
|
||||
fun createFollowAccountsFilter(): TypedFilter {
|
||||
val follows = account.selectedUsersFollowList(account.defaultHomeFollowList)
|
||||
|
||||
val followKeys = follows?.map {
|
||||
it.substring(0, 8)
|
||||
}
|
||||
|
||||
val followSet = followKeys?.plus(account.userProfile().pubkeyHex.substring(0, 8))
|
||||
val follows = account.selectedUsersFollowList(account.defaultHomeFollowList) ?: emptySet()
|
||||
val followSet = follows.plus(account.userProfile().pubkeyHex).toList()
|
||||
|
||||
return TypedFilter(
|
||||
types = setOf(FeedType.FOLLOWS),
|
||||
|
@ -1,7 +1,7 @@
|
||||
package com.vitorpamplona.amethyst.service
|
||||
|
||||
import com.vitorpamplona.amethyst.model.Channel
|
||||
import com.vitorpamplona.amethyst.model.LiveActivitiesChannel
|
||||
import com.vitorpamplona.amethyst.model.LocalCache
|
||||
import com.vitorpamplona.amethyst.model.PublicChatChannel
|
||||
import com.vitorpamplona.amethyst.service.relays.COMMON_FEED_TYPES
|
||||
import com.vitorpamplona.amethyst.service.relays.FeedType
|
||||
@ -11,10 +11,10 @@ import com.vitorpamplona.quartz.events.ChannelCreateEvent
|
||||
import com.vitorpamplona.quartz.events.ChannelMetadataEvent
|
||||
|
||||
object NostrSingleChannelDataSource : NostrDataSource("SingleChannelFeed") {
|
||||
private var channelsToWatch = setOf<String>()
|
||||
private var channelsToWatch = setOf<Channel>()
|
||||
|
||||
private fun createRepliesAndReactionsFilter(): TypedFilter? {
|
||||
val reactionsToWatch = channelsToWatch.map { it }
|
||||
private fun createMetadataChangeFilter(): TypedFilter? {
|
||||
val reactionsToWatch = channelsToWatch.filter { it is PublicChatChannel }.map { it.idHex }
|
||||
|
||||
if (reactionsToWatch.isEmpty()) {
|
||||
return null
|
||||
@ -32,7 +32,6 @@ object NostrSingleChannelDataSource : NostrDataSource("SingleChannelFeed") {
|
||||
|
||||
fun createLoadEventsIfNotLoadedFilter(): TypedFilter? {
|
||||
val directEventsToLoad = channelsToWatch
|
||||
.mapNotNull { LocalCache.checkGetOrCreateChannel(it) }
|
||||
.filter { it.notes.isEmpty() && it is PublicChatChannel }
|
||||
|
||||
val interestedEvents = (directEventsToLoad).map { it.idHex }.toSet()
|
||||
@ -53,7 +52,6 @@ object NostrSingleChannelDataSource : NostrDataSource("SingleChannelFeed") {
|
||||
|
||||
fun createLoadStreamingIfNotLoadedFilter(): List<TypedFilter>? {
|
||||
val directEventsToLoad = channelsToWatch
|
||||
.mapNotNull { LocalCache.checkGetOrCreateChannel(it) }
|
||||
.filterIsInstance<LiveActivitiesChannel>()
|
||||
.filter { it.info == null }
|
||||
|
||||
@ -81,7 +79,7 @@ object NostrSingleChannelDataSource : NostrDataSource("SingleChannelFeed") {
|
||||
val singleChannelChannel = requestNewChannel()
|
||||
|
||||
override fun updateChannelFilters() {
|
||||
val reactions = createRepliesAndReactionsFilter()
|
||||
val reactions = createMetadataChangeFilter()
|
||||
val missing = createLoadEventsIfNotLoadedFilter()
|
||||
val missingStreaming = createLoadStreamingIfNotLoadedFilter()
|
||||
|
||||
@ -90,13 +88,17 @@ object NostrSingleChannelDataSource : NostrDataSource("SingleChannelFeed") {
|
||||
).ifEmpty { null }
|
||||
}
|
||||
|
||||
fun add(eventId: String) {
|
||||
channelsToWatch = channelsToWatch.plus(eventId)
|
||||
invalidateFilters()
|
||||
fun add(eventId: Channel) {
|
||||
if (eventId !in channelsToWatch) {
|
||||
channelsToWatch = channelsToWatch.plus(eventId)
|
||||
invalidateFilters()
|
||||
}
|
||||
}
|
||||
|
||||
fun remove(eventId: String) {
|
||||
channelsToWatch = channelsToWatch.minus(eventId)
|
||||
invalidateFilters()
|
||||
fun remove(eventId: Channel) {
|
||||
if (eventId in channelsToWatch) {
|
||||
channelsToWatch = channelsToWatch.minus(eventId)
|
||||
invalidateFilters()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -12,49 +12,60 @@ import com.vitorpamplona.quartz.events.StatusEvent
|
||||
object NostrSingleUserDataSource : NostrDataSource("SingleUserFeed") {
|
||||
var usersToWatch = setOf<User>()
|
||||
|
||||
fun createUserFilter(): List<TypedFilter>? {
|
||||
fun createUserMetadataFilter(): List<TypedFilter>? {
|
||||
if (usersToWatch.isEmpty()) return null
|
||||
|
||||
return usersToWatch.filter { it.info?.latestMetadata == null }.map {
|
||||
val firstTimers = usersToWatch.filter { it.info?.latestMetadata == null }.map { it.pubkeyHex }
|
||||
|
||||
return listOf(
|
||||
TypedFilter(
|
||||
types = COMMON_FEED_TYPES,
|
||||
filter = JsonFilter(
|
||||
kinds = listOf(MetadataEvent.kind),
|
||||
authors = listOf(it.pubkeyHex),
|
||||
limit = 1
|
||||
kinds = listOf(MetadataEvent.kind, StatusEvent.kind),
|
||||
authors = firstTimers,
|
||||
limit = 10 * firstTimers.size
|
||||
)
|
||||
)
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
fun createUserStatusFilter(): List<TypedFilter>? {
|
||||
fun createUserMetadataFilter(minLatestEOSEs: Map<String, EOSETime>): TypedFilter? {
|
||||
if (usersToWatch.isEmpty()) return null
|
||||
|
||||
return usersToWatch.map {
|
||||
TypedFilter(
|
||||
types = COMMON_FEED_TYPES,
|
||||
filter = JsonFilter(
|
||||
kinds = listOf(StatusEvent.kind),
|
||||
authors = listOf(it.pubkeyHex),
|
||||
since = it.latestEOSEs
|
||||
)
|
||||
return TypedFilter(
|
||||
types = COMMON_FEED_TYPES,
|
||||
filter = JsonFilter(
|
||||
kinds = listOf(StatusEvent.kind),
|
||||
authors = usersToWatch.map { it.pubkeyHex },
|
||||
since = minLatestEOSEs
|
||||
)
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
fun createUserReportFilter(): List<TypedFilter>? {
|
||||
fun createUserStatusFilter(minLatestEOSEs: Map<String, EOSETime>): TypedFilter? {
|
||||
if (usersToWatch.isEmpty()) return null
|
||||
|
||||
return usersToWatch.map {
|
||||
TypedFilter(
|
||||
types = COMMON_FEED_TYPES,
|
||||
filter = JsonFilter(
|
||||
kinds = listOf(ReportEvent.kind),
|
||||
tags = mapOf("p" to listOf(it.pubkeyHex)),
|
||||
since = it.latestEOSEs
|
||||
)
|
||||
return TypedFilter(
|
||||
types = COMMON_FEED_TYPES,
|
||||
filter = JsonFilter(
|
||||
kinds = listOf(StatusEvent.kind),
|
||||
authors = usersToWatch.map { it.pubkeyHex },
|
||||
since = minLatestEOSEs
|
||||
)
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
fun createUserReportFilter(minLatestEOSEs: Map<String, EOSETime>): TypedFilter? {
|
||||
if (usersToWatch.isEmpty()) return null
|
||||
|
||||
return TypedFilter(
|
||||
types = COMMON_FEED_TYPES,
|
||||
filter = JsonFilter(
|
||||
kinds = listOf(ReportEvent.kind),
|
||||
tags = mapOf("p" to usersToWatch.map { it.pubkeyHex }),
|
||||
since = minLatestEOSEs
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
val userChannel = requestNewChannel() { time, relayUrl ->
|
||||
@ -66,17 +77,40 @@ object NostrSingleUserDataSource : NostrDataSource("SingleUserFeed") {
|
||||
eose.time = time
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
val userChannelFirstTimers = requestNewChannel() { time, relayUrl ->
|
||||
// Many relays operate with limits in the amount of filters.
|
||||
// As information comes, the filters will be rotated to get more data.
|
||||
invalidateFilters()
|
||||
}
|
||||
|
||||
val userChannelOnce = requestNewChannel()
|
||||
|
||||
override fun updateChannelFilters() {
|
||||
userChannel.typedFilters = listOfNotNull(createUserReportFilter(), createUserStatusFilter()).flatten().ifEmpty { null }
|
||||
userChannelOnce.typedFilters = listOfNotNull(createUserFilter()).flatten().ifEmpty { null }
|
||||
val minLatestEOSEs = mutableMapOf<String, EOSETime>()
|
||||
val neverGottenAnEOSE = mutableSetOf<String>()
|
||||
usersToWatch.forEach {
|
||||
if (it.latestEOSEs.isEmpty()) { // first time
|
||||
neverGottenAnEOSE.add(it.pubkeyHex)
|
||||
} else {
|
||||
it.latestEOSEs.forEach {
|
||||
val minEose = minLatestEOSEs[it.key]
|
||||
if (minEose == null) {
|
||||
minLatestEOSEs.put(it.key, EOSETime(it.value.time))
|
||||
} else if (it.value.time < minEose.time) {
|
||||
minEose.time = it.value.time
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
userChannel.typedFilters = listOfNotNull(
|
||||
createUserMetadataFilter(minLatestEOSEs),
|
||||
createUserStatusFilter(minLatestEOSEs),
|
||||
createUserReportFilter(minLatestEOSEs)
|
||||
).ifEmpty { null }
|
||||
userChannelFirstTimers.typedFilters = listOfNotNull(
|
||||
createUserMetadataFilter()
|
||||
).flatten().ifEmpty { null }
|
||||
}
|
||||
|
||||
fun add(user: User) {
|
||||
|
@ -36,8 +36,10 @@ object NostrThreadDataSource : NostrDataSource("SingleThreadFeed") {
|
||||
}
|
||||
|
||||
fun loadThread(noteId: String?) {
|
||||
eventToWatch = noteId
|
||||
if (eventToWatch != noteId) {
|
||||
eventToWatch = noteId
|
||||
|
||||
invalidateFilters()
|
||||
invalidateFilters()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -14,16 +14,12 @@ object NostrVideoDataSource : NostrDataSource("VideoFeed") {
|
||||
val latestEOSEs = EOSEAccount()
|
||||
|
||||
fun createContextualFilter(): TypedFilter? {
|
||||
val follows = account.selectedUsersFollowList(account.defaultStoriesFollowList)
|
||||
|
||||
val followKeys = follows?.map {
|
||||
it.substring(0, 6)
|
||||
}
|
||||
val follows = account.selectedUsersFollowList(account.defaultStoriesFollowList)?.toList()
|
||||
|
||||
return TypedFilter(
|
||||
types = setOf(FeedType.GLOBAL),
|
||||
filter = JsonFilter(
|
||||
authors = followKeys,
|
||||
authors = follows,
|
||||
kinds = listOf(FileHeaderEvent.kind, FileStorageHeaderEvent.kind),
|
||||
limit = 200,
|
||||
since = latestEOSEs.users[account.userProfile()]?.followList?.get(account.defaultStoriesFollowList)?.relayList
|
||||
|
@ -57,7 +57,7 @@ class LightningAddressResolver() {
|
||||
if (it.isSuccessful) {
|
||||
onSuccess(it.body.string())
|
||||
} else {
|
||||
onError("Could not resolve $lnaddress. Error: ${it.code}. Check if the server up and if the lightning address $lnaddress is correct")
|
||||
onError("The receiver's lightning service at $url is not available. It was calculated from the lightning address \"${lnaddress}\". Error: ${it.code}. Check if the server up and if the lightning address is correct")
|
||||
}
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
|
@ -23,7 +23,7 @@ import kotlinx.collections.immutable.persistentSetOf
|
||||
|
||||
class EventNotificationConsumer(private val applicationContext: Context) {
|
||||
|
||||
fun consume(event: Event) {
|
||||
suspend fun consume(event: Event) {
|
||||
if (LocalCache.notes[event.id] == null) {
|
||||
if (LocalCache.justVerify(event)) {
|
||||
LocalCache.justConsume(event, null)
|
||||
@ -40,7 +40,7 @@ class EventNotificationConsumer(private val applicationContext: Context) {
|
||||
}
|
||||
}
|
||||
|
||||
fun unwrapAndConsume(event: Event, account: Account): Event? {
|
||||
suspend fun unwrapAndConsume(event: Event, account: Account): Event? {
|
||||
if (!LocalCache.justVerify(event)) return null
|
||||
|
||||
return when (event) {
|
||||
@ -65,7 +65,7 @@ class EventNotificationConsumer(private val applicationContext: Context) {
|
||||
}
|
||||
}
|
||||
|
||||
private fun unwrapAndNotify(giftWrap: GiftWrapEvent) {
|
||||
private suspend fun unwrapAndNotify(giftWrap: GiftWrapEvent) {
|
||||
val giftWrapNote = LocalCache.notes[giftWrap.id] ?: return
|
||||
|
||||
LocalPreferences.allSavedAccounts().forEach {
|
||||
|
@ -60,7 +60,7 @@ class JsonFilter(
|
||||
} else {
|
||||
val jsonObjectSince = factory.objectNode()
|
||||
entries.forEach { sincePairs ->
|
||||
put(sincePairs.key, "${sincePairs.value}")
|
||||
jsonObjectSince.put(sincePairs.key, "${sincePairs.value}")
|
||||
}
|
||||
put("since", jsonObjectSince)
|
||||
}
|
||||
|
@ -186,6 +186,7 @@ class MainActivity : AppCompatActivity() {
|
||||
}
|
||||
|
||||
override fun onPause() {
|
||||
ServiceManager.cleanObservers()
|
||||
// if (BuildConfig.DEBUG) {
|
||||
debugState(this)
|
||||
// }
|
||||
@ -212,7 +213,7 @@ class MainActivity : AppCompatActivity() {
|
||||
super.onTrimMemory(level)
|
||||
println("Trim Memory $level")
|
||||
GlobalScope.launch(Dispatchers.Default) {
|
||||
ServiceManager.cleanUp()
|
||||
ServiceManager.trimMemory()
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1,9 +1,11 @@
|
||||
package com.vitorpamplona.amethyst.ui.actions
|
||||
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.width
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.text.KeyboardOptions
|
||||
import androidx.compose.material.MaterialTheme
|
||||
import androidx.compose.material.OutlinedTextField
|
||||
@ -11,8 +13,6 @@ import androidx.compose.material.Text
|
||||
import androidx.compose.material.TextFieldDefaults
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.saveable.rememberSaveable
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
@ -21,51 +21,11 @@ import androidx.compose.ui.text.input.KeyboardType
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.vitorpamplona.amethyst.R
|
||||
import com.vitorpamplona.amethyst.ui.theme.DoubleHorzSpacer
|
||||
import com.vitorpamplona.amethyst.ui.theme.placeholderText
|
||||
|
||||
@Composable
|
||||
fun NewPollVoteValueRange(pollViewModel: NewPostViewModel) {
|
||||
var textMax by rememberSaveable { mutableStateOf("") }
|
||||
var textMin by rememberSaveable { mutableStateOf("") }
|
||||
|
||||
// check for zapMax amounts < 1
|
||||
pollViewModel.isValidvalueMaximum.value = true
|
||||
if (textMax.isNotEmpty()) {
|
||||
try {
|
||||
val int = textMax.toInt()
|
||||
if (int < 1) {
|
||||
pollViewModel.isValidvalueMaximum.value = false
|
||||
} else { pollViewModel.valueMaximum = int }
|
||||
} catch (e: Exception) { pollViewModel.isValidvalueMaximum.value = false }
|
||||
}
|
||||
|
||||
// check for minZap amounts < 1
|
||||
pollViewModel.isValidvalueMinimum.value = true
|
||||
if (textMin.isNotEmpty()) {
|
||||
try {
|
||||
val int = textMin.toInt()
|
||||
if (int < 1) {
|
||||
pollViewModel.isValidvalueMinimum.value = false
|
||||
} else { pollViewModel.valueMinimum = int }
|
||||
} catch (e: Exception) { pollViewModel.isValidvalueMinimum.value = false }
|
||||
}
|
||||
|
||||
// check for zapMin > zapMax
|
||||
if (textMin.isNotEmpty() && textMax.isNotEmpty()) {
|
||||
try {
|
||||
val intMin = textMin.toInt()
|
||||
val intMax = textMax.toInt()
|
||||
|
||||
if (intMin > intMax) {
|
||||
pollViewModel.isValidvalueMinimum.value = false
|
||||
pollViewModel.isValidvalueMaximum.value = false
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
pollViewModel.isValidvalueMinimum.value = false
|
||||
pollViewModel.isValidvalueMaximum.value = false
|
||||
}
|
||||
}
|
||||
|
||||
val colorInValid = TextFieldDefaults.outlinedTextFieldColors(
|
||||
focusedBorderColor = MaterialTheme.colors.error,
|
||||
unfocusedBorderColor = Color.Red
|
||||
@ -80,10 +40,10 @@ fun NewPollVoteValueRange(pollViewModel: NewPostViewModel) {
|
||||
horizontalArrangement = Arrangement.Center
|
||||
) {
|
||||
OutlinedTextField(
|
||||
value = textMin,
|
||||
onValueChange = { textMin = it },
|
||||
value = pollViewModel.valueMinimum?.toString() ?: "",
|
||||
onValueChange = { pollViewModel.updateMinZapAmountForPoll(it) },
|
||||
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number),
|
||||
modifier = Modifier.width(150.dp),
|
||||
modifier = Modifier.weight(1f),
|
||||
colors = if (pollViewModel.isValidvalueMinimum.value) colorValid else colorInValid,
|
||||
label = {
|
||||
Text(
|
||||
@ -98,11 +58,14 @@ fun NewPollVoteValueRange(pollViewModel: NewPostViewModel) {
|
||||
)
|
||||
}
|
||||
)
|
||||
|
||||
Spacer(modifier = DoubleHorzSpacer)
|
||||
|
||||
OutlinedTextField(
|
||||
value = textMax,
|
||||
onValueChange = { textMax = it },
|
||||
value = pollViewModel.valueMaximum?.toString() ?: "",
|
||||
onValueChange = { pollViewModel.updateMaxZapAmountForPoll(it) },
|
||||
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number),
|
||||
modifier = Modifier.width(150.dp),
|
||||
modifier = Modifier.weight(1f),
|
||||
colors = if (pollViewModel.isValidvalueMaximum.value) colorValid else colorInValid,
|
||||
label = {
|
||||
Text(
|
||||
@ -118,10 +81,25 @@ fun NewPollVoteValueRange(pollViewModel: NewPostViewModel) {
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
Row(
|
||||
Modifier.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.Center
|
||||
) {
|
||||
Text(
|
||||
text = stringResource(R.string.poll_zap_value_min_max_explainer),
|
||||
color = MaterialTheme.colors.placeholderText,
|
||||
modifier = Modifier.padding(vertical = 10.dp)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Preview
|
||||
@Composable
|
||||
fun NewPollVoteValueRangePreview() {
|
||||
NewPollVoteValueRange(NewPostViewModel())
|
||||
Column(
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
) {
|
||||
NewPollVoteValueRange(NewPostViewModel())
|
||||
}
|
||||
}
|
||||
|
@ -542,6 +542,8 @@ private fun PollField(postViewModel: NewPostViewModel) {
|
||||
NewPollOption(postViewModel, index)
|
||||
}
|
||||
|
||||
NewPollVoteValueRange(postViewModel)
|
||||
|
||||
Button(
|
||||
onClick = {
|
||||
postViewModel.pollOptions[postViewModel.pollOptions.size] =
|
||||
|
@ -74,8 +74,8 @@ open class NewPostViewModel() : ViewModel() {
|
||||
var wantsPoll by mutableStateOf(false)
|
||||
var zapRecipients = mutableStateListOf<HexKey>()
|
||||
var pollOptions = newStateMapPollOptions()
|
||||
var valueMaximum: Int? = null
|
||||
var valueMinimum: Int? = null
|
||||
var valueMaximum by mutableStateOf<Int?>(null)
|
||||
var valueMinimum by mutableStateOf<Int?>(null)
|
||||
var consensusThreshold: Int? = null
|
||||
var closedAt: Int? = null
|
||||
|
||||
@ -503,7 +503,7 @@ open class NewPostViewModel() : ViewModel() {
|
||||
return message.text.isNotBlank() && !isUploadingImage && !wantsInvoice &&
|
||||
(!wantsZapraiser || zapRaiserAmount != null) &&
|
||||
(!wantsDirectMessage || !toUsers.text.isNullOrBlank()) &&
|
||||
(!wantsPoll || pollOptions.values.all { it.isNotEmpty() }) &&
|
||||
(!wantsPoll || (pollOptions.values.all { it.isNotEmpty() } && isValidvalueMinimum.value && isValidvalueMaximum.value)) &&
|
||||
contentToAddUrl == null
|
||||
}
|
||||
|
||||
@ -621,6 +621,50 @@ open class NewPostViewModel() : ViewModel() {
|
||||
nip24 = !nip24
|
||||
}
|
||||
}
|
||||
|
||||
fun updateMinZapAmountForPoll(textMin: String) {
|
||||
if (textMin.isNotEmpty()) {
|
||||
try {
|
||||
val int = textMin.toInt()
|
||||
if (int < 1) {
|
||||
valueMinimum = null
|
||||
} else {
|
||||
valueMinimum = int
|
||||
}
|
||||
} catch (e: Exception) {}
|
||||
} else {
|
||||
valueMinimum = null
|
||||
}
|
||||
|
||||
checkMinMax()
|
||||
}
|
||||
|
||||
fun updateMaxZapAmountForPoll(textMax: String) {
|
||||
if (textMax.isNotEmpty()) {
|
||||
try {
|
||||
val int = textMax.toInt()
|
||||
if (int < 1) {
|
||||
valueMaximum = null
|
||||
} else {
|
||||
valueMaximum = int
|
||||
}
|
||||
} catch (e: Exception) {}
|
||||
} else {
|
||||
valueMaximum = null
|
||||
}
|
||||
|
||||
checkMinMax()
|
||||
}
|
||||
|
||||
fun checkMinMax() {
|
||||
if ((valueMinimum ?: 0) > (valueMaximum ?: Int.MAX_VALUE)) {
|
||||
isValidvalueMinimum.value = false
|
||||
isValidvalueMaximum.value = false
|
||||
} else {
|
||||
isValidvalueMinimum.value = true
|
||||
isValidvalueMaximum.value = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
enum class GeohashPrecision(val digits: Int) {
|
||||
|
@ -2,6 +2,7 @@ package com.vitorpamplona.amethyst.ui.components
|
||||
|
||||
import android.content.Intent
|
||||
import android.net.Uri
|
||||
import android.widget.Toast
|
||||
import androidx.compose.animation.Crossfade
|
||||
import androidx.compose.foundation.text.ClickableText
|
||||
import androidx.compose.material.LocalTextStyle
|
||||
@ -12,6 +13,7 @@ import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.text.AnnotatedString
|
||||
import androidx.compose.ui.text.style.TextDirection
|
||||
import androidx.core.content.ContextCompat
|
||||
import com.vitorpamplona.amethyst.R
|
||||
import com.vitorpamplona.quartz.encoders.LnWithdrawalUtil
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
@ -41,10 +43,7 @@ fun MayBeWithdrawal(lnurlWord: String) {
|
||||
@Composable
|
||||
fun ClickableWithdrawal(withdrawalString: String) {
|
||||
val context = LocalContext.current
|
||||
|
||||
val uri = remember(withdrawalString) {
|
||||
Uri.parse("lightning:$withdrawalString")
|
||||
}
|
||||
val scope = rememberCoroutineScope()
|
||||
|
||||
val withdraw = remember(withdrawalString) {
|
||||
AnnotatedString("$withdrawalString ")
|
||||
@ -53,9 +52,18 @@ fun ClickableWithdrawal(withdrawalString: String) {
|
||||
ClickableText(
|
||||
text = withdraw,
|
||||
onClick = {
|
||||
runCatching {
|
||||
val intent = Intent(Intent.ACTION_VIEW, uri)
|
||||
try {
|
||||
val intent = Intent(Intent.ACTION_VIEW, Uri.parse("lightning:$withdrawalString"))
|
||||
intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK
|
||||
ContextCompat.startActivity(context, intent, null)
|
||||
} catch (e: Exception) {
|
||||
scope.launch {
|
||||
Toast.makeText(
|
||||
context,
|
||||
context.getString(R.string.lightning_wallets_not_found),
|
||||
Toast.LENGTH_LONG
|
||||
).show()
|
||||
}
|
||||
}
|
||||
},
|
||||
style = LocalTextStyle.current.copy(color = MaterialTheme.colors.primary)
|
||||
|
@ -2,6 +2,7 @@ package com.vitorpamplona.amethyst.ui.components
|
||||
|
||||
import android.content.Intent
|
||||
import android.net.Uri
|
||||
import android.widget.Toast
|
||||
import androidx.compose.animation.Crossfade
|
||||
import androidx.compose.foundation.border
|
||||
import androidx.compose.foundation.layout.Column
|
||||
@ -66,6 +67,7 @@ fun MayBeInvoicePreview(lnbcWord: String) {
|
||||
@Composable
|
||||
fun InvoicePreview(lnInvoice: String, amount: String?) {
|
||||
val context = LocalContext.current
|
||||
val scope = rememberCoroutineScope()
|
||||
|
||||
Column(
|
||||
modifier = Modifier
|
||||
@ -118,9 +120,18 @@ fun InvoicePreview(lnInvoice: String, amount: String?) {
|
||||
.fillMaxWidth()
|
||||
.padding(vertical = 10.dp),
|
||||
onClick = {
|
||||
runCatching {
|
||||
try {
|
||||
val intent = Intent(Intent.ACTION_VIEW, Uri.parse("lightning:$lnInvoice"))
|
||||
intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK
|
||||
startActivity(context, intent, null)
|
||||
} catch (e: Exception) {
|
||||
scope.launch {
|
||||
Toast.makeText(
|
||||
context,
|
||||
context.getString(R.string.lightning_wallets_not_found),
|
||||
Toast.LENGTH_LONG
|
||||
).show()
|
||||
}
|
||||
}
|
||||
},
|
||||
shape = QuoteBorder,
|
||||
|
@ -12,8 +12,6 @@ import com.vitorpamplona.amethyst.model.ConnectivityType
|
||||
import com.vitorpamplona.amethyst.model.UrlCachedPreviewer
|
||||
import com.vitorpamplona.amethyst.service.connectivitystatus.ConnectivityStatus
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
@Composable
|
||||
fun UrlPreview(url: String, urlText: String, accountViewModel: AccountViewModel) {
|
||||
@ -37,10 +35,8 @@ fun UrlPreview(url: String, urlText: String, accountViewModel: AccountViewModel)
|
||||
// Doesn't use a viewModel because of viewModel reusing issues (too many UrlPreview are created).
|
||||
if (urlPreviewState == UrlPreviewState.Loading) {
|
||||
LaunchedEffect(url) {
|
||||
launch(Dispatchers.IO) {
|
||||
UrlCachedPreviewer.previewInfo(url) {
|
||||
urlPreviewState = it
|
||||
}
|
||||
accountViewModel.urlPreview(url) {
|
||||
urlPreviewState = it
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -7,7 +7,6 @@ import com.vitorpamplona.amethyst.ui.actions.updated
|
||||
import com.vitorpamplona.quartz.events.ChannelMessageEvent
|
||||
import com.vitorpamplona.quartz.events.ChatroomKey
|
||||
import com.vitorpamplona.quartz.events.ChatroomKeyable
|
||||
import kotlin.time.ExperimentalTime
|
||||
import kotlin.time.measureTimedValue
|
||||
|
||||
class ChatroomListKnownFeedFilter(val account: Account) : AdditiveFeedFilter<Note>() {
|
||||
@ -50,7 +49,6 @@ class ChatroomListKnownFeedFilter(val account: Account) : AdditiveFeedFilter<Not
|
||||
.reversed()
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalTime::class)
|
||||
override fun updateListWith(oldList: List<Note>, newItems: Set<Note>): List<Note> {
|
||||
val (feed, elapsed) = measureTimedValue {
|
||||
val me = account.userProfile()
|
||||
|
@ -6,7 +6,6 @@ import com.vitorpamplona.amethyst.ui.actions.updated
|
||||
import com.vitorpamplona.quartz.events.ChatroomKey
|
||||
import com.vitorpamplona.quartz.events.ChatroomKeyable
|
||||
import com.vitorpamplona.quartz.events.PrivateDmEvent
|
||||
import kotlin.time.ExperimentalTime
|
||||
import kotlin.time.measureTimedValue
|
||||
|
||||
class ChatroomListNewFeedFilter(val account: Account) : AdditiveFeedFilter<Note>() {
|
||||
@ -38,7 +37,6 @@ class ChatroomListNewFeedFilter(val account: Account) : AdditiveFeedFilter<Note>
|
||||
.reversed()
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalTime::class)
|
||||
override fun updateListWith(oldList: List<Note>, newItems: Set<Note>): List<Note> {
|
||||
val (feed, elapsed) = measureTimedValue {
|
||||
val me = account.userProfile()
|
||||
|
@ -2,11 +2,9 @@ package com.vitorpamplona.amethyst.ui.dal
|
||||
|
||||
import android.util.Log
|
||||
import com.vitorpamplona.amethyst.service.checkNotInMainThread
|
||||
import kotlin.time.ExperimentalTime
|
||||
import kotlin.time.measureTimedValue
|
||||
|
||||
abstract class FeedFilter<T> {
|
||||
@OptIn(ExperimentalTime::class)
|
||||
fun loadTop(): List<T> {
|
||||
checkNotInMainThread()
|
||||
|
||||
@ -33,7 +31,6 @@ abstract class AdditiveFeedFilter<T> : FeedFilter<T>() {
|
||||
abstract fun applyFilter(collection: Set<T>): Set<T>
|
||||
abstract fun sort(collection: Set<T>): List<T>
|
||||
|
||||
@OptIn(ExperimentalTime::class)
|
||||
open fun updateListWith(oldList: List<T>, newItems: Set<T>): List<T> {
|
||||
checkNotInMainThread()
|
||||
|
||||
|
@ -6,6 +6,7 @@ import com.vitorpamplona.amethyst.model.Note
|
||||
import com.vitorpamplona.quartz.events.AudioHeaderEvent
|
||||
import com.vitorpamplona.quartz.events.ChannelMessageEvent
|
||||
import com.vitorpamplona.quartz.events.LongTextNoteEvent
|
||||
import com.vitorpamplona.quartz.events.PollNoteEvent
|
||||
import com.vitorpamplona.quartz.events.PrivateDmEvent
|
||||
import com.vitorpamplona.quartz.events.TextNoteEvent
|
||||
|
||||
@ -34,6 +35,7 @@ class GeoHashFeedFilter(val tag: String, val account: Account) : AdditiveFeedFil
|
||||
it.event is LongTextNoteEvent ||
|
||||
it.event is ChannelMessageEvent ||
|
||||
it.event is PrivateDmEvent ||
|
||||
it.event is PollNoteEvent ||
|
||||
it.event is AudioHeaderEvent
|
||||
) &&
|
||||
it.event?.isTaggedGeoHash(myTag) == true
|
||||
|
@ -6,6 +6,7 @@ import com.vitorpamplona.amethyst.model.Note
|
||||
import com.vitorpamplona.quartz.events.AudioHeaderEvent
|
||||
import com.vitorpamplona.quartz.events.ChannelMessageEvent
|
||||
import com.vitorpamplona.quartz.events.LongTextNoteEvent
|
||||
import com.vitorpamplona.quartz.events.PollNoteEvent
|
||||
import com.vitorpamplona.quartz.events.PrivateDmEvent
|
||||
import com.vitorpamplona.quartz.events.TextNoteEvent
|
||||
|
||||
@ -34,6 +35,7 @@ class HashtagFeedFilter(val tag: String, val account: Account) : AdditiveFeedFil
|
||||
it.event is LongTextNoteEvent ||
|
||||
it.event is ChannelMessageEvent ||
|
||||
it.event is PrivateDmEvent ||
|
||||
it.event is PollNoteEvent ||
|
||||
it.event is AudioHeaderEvent
|
||||
) &&
|
||||
it.event?.isTaggedHash(myTag) == true
|
||||
|
@ -3,6 +3,9 @@ package com.vitorpamplona.amethyst.ui.navigation
|
||||
import android.content.Context
|
||||
import android.content.ContextWrapper
|
||||
import android.content.Intent
|
||||
import androidx.compose.animation.core.tween
|
||||
import androidx.compose.animation.fadeIn
|
||||
import androidx.compose.animation.fadeOut
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.DisposableEffect
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
@ -80,7 +83,12 @@ fun AppNavigation(
|
||||
}
|
||||
}
|
||||
|
||||
NavHost(navController, startDestination = Route.Home.route) {
|
||||
NavHost(
|
||||
navController,
|
||||
startDestination = Route.Home.route,
|
||||
enterTransition = { fadeIn(animationSpec = tween(200)) },
|
||||
exitTransition = { fadeOut(animationSpec = tween(200)) }
|
||||
) {
|
||||
Route.Home.let { route ->
|
||||
composable(route.route, route.arguments, content = { it ->
|
||||
val nip47 = it.arguments?.getString("nip47")
|
||||
@ -215,6 +223,7 @@ fun AppNavigation(
|
||||
composable(route.route, route.arguments, content = {
|
||||
ChatroomScreen(
|
||||
roomId = it.arguments?.getString("id"),
|
||||
draftMessage = it.arguments?.getString("message"),
|
||||
accountViewModel = accountViewModel,
|
||||
nav = nav
|
||||
)
|
||||
|
@ -754,9 +754,9 @@ fun debugState(context: Context) {
|
||||
Log.d("STATE DUMP", "Image Disk Cache ${(imageLoader.diskCache?.size ?: 0) / (1024 * 1024)}/${(imageLoader.diskCache?.maxSize ?: 0) / (1024 * 1024)} MB")
|
||||
Log.d("STATE DUMP", "Image Memory Cache ${(imageLoader.memoryCache?.size ?: 0) / (1024 * 1024)}/${(imageLoader.memoryCache?.maxSize ?: 0) / (1024 * 1024)} MB")
|
||||
|
||||
Log.d("STATE DUMP", "Notes: " + LocalCache.notes.filter { it.value.liveSet != null }.size + " / " + LocalCache.notes.filter { it.value.event != null }.size + "/" + LocalCache.notes.size)
|
||||
Log.d("STATE DUMP", "Addressables: " + LocalCache.addressables.filter { it.value.liveSet != null }.size + " / " + LocalCache.addressables.filter { it.value.event != null }.size + "/" + LocalCache.addressables.size)
|
||||
Log.d("STATE DUMP", "Users: " + LocalCache.users.filter { it.value.liveSet != null }.size + " / " + LocalCache.users.filter { it.value.info?.latestMetadata != null }.size + "/" + LocalCache.users.size)
|
||||
Log.d("STATE DUMP", "Notes: " + LocalCache.notes.filter { it.value.liveSet != null }.size + " / " + LocalCache.notes.filter { it.value.event != null }.size + " / " + LocalCache.notes.size)
|
||||
Log.d("STATE DUMP", "Addressables: " + LocalCache.addressables.filter { it.value.liveSet != null }.size + " / " + LocalCache.addressables.filter { it.value.event != null }.size + " / " + LocalCache.addressables.size)
|
||||
Log.d("STATE DUMP", "Users: " + LocalCache.users.filter { it.value.liveSet != null }.size + " / " + LocalCache.users.filter { it.value.info?.latestMetadata != null }.size + " / " + LocalCache.users.size)
|
||||
|
||||
Log.d("STATE DUMP", "Memory used by Events: " + LocalCache.notes.values.sumOf { it.event?.countMemory() ?: 0 } / (1024 * 1024) + " MB")
|
||||
|
||||
|
@ -19,6 +19,7 @@ import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.layout.width
|
||||
import androidx.compose.foundation.rememberScrollState
|
||||
import androidx.compose.foundation.shape.CircleShape
|
||||
import androidx.compose.foundation.text.KeyboardActions
|
||||
import androidx.compose.foundation.text.KeyboardOptions
|
||||
import androidx.compose.foundation.verticalScroll
|
||||
import androidx.compose.material.AlertDialog
|
||||
@ -56,6 +57,7 @@ import androidx.compose.ui.platform.LocalFocusManager
|
||||
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.ImeAction
|
||||
import androidx.compose.ui.text.input.KeyboardCapitalization
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.unit.dp
|
||||
@ -278,16 +280,21 @@ private fun EditStatusBox(baseAccountUser: User, accountViewModel: AccountViewMo
|
||||
)
|
||||
},
|
||||
keyboardOptions = KeyboardOptions.Default.copy(
|
||||
imeAction = ImeAction.Send,
|
||||
capitalization = KeyboardCapitalization.Sentences
|
||||
),
|
||||
keyboardActions = KeyboardActions(
|
||||
onSend = {
|
||||
accountViewModel.createStatus(currentStatus.value)
|
||||
focusManager.clearFocus(true)
|
||||
}
|
||||
),
|
||||
singleLine = true,
|
||||
trailingIcon = {
|
||||
if (hasChanged) {
|
||||
UserStatusSendButton() {
|
||||
scope.launch(Dispatchers.IO) {
|
||||
accountViewModel.createStatus(currentStatus.value)
|
||||
focusManager.clearFocus(true)
|
||||
}
|
||||
accountViewModel.createStatus(currentStatus.value)
|
||||
focusManager.clearFocus(true)
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -319,24 +326,26 @@ private fun EditStatusBox(baseAccountUser: User, accountViewModel: AccountViewMo
|
||||
)
|
||||
},
|
||||
keyboardOptions = KeyboardOptions.Default.copy(
|
||||
imeAction = ImeAction.Send,
|
||||
capitalization = KeyboardCapitalization.Sentences
|
||||
),
|
||||
keyboardActions = KeyboardActions(
|
||||
onSend = {
|
||||
accountViewModel.updateStatus(it, thisStatus.value)
|
||||
focusManager.clearFocus(true)
|
||||
}
|
||||
),
|
||||
singleLine = true,
|
||||
trailingIcon = {
|
||||
if (hasChanged) {
|
||||
UserStatusSendButton() {
|
||||
scope.launch(Dispatchers.IO) {
|
||||
accountViewModel.updateStatus(it, thisStatus.value)
|
||||
focusManager.clearFocus(true)
|
||||
}
|
||||
accountViewModel.updateStatus(it, thisStatus.value)
|
||||
focusManager.clearFocus(true)
|
||||
}
|
||||
} else {
|
||||
UserStatusDeleteButton() {
|
||||
scope.launch(Dispatchers.IO) {
|
||||
accountViewModel.updateStatus(it, "")
|
||||
accountViewModel.delete(it, true)
|
||||
focusManager.clearFocus(true)
|
||||
}
|
||||
accountViewModel.deleteStatus(it)
|
||||
focusManager.clearFocus(true)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -120,9 +120,12 @@ sealed class Route(
|
||||
)
|
||||
|
||||
object Room : Route(
|
||||
route = "Room/{id}",
|
||||
route = "Room/{id}?message={message}",
|
||||
icon = R.drawable.ic_moments,
|
||||
arguments = listOf(navArgument("id") { type = NavType.StringType }).toImmutableList()
|
||||
arguments = listOf(
|
||||
navArgument("id") { type = NavType.StringType },
|
||||
navArgument("message") { type = NavType.StringType; nullable = true; defaultValue = null }
|
||||
).toImmutableList()
|
||||
)
|
||||
|
||||
object RoomByAuthor : Route(
|
||||
|
@ -625,7 +625,7 @@ fun RenderCommunitiesThumb(baseNote: Note, accountViewModel: AccountViewModel, n
|
||||
Spacer(modifier = StdHorzSpacer)
|
||||
LikeReaction(baseNote = baseNote, grayTint = MaterialTheme.colors.onSurface, accountViewModel = accountViewModel, nav)
|
||||
Spacer(modifier = StdHorzSpacer)
|
||||
ZapReaction(baseNote = baseNote, grayTint = MaterialTheme.colors.onSurface, accountViewModel = accountViewModel)
|
||||
ZapReaction(baseNote = baseNote, grayTint = MaterialTheme.colors.onSurface, accountViewModel = accountViewModel, nav = nav)
|
||||
}
|
||||
|
||||
description?.let {
|
||||
@ -739,7 +739,7 @@ fun RenderChannelThumb(baseNote: Note, channel: Channel, accountViewModel: Accou
|
||||
Spacer(modifier = StdHorzSpacer)
|
||||
LikeReaction(baseNote = baseNote, grayTint = MaterialTheme.colors.onSurface, accountViewModel = accountViewModel, nav)
|
||||
Spacer(modifier = StdHorzSpacer)
|
||||
ZapReaction(baseNote = baseNote, grayTint = MaterialTheme.colors.onSurface, accountViewModel = accountViewModel)
|
||||
ZapReaction(baseNote = baseNote, grayTint = MaterialTheme.colors.onSurface, accountViewModel = accountViewModel, nav = nav)
|
||||
}
|
||||
|
||||
description?.let {
|
||||
|
@ -569,7 +569,7 @@ private fun StatusRow(
|
||||
Row(verticalAlignment = Alignment.CenterVertically, modifier = ReactionRowHeightChat) {
|
||||
LikeReaction(baseNote, MaterialTheme.colors.placeholderText, accountViewModel, nav)
|
||||
Spacer(modifier = StdHorzSpacer)
|
||||
ZapReaction(baseNote, MaterialTheme.colors.placeholderText, accountViewModel)
|
||||
ZapReaction(baseNote, MaterialTheme.colors.placeholderText, accountViewModel, nav = nav)
|
||||
Spacer(modifier = DoubleHorzSpacer)
|
||||
ReplyReaction(
|
||||
baseNote = baseNote,
|
||||
|
@ -42,7 +42,6 @@ import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import com.vitorpamplona.amethyst.R
|
||||
import com.vitorpamplona.amethyst.model.LocalCache
|
||||
import com.vitorpamplona.amethyst.model.Note
|
||||
import com.vitorpamplona.amethyst.model.User
|
||||
import com.vitorpamplona.amethyst.ui.components.ImageUrlType
|
||||
@ -52,7 +51,6 @@ import com.vitorpamplona.amethyst.ui.components.TranslatableRichTextViewer
|
||||
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.DividerThickness
|
||||
import com.vitorpamplona.amethyst.ui.theme.NotificationIconModifier
|
||||
import com.vitorpamplona.amethyst.ui.theme.NotificationIconModifierSmaller
|
||||
@ -70,8 +68,6 @@ import com.vitorpamplona.amethyst.ui.theme.newItemBackgroundColor
|
||||
import com.vitorpamplona.amethyst.ui.theme.overPictureBackground
|
||||
import com.vitorpamplona.amethyst.ui.theme.profile35dpModifier
|
||||
import com.vitorpamplona.quartz.events.ImmutableListOfLists
|
||||
import com.vitorpamplona.quartz.events.LnZapEvent
|
||||
import com.vitorpamplona.quartz.events.LnZapRequestEvent
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
import kotlinx.collections.immutable.toImmutableList
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
@ -352,8 +348,7 @@ private fun ParseAuthorCommentAndAmount(
|
||||
}
|
||||
|
||||
LaunchedEffect(key1 = zapRequest.idHex, key2 = zapEvent?.idHex) {
|
||||
launch(Dispatchers.IO) {
|
||||
val newState = loadAmountState(zapRequest, zapEvent, accountViewModel)
|
||||
accountViewModel.decryptAmountMessage(zapRequest, zapEvent) { newState ->
|
||||
if (newState != null) {
|
||||
content.value = newState
|
||||
}
|
||||
@ -363,34 +358,6 @@ private fun ParseAuthorCommentAndAmount(
|
||||
onReady(content)
|
||||
}
|
||||
|
||||
private suspend fun loadAmountState(
|
||||
zapRequest: Note,
|
||||
zapEvent: Note?,
|
||||
accountViewModel: AccountViewModel
|
||||
): ZapAmountCommentNotification? {
|
||||
(zapRequest.event as? LnZapRequestEvent)?.let {
|
||||
val decryptedContent = accountViewModel.decryptZap(zapRequest)
|
||||
val amount = (zapEvent?.event as? LnZapEvent)?.amount
|
||||
if (decryptedContent != null) {
|
||||
val newAuthor = LocalCache.getOrCreateUser(decryptedContent.pubKey)
|
||||
return ZapAmountCommentNotification(
|
||||
newAuthor,
|
||||
decryptedContent.content.ifBlank { null },
|
||||
showAmountAxis(amount)
|
||||
)
|
||||
} else {
|
||||
if (!zapRequest.event?.content().isNullOrBlank() || amount != null) {
|
||||
return ZapAmountCommentNotification(
|
||||
zapRequest.author,
|
||||
zapRequest.event?.content()?.ifBlank { null },
|
||||
showAmountAxis(amount)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun RenderState(
|
||||
content: MutableState<ZapAmountCommentNotification>,
|
||||
|
@ -3,6 +3,7 @@ package com.vitorpamplona.amethyst.ui.components
|
||||
import androidx.compose.animation.Crossfade
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.text.ClickableText
|
||||
import androidx.compose.material.Icon
|
||||
@ -33,7 +34,6 @@ import com.vitorpamplona.amethyst.R
|
||||
import com.vitorpamplona.amethyst.model.AddressableNote
|
||||
import com.vitorpamplona.amethyst.model.Note
|
||||
import com.vitorpamplona.amethyst.model.User
|
||||
import com.vitorpamplona.amethyst.service.Nip05Verifier
|
||||
import com.vitorpamplona.amethyst.ui.note.LoadAddressableNote
|
||||
import com.vitorpamplona.amethyst.ui.note.LoadStatuses
|
||||
import com.vitorpamplona.amethyst.ui.note.NIP05CheckingIcon
|
||||
@ -45,7 +45,8 @@ import com.vitorpamplona.amethyst.ui.theme.Font14SP
|
||||
import com.vitorpamplona.amethyst.ui.theme.NIP05IconSize
|
||||
import com.vitorpamplona.amethyst.ui.theme.Size15Modifier
|
||||
import com.vitorpamplona.amethyst.ui.theme.Size16Modifier
|
||||
import com.vitorpamplona.amethyst.ui.theme.Size18Modifier
|
||||
import com.vitorpamplona.amethyst.ui.theme.Size5dp
|
||||
import com.vitorpamplona.amethyst.ui.theme.StdHorzSpacer
|
||||
import com.vitorpamplona.amethyst.ui.theme.lessImportantLink
|
||||
import com.vitorpamplona.amethyst.ui.theme.nip05
|
||||
import com.vitorpamplona.amethyst.ui.theme.placeholderText
|
||||
@ -53,13 +54,11 @@ import com.vitorpamplona.quartz.events.AddressableEvent
|
||||
import com.vitorpamplona.quartz.events.UserMetadata
|
||||
import com.vitorpamplona.quartz.utils.TimeUtils
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlin.time.Duration.Companion.seconds
|
||||
|
||||
@Composable
|
||||
fun nip05VerificationAsAState(userMetadata: UserMetadata, pubkeyHex: String): MutableState<Boolean?> {
|
||||
fun nip05VerificationAsAState(userMetadata: UserMetadata, pubkeyHex: String, accountViewModel: AccountViewModel): MutableState<Boolean?> {
|
||||
val nip05Verified = remember(userMetadata.nip05) {
|
||||
// starts with null if must verify or already filled in if verified in the last hour
|
||||
val default = if ((userMetadata.nip05LastVerificationTime ?: 0) > TimeUtils.oneHourAgo()) {
|
||||
@ -73,37 +72,9 @@ fun nip05VerificationAsAState(userMetadata: UserMetadata, pubkeyHex: String): Mu
|
||||
|
||||
if (nip05Verified.value == null) {
|
||||
LaunchedEffect(key1 = userMetadata.nip05) {
|
||||
launch(Dispatchers.IO) {
|
||||
userMetadata.nip05?.ifBlank { null }?.let { nip05 ->
|
||||
Nip05Verifier().verifyNip05(
|
||||
nip05,
|
||||
onSuccess = {
|
||||
// Marks user as verified
|
||||
if (it == pubkeyHex) {
|
||||
userMetadata.nip05Verified = true
|
||||
userMetadata.nip05LastVerificationTime = TimeUtils.now()
|
||||
|
||||
if (nip05Verified.value != true) {
|
||||
nip05Verified.value = true
|
||||
}
|
||||
} else {
|
||||
userMetadata.nip05Verified = false
|
||||
userMetadata.nip05LastVerificationTime = 0
|
||||
|
||||
if (nip05Verified.value != false) {
|
||||
nip05Verified.value = false
|
||||
}
|
||||
}
|
||||
},
|
||||
onError = {
|
||||
userMetadata.nip05LastVerificationTime = 0
|
||||
userMetadata.nip05Verified = false
|
||||
|
||||
if (nip05Verified.value != false) {
|
||||
nip05Verified.value = false
|
||||
}
|
||||
}
|
||||
)
|
||||
accountViewModel.verifyNip05(userMetadata, pubkeyHex) { newVerificationStatus ->
|
||||
if (nip05Verified.value != newVerificationStatus) {
|
||||
nip05Verified.value = newVerificationStatus
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -154,7 +125,7 @@ private fun VerifyAndDisplayNIP05OrStatusLine(
|
||||
Column(modifier = columnModifier) {
|
||||
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||
if (nip05 != null) {
|
||||
val nip05Verified = nip05VerificationAsAState(baseUser.info!!, baseUser.pubkeyHex)
|
||||
val nip05Verified = nip05VerificationAsAState(baseUser.info!!, baseUser.pubkeyHex, accountViewModel)
|
||||
|
||||
if (nip05Verified.value != true) {
|
||||
DisplayNIP05(nip05, nip05Verified)
|
||||
@ -180,7 +151,7 @@ fun RotateStatuses(
|
||||
accountViewModel: AccountViewModel,
|
||||
nav: (String) -> Unit
|
||||
) {
|
||||
var indexToDisplay by remember {
|
||||
var indexToDisplay by remember(statuses) {
|
||||
mutableIntStateOf(0)
|
||||
}
|
||||
|
||||
@ -190,7 +161,7 @@ fun RotateStatuses(
|
||||
LaunchedEffect(Unit) {
|
||||
while (true) {
|
||||
delay(10.seconds)
|
||||
indexToDisplay = ((indexToDisplay + 1) % (statuses.size + 1))
|
||||
indexToDisplay = (indexToDisplay + 1) % statuses.size
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -233,7 +204,7 @@ fun DisplayStatus(
|
||||
"music" -> Icon(
|
||||
painter = painterResource(id = R.drawable.tunestr),
|
||||
null,
|
||||
modifier = Size18Modifier,
|
||||
modifier = Size15Modifier.padding(end = Size5dp),
|
||||
tint = MaterialTheme.colors.placeholderText
|
||||
)
|
||||
else -> {}
|
||||
@ -249,6 +220,7 @@ fun DisplayStatus(
|
||||
|
||||
if (url != null) {
|
||||
val uri = LocalUriHandler.current
|
||||
Spacer(modifier = StdHorzSpacer)
|
||||
IconButton(
|
||||
modifier = Size15Modifier,
|
||||
onClick = { runCatching { uri.openUri(url.trim()) } }
|
||||
@ -263,6 +235,7 @@ fun DisplayStatus(
|
||||
} else if (nostrATag != null) {
|
||||
LoadAddressableNote(nostrATag) { note ->
|
||||
if (note != null) {
|
||||
Spacer(modifier = StdHorzSpacer)
|
||||
IconButton(
|
||||
modifier = Size15Modifier,
|
||||
onClick = {
|
||||
@ -284,6 +257,7 @@ fun DisplayStatus(
|
||||
} else if (nostrHexID != null) {
|
||||
LoadNote(baseNoteHex = nostrHexID) {
|
||||
if (it != null) {
|
||||
Spacer(modifier = StdHorzSpacer)
|
||||
IconButton(
|
||||
modifier = Size15Modifier,
|
||||
onClick = {
|
||||
@ -353,12 +327,12 @@ private fun NIP05VerifiedSymbol(nip05Verified: MutableState<Boolean?>, modifier:
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun DisplayNip05ProfileStatus(user: User) {
|
||||
fun DisplayNip05ProfileStatus(user: User, accountViewModel: AccountViewModel) {
|
||||
val uri = LocalUriHandler.current
|
||||
|
||||
user.nip05()?.let { nip05 ->
|
||||
if (nip05.split("@").size <= 2) {
|
||||
val nip05Verified = nip05VerificationAsAState(user.info!!, user.pubkeyHex)
|
||||
val nip05Verified = nip05VerificationAsAState(user.info!!, user.pubkeyHex, accountViewModel)
|
||||
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||
NIP05VerifiedSymbol(nip05Verified, Size16Modifier)
|
||||
var domainPadStart = 5.dp
|
||||
|
@ -165,6 +165,7 @@ import com.vitorpamplona.quartz.events.BaseTextNoteEvent
|
||||
import com.vitorpamplona.quartz.events.ChannelCreateEvent
|
||||
import com.vitorpamplona.quartz.events.ChannelMessageEvent
|
||||
import com.vitorpamplona.quartz.events.ChannelMetadataEvent
|
||||
import com.vitorpamplona.quartz.events.ChatroomKey
|
||||
import com.vitorpamplona.quartz.events.ChatroomKeyable
|
||||
import com.vitorpamplona.quartz.events.ClassifiedsEvent
|
||||
import com.vitorpamplona.quartz.events.CommunityDefinitionEvent
|
||||
@ -441,9 +442,7 @@ fun WatchForReports(
|
||||
val noteReportsState by note.live().reports.observeAsState()
|
||||
|
||||
LaunchedEffect(key1 = noteReportsState, key2 = userFollowsState) {
|
||||
launch(Dispatchers.Default) {
|
||||
accountViewModel.isNoteAcceptable(note, onChange)
|
||||
}
|
||||
accountViewModel.isNoteAcceptable(note, onChange)
|
||||
}
|
||||
}
|
||||
|
||||
@ -784,9 +783,9 @@ private fun ShortCommunityActionOptions(
|
||||
nav: (String) -> Unit
|
||||
) {
|
||||
Spacer(modifier = StdHorzSpacer)
|
||||
LikeReaction(baseNote = note, grayTint = MaterialTheme.colors.onSurface, accountViewModel = accountViewModel, nav)
|
||||
LikeReaction(baseNote = note, grayTint = MaterialTheme.colors.onSurface, accountViewModel = accountViewModel, nav = nav)
|
||||
Spacer(modifier = StdHorzSpacer)
|
||||
ZapReaction(baseNote = note, grayTint = MaterialTheme.colors.onSurface, accountViewModel = accountViewModel)
|
||||
ZapReaction(baseNote = note, grayTint = MaterialTheme.colors.onSurface, accountViewModel = accountViewModel, nav = nav)
|
||||
|
||||
WatchAddressableNoteFollows(note, accountViewModel) { isFollowing ->
|
||||
if (!isFollowing) {
|
||||
@ -1238,6 +1237,16 @@ fun routeFor(note: Note, loggedIn: User): String? {
|
||||
return null
|
||||
}
|
||||
|
||||
fun routeToMessage(user: User, draftMessage: String?, accountViewModel: AccountViewModel): String {
|
||||
val withKey = ChatroomKey(persistentSetOf(user.pubkeyHex))
|
||||
accountViewModel.account.userProfile().createChatroom(withKey)
|
||||
return if (draftMessage != null) {
|
||||
"Room/${withKey.hashCode()}?message=$draftMessage"
|
||||
} else {
|
||||
"Room/${withKey.hashCode()}"
|
||||
}
|
||||
}
|
||||
|
||||
fun routeFor(note: Channel): String {
|
||||
return "Channel/${note.idHex}"
|
||||
}
|
||||
@ -1315,7 +1324,7 @@ fun RenderPoll(
|
||||
nav: (String) -> Unit
|
||||
) {
|
||||
val noteEvent = note.event as? PollNoteEvent ?: return
|
||||
val eventContent = remember { noteEvent.content() }
|
||||
val eventContent = remember(note) { noteEvent.content() }
|
||||
|
||||
if (makeItShort && accountViewModel.isLoggedUser(note.author)) {
|
||||
Text(
|
||||
@ -2399,9 +2408,9 @@ private fun ReplyRow(
|
||||
) {
|
||||
val noteEvent = note.event
|
||||
|
||||
val showReply by remember {
|
||||
val showReply by remember(note) {
|
||||
derivedStateOf {
|
||||
noteEvent is TextNoteEvent && (note.replyTo != null || noteEvent.hasAnyTaggedUser())
|
||||
noteEvent is BaseTextNoteEvent && (note.replyTo != null || noteEvent.hasAnyTaggedUser())
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -98,9 +98,7 @@ private fun WatchZapsAndUpdateTallies(
|
||||
val zapsState by baseNote.live().zaps.observeAsState()
|
||||
|
||||
LaunchedEffect(key1 = zapsState) {
|
||||
launch(Dispatchers.Default) {
|
||||
pollViewModel.refreshTallies()
|
||||
}
|
||||
pollViewModel.refreshTallies()
|
||||
}
|
||||
}
|
||||
|
||||
@ -132,8 +130,7 @@ private fun OptionNote(
|
||||
ZapVote(
|
||||
baseNote,
|
||||
poolOption,
|
||||
accountViewModel,
|
||||
pollViewModel,
|
||||
pollViewModel = pollViewModel,
|
||||
nonClickablePrepend = {
|
||||
RenderOptionAfterVote(
|
||||
poolOption.descriptor,
|
||||
@ -147,18 +144,21 @@ private fun OptionNote(
|
||||
)
|
||||
},
|
||||
clickablePrepend = {
|
||||
}
|
||||
},
|
||||
accountViewModel = accountViewModel,
|
||||
nav = nav
|
||||
)
|
||||
} else {
|
||||
ZapVote(
|
||||
baseNote,
|
||||
poolOption,
|
||||
accountViewModel,
|
||||
pollViewModel,
|
||||
pollViewModel = pollViewModel,
|
||||
nonClickablePrepend = {},
|
||||
clickablePrepend = {
|
||||
RenderOptionBeforeVote(poolOption.descriptor, canPreview, tags, backgroundColor, accountViewModel, nav)
|
||||
}
|
||||
},
|
||||
accountViewModel = accountViewModel,
|
||||
nav = nav
|
||||
)
|
||||
}
|
||||
}
|
||||
@ -269,11 +269,12 @@ private fun RenderOptionBeforeVote(
|
||||
fun ZapVote(
|
||||
baseNote: Note,
|
||||
poolOption: PollOption,
|
||||
accountViewModel: AccountViewModel,
|
||||
pollViewModel: PollNoteViewModel,
|
||||
modifier: Modifier = Modifier,
|
||||
pollViewModel: PollNoteViewModel,
|
||||
nonClickablePrepend: @Composable () -> Unit,
|
||||
clickablePrepend: @Composable () -> Unit
|
||||
clickablePrepend: @Composable () -> Unit,
|
||||
accountViewModel: AccountViewModel,
|
||||
nav: (String) -> Unit
|
||||
) {
|
||||
val isLoggedUser by remember {
|
||||
derivedStateOf {
|
||||
@ -283,6 +284,7 @@ fun ZapVote(
|
||||
|
||||
var wantsToZap by remember { mutableStateOf(false) }
|
||||
var zappingProgress by remember { mutableStateOf(0f) }
|
||||
var showErrorMessageDialog by remember { mutableStateOf<String?>(null) }
|
||||
|
||||
val context = LocalContext.current
|
||||
val scope = rememberCoroutineScope()
|
||||
@ -384,7 +386,7 @@ fun ZapVote(
|
||||
onError = {
|
||||
scope.launch {
|
||||
zappingProgress = 0f
|
||||
Toast.makeText(context, it, Toast.LENGTH_SHORT).show()
|
||||
showErrorMessageDialog = it
|
||||
}
|
||||
},
|
||||
onProgress = {
|
||||
@ -395,6 +397,19 @@ fun ZapVote(
|
||||
)
|
||||
}
|
||||
|
||||
if (showErrorMessageDialog != null) {
|
||||
ErrorMessageDialog(
|
||||
title = stringResource(id = R.string.error_dialog_zap_error),
|
||||
textContent = showErrorMessageDialog ?: "",
|
||||
onClickStartMessage = {
|
||||
baseNote.author?.let {
|
||||
nav(routeToMessage(it, showErrorMessageDialog, accountViewModel))
|
||||
}
|
||||
},
|
||||
onDismiss = { showErrorMessageDialog = null }
|
||||
)
|
||||
}
|
||||
|
||||
clickablePrepend()
|
||||
|
||||
if (poolOption.zappedByLoggedIn) {
|
||||
|
@ -3,13 +3,16 @@ package com.vitorpamplona.amethyst.ui.note
|
||||
import androidx.compose.runtime.Immutable
|
||||
import androidx.compose.runtime.Stable
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import com.vitorpamplona.amethyst.model.Account
|
||||
import com.vitorpamplona.amethyst.model.Note
|
||||
import com.vitorpamplona.amethyst.model.User
|
||||
import com.vitorpamplona.quartz.events.*
|
||||
import com.vitorpamplona.quartz.utils.TimeUtils
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.asStateFlow
|
||||
import kotlinx.coroutines.launch
|
||||
import java.math.BigDecimal
|
||||
import java.math.RoundingMode
|
||||
|
||||
@ -52,12 +55,12 @@ class PollNoteViewModel : ViewModel() {
|
||||
closedAt = pollEvent?.getTagInt(CLOSED_AT)
|
||||
}
|
||||
|
||||
suspend fun refreshTallies() {
|
||||
totalZapped = totalZapped()
|
||||
wasZappedByLoggedInAccount = pollNote?.let { account?.calculateIfNoteWasZappedByAccount(it) } ?: false
|
||||
fun refreshTallies() {
|
||||
viewModelScope.launch(Dispatchers.Default) {
|
||||
totalZapped = totalZapped()
|
||||
wasZappedByLoggedInAccount = pollNote?.let { account?.calculateIfNoteWasZappedByAccount(it) } ?: false
|
||||
|
||||
_tallies.emit(
|
||||
pollOptions?.keys?.map {
|
||||
val newOptions = pollOptions?.keys?.map {
|
||||
val zappedInOption = zappedPollOptionAmount(it)
|
||||
|
||||
val myTally = if (totalZapped.compareTo(BigDecimal.ZERO) > 0) {
|
||||
@ -71,8 +74,12 @@ class PollNoteViewModel : ViewModel() {
|
||||
val consensus = consensusThreshold != null && myTally >= consensusThreshold!!
|
||||
|
||||
PollOption(it, pollOptions?.get(it) ?: "", zappedInOption, myTally, consensus, zappedByLoggedIn)
|
||||
} ?: emptyList()
|
||||
)
|
||||
}
|
||||
|
||||
_tallies.emit(
|
||||
newOptions ?: emptyList()
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fun canZap(): Boolean {
|
||||
|
@ -40,6 +40,7 @@ import androidx.compose.material.ProgressIndicatorDefaults
|
||||
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
|
||||
@ -214,7 +215,7 @@ private fun InnerReactionRow(
|
||||
) {
|
||||
val (value, elapsed) = measureTimedValue {
|
||||
Row(verticalAlignment = CenterVertically) {
|
||||
ZapReaction(baseNote, MaterialTheme.colors.placeholderText, accountViewModel)
|
||||
ZapReaction(baseNote, MaterialTheme.colors.placeholderText, accountViewModel, nav = nav)
|
||||
}
|
||||
}
|
||||
Log.d("Rendering Metrics", "Reaction Zaps: ${baseNote.event?.content()?.split("\n")?.getOrNull(0)?.take(15)}.. $elapsed")
|
||||
@ -245,7 +246,7 @@ private fun LoadAndDisplayZapraiser(
|
||||
wantsToSeeReactions: MutableState<Boolean>,
|
||||
accountViewModel: AccountViewModel
|
||||
) {
|
||||
val zapraiserAmount by remember {
|
||||
val zapraiserAmount by remember(baseNote) {
|
||||
derivedStateOf {
|
||||
baseNote.event?.zapraiserAmount() ?: 0
|
||||
}
|
||||
@ -265,42 +266,26 @@ private fun LoadAndDisplayZapraiser(
|
||||
}
|
||||
}
|
||||
|
||||
@Immutable
|
||||
data class ZapraiserStatus(val progress: Float, val left: String)
|
||||
|
||||
@Composable
|
||||
fun RenderZapRaiser(baseNote: Note, zapraiserAmount: Long, details: Boolean, accountViewModel: AccountViewModel) {
|
||||
val zapsState by baseNote.live().zaps.observeAsState()
|
||||
|
||||
var zapraiserProgress by remember { mutableStateOf(0F) }
|
||||
var zapraiserLeft by remember { mutableStateOf("$zapraiserAmount") }
|
||||
var zapraiserStatus by remember { mutableStateOf(ZapraiserStatus(0F, "$zapraiserAmount")) }
|
||||
|
||||
LaunchedEffect(key1 = zapsState) {
|
||||
launch(Dispatchers.Default) {
|
||||
zapsState?.note?.let {
|
||||
val newZapAmount = accountViewModel.calculateZapAmount(it)
|
||||
var percentage = newZapAmount.div(zapraiserAmount.toBigDecimal()).toFloat()
|
||||
|
||||
if (percentage > 1) {
|
||||
percentage = 1f
|
||||
}
|
||||
|
||||
if (Math.abs(zapraiserProgress - percentage) > 0.001) {
|
||||
val newZapraiserProgress = percentage
|
||||
val newZapraiserLeft = if (percentage > 0.99) {
|
||||
"0"
|
||||
} else {
|
||||
showAmount((zapraiserAmount * (1 - percentage)).toBigDecimal())
|
||||
}
|
||||
if (zapraiserLeft != newZapraiserLeft) {
|
||||
zapraiserLeft = newZapraiserLeft
|
||||
}
|
||||
if (zapraiserProgress != newZapraiserProgress) {
|
||||
zapraiserProgress = newZapraiserProgress
|
||||
}
|
||||
zapsState?.note?.let {
|
||||
accountViewModel.calculateZapraiser(baseNote) { newStatus ->
|
||||
if (zapraiserStatus != newStatus) {
|
||||
zapraiserStatus = newStatus
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
val color = if (zapraiserProgress > 0.99) {
|
||||
val color = if (zapraiserStatus.progress > 0.99) {
|
||||
DarkerGreen
|
||||
} else {
|
||||
MaterialTheme.colors.mediumImportanceLink
|
||||
@ -311,7 +296,7 @@ fun RenderZapRaiser(baseNote: Note, zapraiserAmount: Long, details: Boolean, acc
|
||||
.fillMaxWidth()
|
||||
.height(if (details) 24.dp else 4.dp),
|
||||
color = color,
|
||||
progress = zapraiserProgress
|
||||
progress = zapraiserStatus.progress
|
||||
)
|
||||
|
||||
if (details) {
|
||||
@ -319,14 +304,14 @@ fun RenderZapRaiser(baseNote: Note, zapraiserAmount: Long, details: Boolean, acc
|
||||
contentAlignment = Center,
|
||||
modifier = TinyBorders
|
||||
) {
|
||||
val totalPercentage by remember(zapraiserProgress) {
|
||||
val totalPercentage by remember(zapraiserStatus) {
|
||||
derivedStateOf {
|
||||
"${(zapraiserProgress * 100).roundToInt()}%"
|
||||
"${(zapraiserStatus.progress * 100).roundToInt()}%"
|
||||
}
|
||||
}
|
||||
|
||||
Text(
|
||||
text = stringResource(id = R.string.sats_to_complete, totalPercentage, zapraiserLeft),
|
||||
text = stringResource(id = R.string.sats_to_complete, totalPercentage, zapraiserStatus.left),
|
||||
modifier = NoSoTinyBorders,
|
||||
color = MaterialTheme.colors.placeholderText,
|
||||
fontSize = Font14SP,
|
||||
@ -338,15 +323,7 @@ fun RenderZapRaiser(baseNote: Note, zapraiserAmount: Long, details: Boolean, acc
|
||||
|
||||
@Composable
|
||||
private fun WatchReactionsZapsBoostsAndDisplayIfExists(baseNote: Note, content: @Composable () -> Unit) {
|
||||
val hasReactions by baseNote.live().zaps.combineWith(
|
||||
liveData1 = baseNote.live().boosts,
|
||||
liveData2 = baseNote.live().reactions,
|
||||
block = { zapsState, boostsState, reactionsState ->
|
||||
zapsState?.note?.zaps?.isNotEmpty() == true ||
|
||||
boostsState?.note?.boosts?.isNotEmpty() == true ||
|
||||
reactionsState?.note?.reactions?.isNotEmpty() == true
|
||||
}
|
||||
).observeAsState(
|
||||
val hasReactions by baseNote.live().hasReactions.observeAsState(
|
||||
baseNote.zaps.isNotEmpty() ||
|
||||
baseNote.boosts.isNotEmpty() ||
|
||||
baseNote.reactions.isNotEmpty()
|
||||
@ -439,17 +416,16 @@ private fun WatchBoostsAndRenderGallery(
|
||||
nav: (String) -> Unit,
|
||||
accountViewModel: AccountViewModel
|
||||
) {
|
||||
val boostsState by baseNote.live().boosts.observeAsState()
|
||||
val boostsEvents by remember(boostsState) {
|
||||
derivedStateOf { baseNote.boosts.toImmutableList() }
|
||||
}
|
||||
val boostsEvents by baseNote.live().boostList.observeAsState()
|
||||
|
||||
if (boostsEvents.isNotEmpty()) {
|
||||
RenderBoostGallery(
|
||||
boostsEvents,
|
||||
nav,
|
||||
accountViewModel
|
||||
)
|
||||
boostsEvents?.let {
|
||||
if (it.isNotEmpty()) {
|
||||
RenderBoostGallery(
|
||||
it,
|
||||
nav,
|
||||
accountViewModel
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -909,10 +885,7 @@ private fun WatchReactionTypeForNote(baseNote: Note, accountViewModel: AccountVi
|
||||
val reactionsState by baseNote.live().reactions.observeAsState()
|
||||
|
||||
LaunchedEffect(key1 = reactionsState) {
|
||||
launch(Dispatchers.Default) {
|
||||
val reactionNote = reactionsState?.note?.getReactionBy(accountViewModel.userProfile())
|
||||
onNewReactionType(reactionNote)
|
||||
}
|
||||
accountViewModel.loadReactionTo(reactionsState?.note, onNewReactionType)
|
||||
}
|
||||
}
|
||||
|
||||
@ -947,32 +920,9 @@ private fun RenderReactionType(
|
||||
|
||||
@Composable
|
||||
fun LikeText(baseNote: Note, grayTint: Color) {
|
||||
val reactionsCount = remember(baseNote) {
|
||||
mutableStateOf(baseNote.reactions.size)
|
||||
}
|
||||
val reactionCount by baseNote.live().reactionCount.observeAsState(0)
|
||||
|
||||
val scope = rememberCoroutineScope()
|
||||
|
||||
WatchReactionCountForNote(baseNote) { newReactionsCount ->
|
||||
if (reactionsCount.value != newReactionsCount) {
|
||||
scope.launch(Dispatchers.Main) {
|
||||
reactionsCount.value = newReactionsCount
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
SlidingAnimationCount(reactionsCount, grayTint)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun WatchReactionCountForNote(baseNote: Note, onNewReactionCount: (Int) -> Unit) {
|
||||
val reactionsState by baseNote.live().reactions.observeAsState()
|
||||
|
||||
LaunchedEffect(key1 = reactionsState) {
|
||||
launch(Dispatchers.Default) {
|
||||
onNewReactionCount(reactionsState?.note?.countReactions() ?: 0)
|
||||
}
|
||||
}
|
||||
SlidingAnimationCount(reactionCount, grayTint)
|
||||
}
|
||||
|
||||
private fun likeClick(
|
||||
@ -1028,11 +978,13 @@ fun ZapReaction(
|
||||
grayTint: Color,
|
||||
accountViewModel: AccountViewModel,
|
||||
iconSize: Dp = 20.dp,
|
||||
animationSize: Dp = 14.dp
|
||||
animationSize: Dp = 14.dp,
|
||||
nav: (String) -> Unit
|
||||
) {
|
||||
var wantsToZap by remember { mutableStateOf(false) }
|
||||
var wantsToChangeZapAmount by remember { mutableStateOf(false) }
|
||||
var wantsToSetCustomZap by remember { mutableStateOf(false) }
|
||||
var showErrorMessageDialog by remember { mutableStateOf<String?>(null) }
|
||||
|
||||
val context = LocalContext.current
|
||||
val scope = rememberCoroutineScope()
|
||||
@ -1084,7 +1036,7 @@ fun ZapReaction(
|
||||
onError = {
|
||||
scope.launch {
|
||||
zappingProgress = 0f
|
||||
Toast.makeText(context, it, Toast.LENGTH_SHORT).show()
|
||||
showErrorMessageDialog = it
|
||||
}
|
||||
},
|
||||
onProgress = {
|
||||
@ -1095,6 +1047,19 @@ fun ZapReaction(
|
||||
)
|
||||
}
|
||||
|
||||
if (showErrorMessageDialog != null) {
|
||||
ErrorMessageDialog(
|
||||
title = stringResource(id = R.string.error_dialog_zap_error),
|
||||
textContent = showErrorMessageDialog ?: "",
|
||||
onClickStartMessage = {
|
||||
baseNote.author?.let {
|
||||
nav(routeToMessage(it, showErrorMessageDialog, accountViewModel))
|
||||
}
|
||||
},
|
||||
onDismiss = { showErrorMessageDialog = null }
|
||||
)
|
||||
}
|
||||
|
||||
if (wantsToChangeZapAmount) {
|
||||
UpdateZapAmountDialog({ wantsToChangeZapAmount = false }, accountViewModel = accountViewModel)
|
||||
}
|
||||
@ -1217,9 +1182,7 @@ private fun WatchZapsForNote(baseNote: Note, accountViewModel: AccountViewModel,
|
||||
val zapsState by baseNote.live().zaps.observeAsState()
|
||||
|
||||
LaunchedEffect(key1 = zapsState) {
|
||||
launch(Dispatchers.Default) {
|
||||
onWasZapped(accountViewModel.calculateIfNoteWasZappedByAccount(baseNote))
|
||||
}
|
||||
accountViewModel.calculateIfNoteWasZappedByAccount(baseNote, onWasZapped)
|
||||
}
|
||||
}
|
||||
|
||||
@ -1249,9 +1212,7 @@ fun WatchZapAmountsForNote(baseNote: Note, accountViewModel: AccountViewModel, o
|
||||
val zapsState by baseNote.live().zaps.observeAsState()
|
||||
|
||||
LaunchedEffect(key1 = zapsState) {
|
||||
launch(Dispatchers.Default) {
|
||||
onZapAmount(showAmount(accountViewModel.calculateZapAmount(baseNote)))
|
||||
}
|
||||
accountViewModel.calculateZapAmount(baseNote, onZapAmount)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -4,11 +4,14 @@ import android.widget.Toast
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.text.KeyboardOptions
|
||||
import androidx.compose.material.*
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.outlined.Done
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.input.KeyboardCapitalization
|
||||
import androidx.compose.ui.text.input.KeyboardType
|
||||
@ -111,8 +114,7 @@ fun ZapCustomDialog(onClose: () -> Unit, accountViewModel: AccountViewModel, bas
|
||||
onError = {
|
||||
zappingProgress = 0f
|
||||
scope.launch {
|
||||
Toast
|
||||
.makeText(context, it, Toast.LENGTH_SHORT).show()
|
||||
Toast.makeText(context, it, Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
},
|
||||
onProgress = {
|
||||
@ -225,3 +227,51 @@ fun ZapButton(isActive: Boolean, onPost: () -> Unit) {
|
||||
Text(text = "⚡Zap ", color = Color.White)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun ErrorMessageDialog(
|
||||
title: String,
|
||||
textContent: String,
|
||||
buttonColors: ButtonColors = ButtonDefaults.buttonColors(),
|
||||
onClickStartMessage: () -> Unit,
|
||||
onDismiss: () -> Unit
|
||||
) {
|
||||
AlertDialog(
|
||||
onDismissRequest = onDismiss,
|
||||
title = {
|
||||
Text(title)
|
||||
},
|
||||
text = {
|
||||
Text(textContent)
|
||||
},
|
||||
buttons = {
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.padding(all = 8.dp)
|
||||
.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.SpaceBetween
|
||||
) {
|
||||
TextButton(onClick = onClickStartMessage) {
|
||||
Icon(
|
||||
painter = painterResource(R.drawable.ic_dm),
|
||||
contentDescription = null
|
||||
)
|
||||
Spacer(Modifier.width(8.dp))
|
||||
Text(stringResource(R.string.error_dialog_talk_to_user))
|
||||
}
|
||||
Button(onClick = onDismiss, colors = buttonColors) {
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Icon(
|
||||
imageVector = Icons.Outlined.Done,
|
||||
contentDescription = null
|
||||
)
|
||||
Spacer(Modifier.width(8.dp))
|
||||
Text(stringResource(R.string.error_dialog_button_ok))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
@ -35,7 +35,6 @@ import com.vitorpamplona.amethyst.ui.note.MultiSetCompose
|
||||
import com.vitorpamplona.amethyst.ui.note.NoteCompose
|
||||
import com.vitorpamplona.amethyst.ui.note.ZapUserSetCompose
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
@OptIn(ExperimentalMaterialApi::class)
|
||||
@Composable
|
||||
@ -102,11 +101,9 @@ private fun WatchScrollToTop(
|
||||
val scrollToTop by viewModel.scrollToTop.collectAsState()
|
||||
|
||||
LaunchedEffect(scrollToTop) {
|
||||
launch {
|
||||
if (scrollToTop > 0 && viewModel.scrolltoTopPending) {
|
||||
listState.scrollToItem(index = 0)
|
||||
viewModel.sentToTop()
|
||||
}
|
||||
if (scrollToTop > 0 && viewModel.scrolltoTopPending) {
|
||||
listState.scrollToItem(index = 0)
|
||||
viewModel.sentToTop()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -34,7 +34,6 @@ import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.asStateFlow
|
||||
import kotlinx.coroutines.flow.update
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlin.time.ExperimentalTime
|
||||
import kotlin.time.measureTimedValue
|
||||
|
||||
@Stable
|
||||
@ -270,7 +269,6 @@ open class CardFeedViewModel(val localFilter: FeedFilter<Note>) : ViewModel() {
|
||||
private val bundler = BundledUpdate(1000, Dispatchers.IO)
|
||||
private val bundlerInsert = BundledInsert<Set<Note>>(1000, Dispatchers.IO)
|
||||
|
||||
@OptIn(ExperimentalTime::class)
|
||||
fun invalidateData(ignoreIfDoing: Boolean = false) {
|
||||
bundler.invalidate(ignoreIfDoing) {
|
||||
// adds the time to perform the refresh into this delay
|
||||
@ -282,7 +280,6 @@ open class CardFeedViewModel(val localFilter: FeedFilter<Note>) : ViewModel() {
|
||||
}
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalTime::class)
|
||||
fun invalidateDataAndSendToTop() {
|
||||
clear()
|
||||
bundler.invalidate(false) {
|
||||
@ -296,7 +293,6 @@ open class CardFeedViewModel(val localFilter: FeedFilter<Note>) : ViewModel() {
|
||||
}
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalTime::class)
|
||||
fun checkKeysInvalidateDataAndSendToTop() {
|
||||
if (lastFeedKey != localFilter.feedKey()) {
|
||||
clear()
|
||||
@ -312,7 +308,6 @@ open class CardFeedViewModel(val localFilter: FeedFilter<Note>) : ViewModel() {
|
||||
}
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalTime::class)
|
||||
fun invalidateInsertData(newItems: Set<Note>) {
|
||||
bundlerInsert.invalidateList(newItems) {
|
||||
val newObjects = it.flatten().toSet()
|
||||
|
@ -236,7 +236,7 @@ abstract class FeedViewModel(val localFilter: FeedFilter<Note>) : ViewModel(), I
|
||||
checkNotInMainThread()
|
||||
|
||||
lastFeedKey = localFilter.feedKey()
|
||||
val notes = localFilter.loadTop().toImmutableList()
|
||||
val notes = localFilter.loadTop().distinctBy { it.idHex }.toImmutableList()
|
||||
|
||||
val oldNotesState = _feedContent.value
|
||||
if (oldNotesState is FeedState.Loaded) {
|
||||
@ -301,11 +301,13 @@ abstract class FeedViewModel(val localFilter: FeedFilter<Note>) : ViewModel(), I
|
||||
|
||||
fun checkKeysInvalidateDataAndSendToTop() {
|
||||
if (lastFeedKey != localFilter.feedKey()) {
|
||||
bundler.invalidate(false) {
|
||||
// adds the time to perform the refresh into this delay
|
||||
// holding off new updates in case of heavy refresh routines.
|
||||
refreshSuspended()
|
||||
sendToTop()
|
||||
viewModelScope.launch(Dispatchers.IO) {
|
||||
bundler.invalidate(false) {
|
||||
// adds the time to perform the refresh into this delay
|
||||
// holding off new updates in case of heavy refresh routines.
|
||||
refreshSuspended()
|
||||
sendToTop()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -16,20 +16,31 @@ import com.vitorpamplona.amethyst.model.Account
|
||||
import com.vitorpamplona.amethyst.model.AccountState
|
||||
import com.vitorpamplona.amethyst.model.AddressableNote
|
||||
import com.vitorpamplona.amethyst.model.ConnectivityType
|
||||
import com.vitorpamplona.amethyst.model.LocalCache
|
||||
import com.vitorpamplona.amethyst.model.Note
|
||||
import com.vitorpamplona.amethyst.model.UrlCachedPreviewer
|
||||
import com.vitorpamplona.amethyst.model.User
|
||||
import com.vitorpamplona.amethyst.model.UserState
|
||||
import com.vitorpamplona.amethyst.service.Nip05Verifier
|
||||
import com.vitorpamplona.amethyst.service.OnlineChecker
|
||||
import com.vitorpamplona.amethyst.service.lnurl.LightningAddressResolver
|
||||
import com.vitorpamplona.amethyst.ui.components.UrlPreviewState
|
||||
import com.vitorpamplona.amethyst.ui.note.ZapAmountCommentNotification
|
||||
import com.vitorpamplona.amethyst.ui.note.ZapraiserStatus
|
||||
import com.vitorpamplona.amethyst.ui.note.showAmount
|
||||
import com.vitorpamplona.quartz.encoders.HexKey
|
||||
import com.vitorpamplona.quartz.events.BookmarkListEvent
|
||||
import com.vitorpamplona.quartz.events.DeletionEvent
|
||||
import com.vitorpamplona.quartz.events.Event
|
||||
import com.vitorpamplona.quartz.events.GiftWrapEvent
|
||||
import com.vitorpamplona.quartz.events.LnZapEvent
|
||||
import com.vitorpamplona.quartz.events.LnZapRequestEvent
|
||||
import com.vitorpamplona.quartz.events.PayInvoiceErrorResponse
|
||||
import com.vitorpamplona.quartz.events.ReactionEvent
|
||||
import com.vitorpamplona.quartz.events.ReportEvent
|
||||
import com.vitorpamplona.quartz.events.SealedGossipEvent
|
||||
import com.vitorpamplona.quartz.events.UserMetadata
|
||||
import com.vitorpamplona.quartz.utils.TimeUtils
|
||||
import kotlinx.collections.immutable.ImmutableSet
|
||||
import kotlinx.collections.immutable.persistentSetOf
|
||||
import kotlinx.collections.immutable.toImmutableSet
|
||||
@ -128,21 +139,86 @@ class AccountViewModel(val account: Account) : ViewModel() {
|
||||
return account.delete(account.boostsTo(note), signEvent)
|
||||
}
|
||||
|
||||
fun calculateIfNoteWasZappedByAccount(zappedNote: Note): Boolean {
|
||||
return account.calculateIfNoteWasZappedByAccount(zappedNote)
|
||||
fun calculateIfNoteWasZappedByAccount(zappedNote: Note, onWasZapped: (Boolean) -> Unit) {
|
||||
viewModelScope.launch(Dispatchers.Default) {
|
||||
onWasZapped(account.calculateIfNoteWasZappedByAccount(zappedNote))
|
||||
}
|
||||
}
|
||||
|
||||
fun calculateZapAmount(zappedNote: Note): BigDecimal {
|
||||
suspend fun calculateZapAmount(zappedNote: Note): BigDecimal {
|
||||
return account.calculateZappedAmount(zappedNote)
|
||||
}
|
||||
|
||||
fun calculateZapAmount(zappedNote: Note, onZapAmount: (String) -> Unit) {
|
||||
viewModelScope.launch(Dispatchers.IO) {
|
||||
onZapAmount(showAmount(account.calculateZappedAmount(zappedNote)))
|
||||
}
|
||||
}
|
||||
|
||||
fun calculateZapraiser(zappedNote: Note, onZapraiserStatus: (ZapraiserStatus) -> Unit) {
|
||||
viewModelScope.launch(Dispatchers.IO) {
|
||||
val zapraiserAmount = zappedNote.event?.zapraiserAmount() ?: 0
|
||||
val newZapAmount = calculateZapAmount(zappedNote)
|
||||
var percentage = newZapAmount.div(zapraiserAmount.toBigDecimal()).toFloat()
|
||||
|
||||
if (percentage > 1) {
|
||||
percentage = 1f
|
||||
}
|
||||
|
||||
val newZapraiserProgress = percentage
|
||||
val newZapraiserLeft = if (percentage > 0.99) {
|
||||
"0"
|
||||
} else {
|
||||
showAmount((zapraiserAmount * (1 - percentage)).toBigDecimal())
|
||||
}
|
||||
onZapraiserStatus(ZapraiserStatus(newZapraiserProgress, newZapraiserLeft))
|
||||
}
|
||||
}
|
||||
|
||||
fun decryptAmountMessage(
|
||||
zapRequest: Note,
|
||||
zapEvent: Note?,
|
||||
onNewState: (ZapAmountCommentNotification?) -> Unit
|
||||
) {
|
||||
viewModelScope.launch(Dispatchers.IO) {
|
||||
onNewState(innerDecryptAmountMessage(zapRequest, zapEvent))
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun innerDecryptAmountMessage(
|
||||
zapRequest: Note,
|
||||
zapEvent: Note?
|
||||
): ZapAmountCommentNotification? {
|
||||
(zapRequest.event as? LnZapRequestEvent)?.let {
|
||||
val decryptedContent = decryptZap(zapRequest)
|
||||
val amount = (zapEvent?.event as? LnZapEvent)?.amount
|
||||
if (decryptedContent != null) {
|
||||
val newAuthor = LocalCache.getOrCreateUser(decryptedContent.pubKey)
|
||||
return ZapAmountCommentNotification(
|
||||
newAuthor,
|
||||
decryptedContent.content.ifBlank { null },
|
||||
showAmountAxis(amount)
|
||||
)
|
||||
} else {
|
||||
if (!zapRequest.event?.content().isNullOrBlank() || amount != null) {
|
||||
return ZapAmountCommentNotification(
|
||||
zapRequest.author,
|
||||
zapRequest.event?.content()?.ifBlank { null },
|
||||
showAmountAxis(amount)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
fun zap(note: Note, amount: Long, pollOption: Int?, message: String, context: Context, onError: (String) -> Unit, onProgress: (percent: Float) -> Unit, zapType: LnZapEvent.ZapType) {
|
||||
viewModelScope.launch(Dispatchers.IO) {
|
||||
innerZap(note, amount, pollOption, message, context, onError, onProgress, zapType)
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun innerZap(note: Note, amount: Long, pollOption: Int?, message: String, context: Context, onError: (String) -> Unit, onProgress: (percent: Float) -> Unit, zapType: LnZapEvent.ZapType) {
|
||||
private suspend fun innerZap(note: Note, amount: Long, pollOption: Int?, message: String, context: Context, onError: (String) -> Unit, onProgress: (percent: Float) -> Unit, zapType: LnZapEvent.ZapType) {
|
||||
val lud16 = note.event?.zapAddress() ?: note.author?.info?.lud16?.trim() ?: note.author?.info?.lud06?.trim()
|
||||
|
||||
if (lud16.isNullOrBlank()) {
|
||||
@ -193,9 +269,12 @@ class AccountViewModel(val account: Account) : ViewModel() {
|
||||
onProgress(0f)
|
||||
}
|
||||
} else {
|
||||
runCatching {
|
||||
try {
|
||||
val intent = Intent(Intent.ACTION_VIEW, Uri.parse("lightning:$it"))
|
||||
intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK
|
||||
ContextCompat.startActivity(context, intent, null)
|
||||
} catch (e: Exception) {
|
||||
onError(context.getString(R.string.lightning_wallets_not_found))
|
||||
}
|
||||
onProgress(0f)
|
||||
}
|
||||
@ -347,24 +426,26 @@ class AccountViewModel(val account: Account) : ViewModel() {
|
||||
}
|
||||
|
||||
fun isNoteAcceptable(note: Note, onReady: (Boolean, Boolean, ImmutableSet<Note>) -> Unit) {
|
||||
val isFromLoggedIn = note.author?.pubkeyHex == userProfile().pubkeyHex
|
||||
val isFromLoggedInFollow = note.author?.let { userProfile().isFollowingCached(it) } ?: true
|
||||
viewModelScope.launch {
|
||||
val isFromLoggedIn = note.author?.pubkeyHex == userProfile().pubkeyHex
|
||||
val isFromLoggedInFollow = note.author?.let { userProfile().isFollowingCached(it) } ?: true
|
||||
|
||||
if (isFromLoggedIn || isFromLoggedInFollow) {
|
||||
// No need to process if from trusted people
|
||||
onReady(true, true, persistentSetOf())
|
||||
} else {
|
||||
val newCanPreview = !note.hasAnyReports()
|
||||
|
||||
val newIsAcceptable = account.isAcceptable(note)
|
||||
|
||||
if (newCanPreview && newIsAcceptable) {
|
||||
// No need to process reports if nothing is wrong
|
||||
if (isFromLoggedIn || isFromLoggedInFollow) {
|
||||
// No need to process if from trusted people
|
||||
onReady(true, true, persistentSetOf())
|
||||
} else {
|
||||
val newRelevantReports = account.getRelevantReports(note)
|
||||
val newCanPreview = !note.hasAnyReports()
|
||||
|
||||
onReady(newIsAcceptable, newCanPreview, newRelevantReports.toImmutableSet())
|
||||
val newIsAcceptable = account.isAcceptable(note)
|
||||
|
||||
if (newCanPreview && newIsAcceptable) {
|
||||
// No need to process reports if nothing is wrong
|
||||
onReady(true, true, persistentSetOf())
|
||||
} else {
|
||||
val newRelevantReports = account.getRelevantReports(note)
|
||||
|
||||
onReady(newIsAcceptable, newCanPreview, newRelevantReports.toImmutableSet())
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -395,11 +476,72 @@ class AccountViewModel(val account: Account) : ViewModel() {
|
||||
}
|
||||
|
||||
fun createStatus(newStatus: String) {
|
||||
account.createStatus(newStatus)
|
||||
viewModelScope.launch(Dispatchers.IO) {
|
||||
account.createStatus(newStatus)
|
||||
}
|
||||
}
|
||||
|
||||
fun updateStatus(it: AddressableNote, newStatus: String) {
|
||||
account.updateStatus(it, newStatus)
|
||||
viewModelScope.launch(Dispatchers.IO) {
|
||||
account.updateStatus(it, newStatus)
|
||||
}
|
||||
}
|
||||
|
||||
fun deleteStatus(it: AddressableNote) {
|
||||
viewModelScope.launch(Dispatchers.IO) {
|
||||
account.deleteStatus(it)
|
||||
}
|
||||
}
|
||||
|
||||
fun checkIfOnline(url: String, onResult: (Boolean) -> Unit) {
|
||||
viewModelScope.launch(Dispatchers.IO) {
|
||||
val isOnline = OnlineChecker.isOnline(url)
|
||||
onResult(isOnline)
|
||||
}
|
||||
}
|
||||
|
||||
fun urlPreview(url: String, onResult: suspend (UrlPreviewState) -> Unit) {
|
||||
viewModelScope.launch(Dispatchers.IO) {
|
||||
UrlCachedPreviewer.previewInfo(url, onResult)
|
||||
}
|
||||
}
|
||||
|
||||
fun loadReactionTo(note: Note?, onNewReactionType: (String?) -> Unit) {
|
||||
if (note == null) return
|
||||
|
||||
viewModelScope.launch(Dispatchers.Default) {
|
||||
onNewReactionType(note.getReactionBy(userProfile()))
|
||||
}
|
||||
}
|
||||
|
||||
fun verifyNip05(userMetadata: UserMetadata, pubkeyHex: String, onResult: (Boolean) -> Unit) {
|
||||
val nip05 = userMetadata.nip05?.ifBlank { null } ?: return
|
||||
|
||||
viewModelScope.launch(Dispatchers.IO) {
|
||||
Nip05Verifier().verifyNip05(
|
||||
nip05,
|
||||
onSuccess = {
|
||||
// Marks user as verified
|
||||
if (it == pubkeyHex) {
|
||||
userMetadata.nip05Verified = true
|
||||
userMetadata.nip05LastVerificationTime = TimeUtils.now()
|
||||
|
||||
onResult(userMetadata.nip05Verified)
|
||||
} else {
|
||||
userMetadata.nip05Verified = false
|
||||
userMetadata.nip05LastVerificationTime = 0
|
||||
|
||||
onResult(userMetadata.nip05Verified)
|
||||
}
|
||||
},
|
||||
onError = {
|
||||
userMetadata.nip05LastVerificationTime = 0
|
||||
userMetadata.nip05Verified = false
|
||||
|
||||
onResult(userMetadata.nip05Verified)
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
class Factory(val account: Account) : ViewModelProvider.Factory {
|
||||
|
@ -626,7 +626,7 @@ fun ShowVideoStreaming(
|
||||
}
|
||||
|
||||
url?.let {
|
||||
CheckIfUrlIsOnline(url) {
|
||||
CheckIfUrlIsOnline(url, accountViewModel) {
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
modifier = remember { Modifier.heightIn(max = 300.dp) }
|
||||
@ -896,7 +896,7 @@ private fun ShortChannelActionOptions(
|
||||
Spacer(modifier = StdHorzSpacer)
|
||||
LikeReaction(baseNote = it, grayTint = MaterialTheme.colors.onSurface, accountViewModel = accountViewModel, nav)
|
||||
Spacer(modifier = StdHorzSpacer)
|
||||
ZapReaction(baseNote = it, grayTint = MaterialTheme.colors.onSurface, accountViewModel = accountViewModel)
|
||||
ZapReaction(baseNote = it, grayTint = MaterialTheme.colors.onSurface, accountViewModel = accountViewModel, nav = nav)
|
||||
Spacer(modifier = StdHorzSpacer)
|
||||
}
|
||||
}
|
||||
@ -971,7 +971,7 @@ private fun LiveChannelActionOptions(
|
||||
|
||||
LikeReaction(baseNote = it, grayTint = MaterialTheme.colors.onSurface, accountViewModel = accountViewModel, nav)
|
||||
Spacer(modifier = StdHorzSpacer)
|
||||
ZapReaction(baseNote = it, grayTint = MaterialTheme.colors.onSurface, accountViewModel = accountViewModel)
|
||||
ZapReaction(baseNote = it, grayTint = MaterialTheme.colors.onSurface, accountViewModel = accountViewModel, nav = nav)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -106,6 +106,7 @@ import kotlinx.coroutines.withContext
|
||||
@Composable
|
||||
fun ChatroomScreen(
|
||||
roomId: String?,
|
||||
draftMessage: String? = null,
|
||||
accountViewModel: AccountViewModel,
|
||||
nav: (String) -> Unit
|
||||
) {
|
||||
@ -115,6 +116,7 @@ fun ChatroomScreen(
|
||||
it?.let {
|
||||
PrepareChatroomViewModels(
|
||||
room = it,
|
||||
draftMessage = draftMessage,
|
||||
accountViewModel = accountViewModel,
|
||||
nav = nav
|
||||
)
|
||||
@ -125,6 +127,7 @@ fun ChatroomScreen(
|
||||
@Composable
|
||||
fun ChatroomScreenByAuthor(
|
||||
authorPubKeyHex: String?,
|
||||
draftMessage: String? = null,
|
||||
accountViewModel: AccountViewModel,
|
||||
nav: (String) -> Unit
|
||||
) {
|
||||
@ -134,6 +137,7 @@ fun ChatroomScreenByAuthor(
|
||||
it?.let {
|
||||
PrepareChatroomViewModels(
|
||||
room = it,
|
||||
draftMessage = draftMessage,
|
||||
accountViewModel = accountViewModel,
|
||||
nav = nav
|
||||
)
|
||||
@ -171,7 +175,12 @@ fun LoadRoomByAuthor(authorPubKeyHex: String, accountViewModel: AccountViewModel
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun PrepareChatroomViewModels(room: ChatroomKey, accountViewModel: AccountViewModel, nav: (String) -> Unit) {
|
||||
fun PrepareChatroomViewModels(
|
||||
room: ChatroomKey,
|
||||
draftMessage: String?,
|
||||
accountViewModel: AccountViewModel,
|
||||
nav: (String) -> Unit
|
||||
) {
|
||||
val feedViewModel: NostrChatroomFeedViewModel = viewModel(
|
||||
key = room.hashCode().toString() + "ChatroomViewModels",
|
||||
factory = NostrChatroomFeedViewModel.Factory(
|
||||
@ -198,6 +207,12 @@ fun PrepareChatroomViewModels(room: ChatroomKey, accountViewModel: AccountViewMo
|
||||
}
|
||||
}
|
||||
|
||||
if (draftMessage != null) {
|
||||
LaunchedEffect(key1 = draftMessage) {
|
||||
newPostModel.message = TextFieldValue(draftMessage)
|
||||
}
|
||||
}
|
||||
|
||||
ChatroomScreen(
|
||||
room = room,
|
||||
feedViewModel = feedViewModel,
|
||||
|
@ -29,7 +29,6 @@ import androidx.lifecycle.Lifecycle
|
||||
import androidx.lifecycle.LifecycleEventObserver
|
||||
import com.vitorpamplona.amethyst.R
|
||||
import com.vitorpamplona.amethyst.service.NostrHomeDataSource
|
||||
import com.vitorpamplona.amethyst.service.OnlineChecker
|
||||
import com.vitorpamplona.amethyst.ui.navigation.Route
|
||||
import com.vitorpamplona.amethyst.ui.note.UpdateZapAmountDialog
|
||||
import com.vitorpamplona.amethyst.ui.screen.FeedViewModel
|
||||
@ -42,7 +41,6 @@ import com.vitorpamplona.amethyst.ui.screen.rememberForeverPagerState
|
||||
import com.vitorpamplona.amethyst.ui.theme.TabRowHeight
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
import kotlinx.collections.immutable.toImmutableList
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
@OptIn(ExperimentalFoundationApi::class)
|
||||
@ -136,12 +134,14 @@ private fun HomePages(
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun CheckIfUrlIsOnline(url: String, whenOnline: @Composable () -> Unit) {
|
||||
fun CheckIfUrlIsOnline(url: String, accountViewModel: AccountViewModel, whenOnline: @Composable () -> Unit) {
|
||||
var online by remember { mutableStateOf(false) }
|
||||
|
||||
LaunchedEffect(key1 = url) {
|
||||
launch(Dispatchers.IO) {
|
||||
online = OnlineChecker.isOnline(url)
|
||||
accountViewModel.checkIfOnline(url) { isOnline ->
|
||||
if (online != isOnline) {
|
||||
online = isOnline
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -162,11 +162,9 @@ fun WatchAccountForHomeScreen(
|
||||
val followState by accountViewModel.account.userProfile().live().follows.observeAsState()
|
||||
|
||||
LaunchedEffect(accountViewModel, accountState?.account?.defaultHomeFollowList, followState) {
|
||||
launch(Dispatchers.IO) {
|
||||
NostrHomeDataSource.invalidateFilters()
|
||||
homeFeedViewModel.checkKeysInvalidateDataAndSendToTop()
|
||||
repliesFeedViewModel.checkKeysInvalidateDataAndSendToTop()
|
||||
}
|
||||
NostrHomeDataSource.invalidateFilters()
|
||||
homeFeedViewModel.checkKeysInvalidateDataAndSendToTop()
|
||||
repliesFeedViewModel.checkKeysInvalidateDataAndSendToTop()
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -73,42 +73,38 @@ fun LoadRedirectScreen(eventId: String?, accountViewModel: AccountViewModel, nav
|
||||
fun LoadRedirectScreen(baseNote: Note, accountViewModel: AccountViewModel, nav: (String) -> Unit) {
|
||||
val noteState by baseNote.live().metadata.observeAsState()
|
||||
|
||||
val scope = rememberCoroutineScope()
|
||||
|
||||
LaunchedEffect(key1 = noteState) {
|
||||
scope.launch {
|
||||
val note = noteState?.note ?: return@launch
|
||||
var event = note.event
|
||||
val channelHex = note.channelHex()
|
||||
val note = noteState?.note ?: return@LaunchedEffect
|
||||
var event = note.event
|
||||
val channelHex = note.channelHex()
|
||||
|
||||
if (event is GiftWrapEvent) {
|
||||
event = accountViewModel.unwrap(event)
|
||||
}
|
||||
if (event is GiftWrapEvent) {
|
||||
event = accountViewModel.unwrap(event)
|
||||
}
|
||||
|
||||
if (event is SealedGossipEvent) {
|
||||
event = accountViewModel.unseal(event)
|
||||
}
|
||||
if (event is SealedGossipEvent) {
|
||||
event = accountViewModel.unseal(event)
|
||||
}
|
||||
|
||||
if (event == null) {
|
||||
// stay here, loading
|
||||
} else if (event is ChannelCreateEvent) {
|
||||
nav("Channel/${note.idHex}")
|
||||
} else if (event is ChatroomKeyable) {
|
||||
note.author?.let {
|
||||
val withKey = (event as ChatroomKeyable)
|
||||
.chatroomKey(accountViewModel.userProfile().pubkeyHex)
|
||||
if (event == null) {
|
||||
// stay here, loading
|
||||
} else if (event is ChannelCreateEvent) {
|
||||
nav("Channel/${note.idHex}")
|
||||
} else if (event is ChatroomKeyable) {
|
||||
note.author?.let {
|
||||
val withKey = (event as ChatroomKeyable)
|
||||
.chatroomKey(accountViewModel.userProfile().pubkeyHex)
|
||||
|
||||
withContext(Dispatchers.IO) {
|
||||
accountViewModel.userProfile().createChatroom(withKey)
|
||||
}
|
||||
|
||||
nav("Room/${withKey.hashCode()}")
|
||||
withContext(Dispatchers.IO) {
|
||||
accountViewModel.userProfile().createChatroom(withKey)
|
||||
}
|
||||
} else if (channelHex != null) {
|
||||
nav("Channel/$channelHex")
|
||||
} else {
|
||||
nav("Note/${note.idHex}")
|
||||
|
||||
nav("Room/${withKey.hashCode()}")
|
||||
}
|
||||
} else if (channelHex != null) {
|
||||
nav("Channel/$channelHex")
|
||||
} else {
|
||||
nav("Note/${note.idHex}")
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -955,7 +955,7 @@ private fun DrawAdditionalInfo(
|
||||
|
||||
DisplayBadges(baseUser, nav)
|
||||
|
||||
DisplayNip05ProfileStatus(user)
|
||||
DisplayNip05ProfileStatus(user, accountViewModel)
|
||||
|
||||
val website = user.info?.website
|
||||
if (!website.isNullOrEmpty()) {
|
||||
@ -1085,9 +1085,18 @@ fun DisplayLNAddress(
|
||||
}
|
||||
}
|
||||
} else {
|
||||
runCatching {
|
||||
try {
|
||||
val intent = Intent(Intent.ACTION_VIEW, Uri.parse("lightning:$it"))
|
||||
intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK
|
||||
ContextCompat.startActivity(context, intent, null)
|
||||
} catch (e: Exception) {
|
||||
scope.launch {
|
||||
Toast.makeText(
|
||||
context,
|
||||
context.getString(R.string.lightning_wallets_not_found),
|
||||
Toast.LENGTH_LONG
|
||||
).show()
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
|
@ -26,9 +26,10 @@ fun ThreadScreen(noteId: String?, accountViewModel: AccountViewModel, nav: (Stri
|
||||
factory = NostrThreadFeedViewModel.Factory(noteId)
|
||||
)
|
||||
|
||||
NostrThreadDataSource.loadThread(noteId)
|
||||
|
||||
LaunchedEffect(noteId) {
|
||||
NostrThreadDataSource.loadThread(noteId)
|
||||
feedViewModel.invalidateData()
|
||||
feedViewModel.invalidateData(true)
|
||||
}
|
||||
|
||||
DisposableEffect(accountViewModel) {
|
||||
@ -37,7 +38,7 @@ fun ThreadScreen(noteId: String?, accountViewModel: AccountViewModel, nav: (Stri
|
||||
println("Thread Start")
|
||||
NostrThreadDataSource.loadThread(noteId)
|
||||
NostrThreadDataSource.start()
|
||||
feedViewModel.invalidateData()
|
||||
feedViewModel.invalidateData(true)
|
||||
}
|
||||
if (event == Lifecycle.Event.ON_PAUSE) {
|
||||
println("Thread Stop")
|
||||
|
@ -467,7 +467,7 @@ fun ReactionsColumn(baseNote: Note, accountViewModel: AccountViewModel, nav: (St
|
||||
wantsToQuote = baseNote
|
||||
}
|
||||
LikeReaction(baseNote, grayTint = MaterialTheme.colors.onBackground, accountViewModel, nav, iconSize = 40.dp, heartSize = Size35dp, 28.sp)
|
||||
ZapReaction(baseNote, grayTint = MaterialTheme.colors.onBackground, accountViewModel, iconSize = 40.dp, animationSize = Size35dp)
|
||||
ZapReaction(baseNote, grayTint = MaterialTheme.colors.onBackground, accountViewModel, iconSize = 40.dp, animationSize = Size35dp, nav = nav)
|
||||
ViewCountReaction(baseNote, grayTint = MaterialTheme.colors.onBackground, barChartSize = 39.dp, viewCountColorFilter = MaterialTheme.colors.onBackgroundColorFilter)
|
||||
}
|
||||
}
|
||||
|
@ -95,7 +95,7 @@
|
||||
<string name="uploading">Chargement…</string>
|
||||
<string name="user_does_not_have_a_lightning_address_setup_to_receive_sats">L\'utilisateur n\'a pas configuré d\'adresse Lightning pour recevoir des sats</string>
|
||||
<string name="reply_here">"Répondre ici.. "</string>
|
||||
<string name="copies_the_note_id_to_the_clipboard_for_sharing">Copie l\'ID de note dans le presse-papiers pour le partage</string>
|
||||
<string name="copies_the_note_id_to_the_clipboard_for_sharing">Copie l\'ID de note dans le presse-papiers pour le partage sur Nostr</string>
|
||||
<string name="copy_channel_id_note_to_the_clipboard">Copier l\'ID de la chaîne (Note) dans le presse-papiers</string>
|
||||
<string name="edits_the_channel_metadata">Modifie les métadonnées du canal</string>
|
||||
<string name="join">Joindre</string>
|
||||
@ -474,9 +474,9 @@
|
||||
<string name="application_preferences">Préférences de l\'application</string>
|
||||
<string name="language">Langage</string>
|
||||
<string name="theme">Thème</string>
|
||||
<string name="automatically_load_images_gifs">Chargement automatique des images/gifs</string>
|
||||
<string name="automatically_play_videos">Démarrage automatique des vidéos</string>
|
||||
<string name="automatically_show_url_preview">Prévisualisation automatique des liens</string>
|
||||
<string name="automatically_load_images_gifs">Prévisualisation des images</string>
|
||||
<string name="automatically_play_videos">Lecture vidéo</string>
|
||||
<string name="automatically_show_url_preview">Prévisualisation des URLs</string>
|
||||
<string name="load_image">Charger l\'image</string>
|
||||
|
||||
<string name="spamming_users">Spammeurs</string>
|
||||
@ -499,4 +499,41 @@
|
||||
<string name="geohash_explainer">Ajoute un Geohash de votre emplacement au message. Le public saura que vous êtes à moins de 5km de l\'emplacement actuel</string>
|
||||
|
||||
<string name="add_sensitive_content_explainer">Ajoute un avertissement de contenu sensible avant de montrer votre contenu. C\'est idéal pour tout contenu NSFW ou contenu que certaines personnes peuvent trouver offensant ou dérangeant</string>
|
||||
|
||||
<string name="new_feature_nip24_might_not_be_available_title">Nouvelle Fonctionnalité</string>
|
||||
<string name="new_feature_nip24_might_not_be_available_description">Pour activer ce mode, Amethyst doit envoyer un message NIP-24 (GiftWrapped, Sealed Direct et Group Messages). Le protocole NIP-24 est nouveau et la plupart des clients ne l\'ont pas encore mis en oeuvre. Assurez-vous que le destinataire utilise un client compatible.</string>
|
||||
<string name="new_feature_nip24_activate">Activer</string>
|
||||
|
||||
<string name="messages_create_public_chat">Public</string>
|
||||
<string name="messages_new_message">Privé</string>
|
||||
<string name="messages_new_message_to">À</string>
|
||||
<string name="messages_new_message_subject">Sujet</string>
|
||||
<string name="messages_new_message_subject_caption">Sujet de la conversation</string>
|
||||
<string name="messages_new_message_to_caption">"@Utilisateur1, @Utilisateur2, @Utilisateur3"</string>
|
||||
|
||||
<string name="messages_group_descriptor">Membres de ce groupe</string>
|
||||
<string name="messages_new_subject_message">Explication aux membres</string>
|
||||
<string name="messages_new_subject_message_placeholder">Changement de nom pour les nouveaux objectifs.</string>
|
||||
|
||||
<string name="language_description">Pour l\'interface de l\'App</string>
|
||||
<string name="theme_description">Sombre, Clair ou thème Système</string>
|
||||
<string name="automatically_load_images_gifs_description">Charger automatiquement les images et les GIFs</string>
|
||||
<string name="automatically_play_videos_description">Lire automatiquement les vidéos et les GIFs</string>
|
||||
<string name="automatically_show_url_preview_description">Afficher la prévisualisation d\'URL</string>
|
||||
<string name="load_image_description">Quand charger les images</string>
|
||||
|
||||
<string name="copy_url_to_clipboard">Copier l\'URL dans le presse-papiers</string>
|
||||
<string name="copy_the_note_id_to_the_clipboard">Copier l\'ID de la note dans le presse-papiers</string>
|
||||
|
||||
<string name="created_at">Créé le</string>
|
||||
<string name="rules">Règles</string>
|
||||
|
||||
<string name="status_update">Mettez à jour votre statut</string>
|
||||
|
||||
<string name="lightning_wallets_not_found">Erreur d\'analyse du message d\'erreur</string>
|
||||
<string name="poll_zap_value_min_max_explainer">Les votes sont pondérés par le montant du zap. Vous pouvez définir un montant minimum pour éviter les spammeurs et un montant maximum pour éviter qu\'un grand nombre de zappeurs ne prenne le contrôle du sondage. Utilisez le même montant dans les deux champs pour vous assurer que chaque vote a la même valeur. Laissez le champ vide pour accepter n\'importe quel montant.</string>
|
||||
|
||||
<string name="error_dialog_zap_error">Impossible d\'envoyer un zap</string>
|
||||
<string name="error_dialog_talk_to_user">Contacter l\'Utilisateur</string>
|
||||
<string name="error_dialog_button_ok">Ok</string>
|
||||
</resources>
|
||||
|
@ -356,7 +356,7 @@
|
||||
\n4. Ha szükséges az Orbot-ban változtasd meg a portot
|
||||
\n5. Ezen a felületen állítsd be a Socks portot
|
||||
\n6. Kattíts az Aktiválás gombra, hogy az Orbot-ot átjátszóként használd
|
||||
</string>
|
||||
</string>ít
|
||||
<string name="orbot_socks_port">Orbot Socks portja</string>
|
||||
<string name="invalid_port_number">Érvénytelen Port szám</string>
|
||||
<string name="use_orbot">Használd az Orbot-ot</string>
|
||||
@ -476,4 +476,48 @@
|
||||
<string name="nip05_verified">A Nostr cím ellenőrzésre került</string>
|
||||
<string name="nip05_failed">A Nostr cím ellenőrzése sikeretelen</string>
|
||||
<string name="nip05_checking">A Nostr cím ellenőrzése</string>
|
||||
<string name="select_deselect_all">Mind kijelölése/kijelölés visszavonása</string>
|
||||
<string name="default_relays">Alapértelmezett</string>
|
||||
<string name="select_a_relay_to_continue">A folytatáshoz válassz egy csomópontot</string>
|
||||
|
||||
<string name="zap_forward_title">Zap-ek továbbítása:</string>
|
||||
<string name="zap_forward_explainer">A funkciót támogató kliensek a Zap-eket az Ön tárcája helyett, az alábbi LN-címre vagy felhasználói profilra továbbítják</string>
|
||||
|
||||
<string name="geohash_title">A Hely megjelenítése mint </string>
|
||||
<string name="geohash_explainer">A bejegyzéshez az Ön tartózkodási helyének Geohash-ét hozzáadja. A közönség tudni fogja, hogy az aktuális helytől 5 km-en (3 mérföldön) belül van</string>
|
||||
|
||||
<string name="add_sensitive_content_explainer">A kényes tartalom miatt, azon megjelenítése előtt figyelmeztetést ad. Ez ideális minden Felnőtt tartalomhoz vagy olyan tartalomhoz, amelyet egyesek sértőnek vagy zavarónak találhatnak</string>
|
||||
|
||||
<string name="new_feature_nip24_might_not_be_available_title">Új funkció</string>
|
||||
<string name="new_feature_nip24_might_not_be_available_description">Az Amethystnek ennek a módnak az aktiválásához NIP-24 üzenetet kell küldenie (GiftWrapped, Zárolt direkt és csoportos üzeneteket). A NIP-24 új, és a legtöbb kliens még nem implementálta. Győződj meg arról, hogy a fogadó fél kompatibilis klienst használ.</string>
|
||||
<string name="new_feature_nip24_activate">Aktiválás</string>
|
||||
|
||||
<string name="messages_create_public_chat">Publikus</string>
|
||||
<string name="messages_new_message">Privát</string>
|
||||
<string name="messages_new_message_to">Címzett</string>
|
||||
<string name="messages_new_message_subject">Téma</string>
|
||||
<string name="messages_new_message_subject_caption">A beszélgetés témája</string>
|
||||
<string name="messages_new_message_to_caption">"@Felhasználó1, @Felhasználó2, @UFelhasználó3"</string>
|
||||
|
||||
<string name="messages_group_descriptor">A csoport tagjai</string>
|
||||
<string name="messages_new_subject_message">Magyarázat a csoport tagjainak</string>
|
||||
<string name="messages_new_subject_message_placeholder">Az új célok érdekében, a név megváltoztatása.</string>
|
||||
|
||||
<string name="language_description">Az applikáció felülete</string>
|
||||
<string name="theme_description">Sötét, Világos vagy Rendszer által használt téma</string>
|
||||
<string name="automatically_load_images_gifs_description">Képek és GIF-ek automatikus betöltése</string>
|
||||
<string name="automatically_play_videos_description">A videók és a GIF-ek automatikus lejátszása</string>
|
||||
<string name="automatically_show_url_preview_description">URL előnézetek megjelenítése</string>
|
||||
<string name="load_image_description">Mikor kell a képeket betölteni</string>
|
||||
|
||||
<string name="copy_url_to_clipboard">Az URL vágólapra másolása</string>
|
||||
<string name="copy_the_note_id_to_the_clipboard">A bejegyzésazonosító vágólapra másolása</string>
|
||||
|
||||
<string name="created_at">Létrehozva</string>
|
||||
<string name="rules">Szabályok</string>
|
||||
|
||||
<string name="status_update">Állapotod változtatása</string>
|
||||
|
||||
<string name="lightning_wallets_not_found">Hiba a hibaüzenet elemzésekor</string>
|
||||
<string name="poll_zap_value_min_max_explainer">A szavazatokat a Zap-ek összegével súlyozzuk. Beállíthatsz egy minimális összeget, hogy a kéretlen leveleket elkerüld, és egy maximális összeget annak elkerülésére, hogy a szavazás feletti irányítást a nagy Zapperek vegyék át. Mindkét mezőben ugyanazt az összeget használd, hogy minden szavazat azonos értéket kapjon. Bármilyen összeg elfogadásához, hagyjd üresen.</string>
|
||||
</resources>
|
||||
|
@ -555,4 +555,11 @@
|
||||
<string name="login_with_amber">Login with Amber</string>
|
||||
|
||||
<string name="status_update">Update your status</string>
|
||||
|
||||
<string name="lightning_wallets_not_found">Error parsing error message</string>
|
||||
<string name="poll_zap_value_min_max_explainer">Votes are weighted by the zap amount. You can set a minimum amount to avoid spammers and a maximum amount to avoid a large zappers taking over the poll. Use the same amount in both fields to make sure every vote is valued the same amount. Leave it empty to accept any amount.</string>
|
||||
|
||||
<string name="error_dialog_zap_error">Unable to send zap</string>
|
||||
<string name="error_dialog_talk_to_user">Message the User</string>
|
||||
<string name="error_dialog_button_ok">Ok</string>
|
||||
</resources>
|
||||
|
@ -5,13 +5,13 @@ buildscript {
|
||||
fragment_version = "1.6.1"
|
||||
lifecycle_version = '2.6.1'
|
||||
compose_ui_version = '1.5.0'
|
||||
nav_version = "2.7.0"
|
||||
nav_version = "2.7.1"
|
||||
room_version = "2.4.3"
|
||||
accompanist_version = '0.30.1'
|
||||
coil_version = '2.4.0'
|
||||
vico_version = '1.9.2'
|
||||
vico_version = '1.11.0'
|
||||
exoplayer_version = '1.1.1'
|
||||
media3_version = '1.1.0'
|
||||
media3_version = '1.1.1'
|
||||
core_ktx_version = '1.10.1'
|
||||
}
|
||||
dependencies {
|
||||
|
@ -72,7 +72,7 @@ open class Event(
|
||||
override fun firstTaggedUser() = tags.firstOrNull { it.size > 1 && it[0] == "p" }?.let { it[1] }
|
||||
override fun firstTaggedEvent() = tags.firstOrNull { it.size > 1 && it[0] == "e" }?.let { it[1] }
|
||||
override fun firstTaggedUrl() = tags.firstOrNull { it.size > 1 && it[0] == "r" }?.let { it[1] }
|
||||
override fun firstTaggedAddress() = tags.firstOrNull { it.size > 1 && it[0] == "r" }?.let {
|
||||
override fun firstTaggedAddress() = tags.firstOrNull { it.size > 1 && it[0] == "a" }?.let {
|
||||
val aTagValue = it[1]
|
||||
val relay = it.getOrNull(2)
|
||||
|
||||
@ -218,7 +218,7 @@ open class Event(
|
||||
return try {
|
||||
hasCorrectIDHash() && hasVerifedSignature()
|
||||
} catch (e: Exception) {
|
||||
Log.e("Event", "Event $id does not have a valid signature: ${toJson()}", e)
|
||||
Log.w("Event", "Event $id does not have a valid signature: ${toJson()}", e)
|
||||
false
|
||||
}
|
||||
}
|
||||
|
@ -50,5 +50,18 @@ class StatusEvent(
|
||||
val sig = CryptoUtils.sign(id, privateKey)
|
||||
return StatusEvent(id.toHexKey(), pubKey, createdAt, tags, newStatus, sig.toHexKey())
|
||||
}
|
||||
|
||||
fun clear(
|
||||
event: StatusEvent,
|
||||
privateKey: ByteArray,
|
||||
createdAt: Long = TimeUtils.now()
|
||||
): StatusEvent {
|
||||
val msg = ""
|
||||
val tags = event.tags.filter { it.size > 1 && it[0] == "d" }
|
||||
val pubKey = event.pubKey()
|
||||
val id = generateId(pubKey, createdAt, kind, tags, msg)
|
||||
val sig = CryptoUtils.sign(id, privateKey)
|
||||
return StatusEvent(id.toHexKey(), pubKey, createdAt, tags, msg, sig.toHexKey())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user