mirror of
https://github.com/vitorpamplona/amethyst.git
synced 2025-03-26 17:52:29 +01:00
Adds private outbox relays to save Draft events.
This commit is contained in:
parent
6ecb5ecc47
commit
a38346c7d3
@ -89,6 +89,7 @@ import com.vitorpamplona.quartz.events.PeopleListEvent
|
||||
import com.vitorpamplona.quartz.events.PollNoteEvent
|
||||
import com.vitorpamplona.quartz.events.Price
|
||||
import com.vitorpamplona.quartz.events.PrivateDmEvent
|
||||
import com.vitorpamplona.quartz.events.PrivateOutboxRelayListEvent
|
||||
import com.vitorpamplona.quartz.events.ReactionEvent
|
||||
import com.vitorpamplona.quartz.events.RelayAuthEvent
|
||||
import com.vitorpamplona.quartz.events.ReportEvent
|
||||
@ -233,12 +234,14 @@ class Account(
|
||||
getNIP65RelayListFlow(),
|
||||
getDMRelayListFlow(),
|
||||
getSearchRelayListFlow(),
|
||||
getPrivateOutboxRelayListFlow(),
|
||||
userProfile().flow().relays.stateFlow,
|
||||
) { nip65RelayList, dmRelayList, searchRelayList, userProfile ->
|
||||
) { nip65RelayList, dmRelayList, searchRelayList, privateOutBox, userProfile ->
|
||||
val baseRelaySet = activeRelays() ?: convertLocalRelays()
|
||||
val newDMRelaySet = (dmRelayList.note.event as? ChatMessageRelayListEvent)?.relays()?.toSet() ?: emptySet()
|
||||
val searchRelaySet = (searchRelayList.note.event as? SearchRelayListEvent)?.relays()?.toSet() ?: Constants.defaultSearchRelaySet
|
||||
val nip65RelaySet = (nip65RelayList.note.event as? AdvertisedRelayListEvent)?.relays()
|
||||
val privateOutboxRelaySet = (privateOutBox.note.event as? PrivateOutboxRelayListEvent)?.relays() ?: emptySet()
|
||||
|
||||
var mappedRelaySet =
|
||||
baseRelaySet.map {
|
||||
@ -270,6 +273,21 @@ class Account(
|
||||
}
|
||||
}
|
||||
|
||||
mappedRelaySet =
|
||||
mappedRelaySet.map {
|
||||
if (privateOutboxRelaySet.contains(it.url) == true) {
|
||||
Relay(it.url, true, true, it.activeTypes + setOf(FeedType.FOLLOWS, FeedType.PUBLIC_CHATS, FeedType.GLOBAL, FeedType.PRIVATE_DMS))
|
||||
} else {
|
||||
it
|
||||
}
|
||||
}
|
||||
|
||||
privateOutboxRelaySet.forEach { newUrl ->
|
||||
if (mappedRelaySet.filter { it.url == newUrl }.isEmpty()) {
|
||||
mappedRelaySet = mappedRelaySet + Relay(newUrl, true, true, setOf(FeedType.FOLLOWS, FeedType.PUBLIC_CHATS, FeedType.GLOBAL, FeedType.PRIVATE_DMS))
|
||||
}
|
||||
}
|
||||
|
||||
mappedRelaySet =
|
||||
mappedRelaySet.map {
|
||||
if (localRelayServers.contains(it.url) == true) {
|
||||
@ -1416,7 +1434,12 @@ class Account(
|
||||
deleteDraft(draftTag)
|
||||
} else {
|
||||
DraftEvent.create(draftTag, it, emptyList(), signer) { draftEvent ->
|
||||
Client.send(draftEvent, relayList = relayList)
|
||||
val newRelayList = getPrivateOutboxRelayList()?.relays()
|
||||
if (newRelayList != null) {
|
||||
Client.sendPrivately(draftEvent, newRelayList)
|
||||
} else {
|
||||
Client.send(draftEvent, relayList = relayList)
|
||||
}
|
||||
LocalCache.justConsume(draftEvent, null)
|
||||
}
|
||||
}
|
||||
@ -1480,7 +1503,12 @@ class Account(
|
||||
deleteDraft(draftTag)
|
||||
} else {
|
||||
DraftEvent.create(draftTag, it, signer) { draftEvent ->
|
||||
Client.send(draftEvent, relayList = relayList)
|
||||
val newRelayList = getPrivateOutboxRelayList()?.relays()
|
||||
if (newRelayList != null) {
|
||||
Client.sendPrivately(draftEvent, newRelayList)
|
||||
} else {
|
||||
Client.send(draftEvent, relayList = relayList)
|
||||
}
|
||||
LocalCache.justConsume(draftEvent, null)
|
||||
}
|
||||
}
|
||||
@ -1506,15 +1534,15 @@ class Account(
|
||||
|
||||
fun deleteDraft(draftTag: String) {
|
||||
val key = DraftEvent.createAddressTag(userProfile().pubkeyHex, draftTag)
|
||||
LocalCache.getAddressableNoteIfExists(key)?.let {
|
||||
val noteEvent = it.event
|
||||
LocalCache.getAddressableNoteIfExists(key)?.let { note ->
|
||||
val noteEvent = note.event
|
||||
if (noteEvent is DraftEvent) {
|
||||
noteEvent.createDeletedEvent(signer) {
|
||||
Client.send(it)
|
||||
Client.sendPrivately(it, relayList = note.relays.map { it.url })
|
||||
LocalCache.justConsume(it, null)
|
||||
}
|
||||
}
|
||||
delete(it)
|
||||
delete(note)
|
||||
}
|
||||
}
|
||||
|
||||
@ -1564,7 +1592,12 @@ class Account(
|
||||
deleteDraft(draftTag)
|
||||
} else {
|
||||
DraftEvent.create(draftTag, it, signer) { draftEvent ->
|
||||
Client.send(draftEvent, relayList = relayList)
|
||||
val newRelayList = getPrivateOutboxRelayList()?.relays()
|
||||
if (newRelayList != null) {
|
||||
Client.sendPrivately(draftEvent, newRelayList)
|
||||
} else {
|
||||
Client.send(draftEvent, relayList = relayList)
|
||||
}
|
||||
LocalCache.justConsume(draftEvent, null)
|
||||
}
|
||||
}
|
||||
@ -1657,7 +1690,12 @@ class Account(
|
||||
deleteDraft(draftTag)
|
||||
} else {
|
||||
DraftEvent.create(draftTag, it, signer) { draftEvent ->
|
||||
Client.send(draftEvent, relayList = relayList)
|
||||
val newRelayList = getPrivateOutboxRelayList()?.relays()
|
||||
if (newRelayList != null) {
|
||||
Client.sendPrivately(draftEvent, newRelayList)
|
||||
} else {
|
||||
Client.send(draftEvent, relayList = relayList)
|
||||
}
|
||||
LocalCache.justConsume(draftEvent, null)
|
||||
}
|
||||
}
|
||||
@ -1711,7 +1749,12 @@ class Account(
|
||||
deleteDraft(draftTag)
|
||||
} else {
|
||||
DraftEvent.create(draftTag, it, signer) { draftEvent ->
|
||||
Client.send(draftEvent)
|
||||
val newRelayList = getPrivateOutboxRelayList()?.relays()
|
||||
if (newRelayList != null) {
|
||||
Client.sendPrivately(draftEvent, newRelayList)
|
||||
} else {
|
||||
Client.send(draftEvent)
|
||||
}
|
||||
LocalCache.justConsume(draftEvent, null)
|
||||
}
|
||||
}
|
||||
@ -1758,7 +1801,12 @@ class Account(
|
||||
deleteDraft(draftTag)
|
||||
} else {
|
||||
DraftEvent.create(draftTag, it, signer) { draftEvent ->
|
||||
Client.send(draftEvent)
|
||||
val newRelayList = getPrivateOutboxRelayList()?.relays()
|
||||
if (newRelayList != null) {
|
||||
Client.sendPrivately(draftEvent, newRelayList)
|
||||
} else {
|
||||
Client.send(draftEvent)
|
||||
}
|
||||
LocalCache.justConsume(draftEvent, null)
|
||||
}
|
||||
}
|
||||
@ -1832,7 +1880,12 @@ class Account(
|
||||
deleteDraft(draftTag)
|
||||
} else {
|
||||
DraftEvent.create(draftTag, it, emptyList(), signer) { draftEvent ->
|
||||
Client.send(draftEvent)
|
||||
val newRelayList = getPrivateOutboxRelayList()?.relays()
|
||||
if (newRelayList != null) {
|
||||
Client.sendPrivately(draftEvent, newRelayList)
|
||||
} else {
|
||||
Client.send(draftEvent)
|
||||
}
|
||||
LocalCache.justConsume(draftEvent, null)
|
||||
}
|
||||
}
|
||||
@ -1880,7 +1933,12 @@ class Account(
|
||||
deleteDraft(draftTag)
|
||||
} else {
|
||||
DraftEvent.create(draftTag, it.msg, emptyList(), signer) { draftEvent ->
|
||||
Client.send(draftEvent)
|
||||
val newRelayList = getPrivateOutboxRelayList()?.relays()
|
||||
if (newRelayList != null) {
|
||||
Client.sendPrivately(draftEvent, newRelayList)
|
||||
} else {
|
||||
Client.send(draftEvent)
|
||||
}
|
||||
LocalCache.justConsume(draftEvent, null)
|
||||
}
|
||||
}
|
||||
@ -2655,11 +2713,7 @@ class Account(
|
||||
fun saveDMRelayList(dmRelays: List<String>) {
|
||||
if (!isWriteable()) return
|
||||
|
||||
val relayListForDMs =
|
||||
LocalCache.getOrCreateAddressableNote(
|
||||
ChatMessageRelayListEvent.createAddressATag(signer.pubKey),
|
||||
).event as? ChatMessageRelayListEvent
|
||||
|
||||
val relayListForDMs = getDMRelayList()
|
||||
if (relayListForDMs != null && relayListForDMs.tags.isNotEmpty()) {
|
||||
ChatMessageRelayListEvent.updateRelayList(
|
||||
earlierVersion = relayListForDMs,
|
||||
@ -2680,6 +2734,45 @@ class Account(
|
||||
}
|
||||
}
|
||||
|
||||
fun getPrivateOutboxRelayListNote(): AddressableNote {
|
||||
return LocalCache.getOrCreateAddressableNote(
|
||||
PrivateOutboxRelayListEvent.createAddressATag(signer.pubKey),
|
||||
)
|
||||
}
|
||||
|
||||
fun getPrivateOutboxRelayListFlow(): StateFlow<NoteState> {
|
||||
return getPrivateOutboxRelayListNote().flow().metadata.stateFlow
|
||||
}
|
||||
|
||||
fun getPrivateOutboxRelayList(): PrivateOutboxRelayListEvent? {
|
||||
return getPrivateOutboxRelayListNote().event as? PrivateOutboxRelayListEvent
|
||||
}
|
||||
|
||||
fun savePrivateOutboxRelayList(relays: List<String>) {
|
||||
if (!isWriteable()) return
|
||||
|
||||
val relayListForPrivateOutbox = getPrivateOutboxRelayList()
|
||||
|
||||
if (relayListForPrivateOutbox != null && !relayListForPrivateOutbox.cachedPrivateTags().isNullOrEmpty()) {
|
||||
PrivateOutboxRelayListEvent.updateRelayList(
|
||||
earlierVersion = relayListForPrivateOutbox,
|
||||
relays = relays,
|
||||
signer = signer,
|
||||
) {
|
||||
Client.send(it)
|
||||
LocalCache.justConsume(it, null)
|
||||
}
|
||||
} else {
|
||||
PrivateOutboxRelayListEvent.createFromScratch(
|
||||
relays = relays,
|
||||
signer = signer,
|
||||
) {
|
||||
Client.send(it)
|
||||
LocalCache.justConsume(it, null)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun getSearchRelayListNote(): AddressableNote {
|
||||
return LocalCache.getOrCreateAddressableNote(
|
||||
SearchRelayListEvent.createAddressATag(signer.pubKey),
|
||||
|
@ -102,6 +102,7 @@ import com.vitorpamplona.quartz.events.PeopleListEvent
|
||||
import com.vitorpamplona.quartz.events.PinListEvent
|
||||
import com.vitorpamplona.quartz.events.PollNoteEvent
|
||||
import com.vitorpamplona.quartz.events.PrivateDmEvent
|
||||
import com.vitorpamplona.quartz.events.PrivateOutboxRelayListEvent
|
||||
import com.vitorpamplona.quartz.events.ReactionEvent
|
||||
import com.vitorpamplona.quartz.events.RecommendRelayEvent
|
||||
import com.vitorpamplona.quartz.events.RelaySetEvent
|
||||
@ -929,6 +930,13 @@ object LocalCache {
|
||||
consumeBaseReplaceable(event, relay)
|
||||
}
|
||||
|
||||
private fun consume(
|
||||
event: PrivateOutboxRelayListEvent,
|
||||
relay: Relay?,
|
||||
) {
|
||||
consumeBaseReplaceable(event, relay)
|
||||
}
|
||||
|
||||
private fun consume(
|
||||
event: SearchRelayListEvent,
|
||||
relay: Relay?,
|
||||
@ -2565,6 +2573,7 @@ object LocalCache {
|
||||
is NNSEvent -> comsume(event, relay)
|
||||
is OtsEvent -> consume(event, relay)
|
||||
is PrivateDmEvent -> consume(event, relay)
|
||||
is PrivateOutboxRelayListEvent -> consume(event, relay)
|
||||
is PinListEvent -> consume(event, relay)
|
||||
is PeopleListEvent -> consume(event, relay)
|
||||
is PollNoteEvent -> consume(event, relay)
|
||||
|
@ -58,6 +58,7 @@ import com.vitorpamplona.quartz.events.MetadataEvent
|
||||
import com.vitorpamplona.quartz.events.MuteListEvent
|
||||
import com.vitorpamplona.quartz.events.PeopleListEvent
|
||||
import com.vitorpamplona.quartz.events.PollNoteEvent
|
||||
import com.vitorpamplona.quartz.events.PrivateOutboxRelayListEvent
|
||||
import com.vitorpamplona.quartz.events.ReactionEvent
|
||||
import com.vitorpamplona.quartz.events.ReportEvent
|
||||
import com.vitorpamplona.quartz.events.RepostEvent
|
||||
@ -104,7 +105,7 @@ object NostrAccountDataSource : NostrDataSource("AccountData") {
|
||||
types = COMMON_FEED_TYPES,
|
||||
filter =
|
||||
JsonFilter(
|
||||
kinds = listOf(StatusEvent.KIND, AdvertisedRelayListEvent.KIND, ChatMessageRelayListEvent.KIND, SearchRelayListEvent.KIND),
|
||||
kinds = listOf(StatusEvent.KIND, AdvertisedRelayListEvent.KIND, ChatMessageRelayListEvent.KIND, SearchRelayListEvent.KIND, PrivateOutboxRelayListEvent.KIND),
|
||||
authors = listOf(account.userProfile().pubkeyHex),
|
||||
limit = 10,
|
||||
),
|
||||
@ -123,6 +124,7 @@ object NostrAccountDataSource : NostrDataSource("AccountData") {
|
||||
ContactListEvent.KIND,
|
||||
AdvertisedRelayListEvent.KIND,
|
||||
ChatMessageRelayListEvent.KIND,
|
||||
PrivateOutboxRelayListEvent.KIND,
|
||||
SearchRelayListEvent.KIND,
|
||||
FileServersEvent.KIND,
|
||||
MuteListEvent.KIND,
|
||||
@ -285,6 +287,16 @@ object NostrAccountDataSource : NostrDataSource("AccountData") {
|
||||
|
||||
if (LocalCache.justVerify(event)) {
|
||||
when (event) {
|
||||
is PrivateOutboxRelayListEvent -> {
|
||||
val note = LocalCache.getAddressableNoteIfExists(event.addressTag())
|
||||
val noteEvent = note?.event
|
||||
if (noteEvent == null || event.createdAt > noteEvent.createdAt()) {
|
||||
event.privateTags(account.signer) {
|
||||
LocalCache.justConsume(event, relay)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
is DraftEvent -> {
|
||||
// Avoid decrypting over and over again if the event already exist.
|
||||
|
||||
|
@ -74,6 +74,9 @@ fun AllRelayListView(
|
||||
val homeFeedState by nip65ViewModel.homeRelays.collectAsStateWithLifecycle()
|
||||
val notifFeedState by nip65ViewModel.notificationRelays.collectAsStateWithLifecycle()
|
||||
|
||||
val privateOutboxViewModel: PrivateOutboxRelayListViewModel = viewModel()
|
||||
val privateOutboxFeedState by privateOutboxViewModel.relays.collectAsStateWithLifecycle()
|
||||
|
||||
val searchViewModel: SearchRelayListViewModel = viewModel()
|
||||
val searchFeedState by searchViewModel.relays.collectAsStateWithLifecycle()
|
||||
|
||||
@ -86,6 +89,7 @@ fun AllRelayListView(
|
||||
nip65ViewModel.load(accountViewModel.account)
|
||||
searchViewModel.load(accountViewModel.account)
|
||||
localViewModel.load(accountViewModel.account)
|
||||
privateOutboxViewModel.load(accountViewModel.account)
|
||||
}
|
||||
|
||||
Dialog(
|
||||
@ -115,6 +119,7 @@ fun AllRelayListView(
|
||||
nip65ViewModel.create()
|
||||
searchViewModel.create()
|
||||
localViewModel.create()
|
||||
privateOutboxViewModel.create()
|
||||
onClose()
|
||||
},
|
||||
true,
|
||||
@ -127,6 +132,11 @@ fun AllRelayListView(
|
||||
CloseButton(
|
||||
onPress = {
|
||||
kind3ViewModel.clear()
|
||||
dmViewModel.clear()
|
||||
nip65ViewModel.clear()
|
||||
searchViewModel.clear()
|
||||
localViewModel.clear()
|
||||
privateOutboxViewModel.clear()
|
||||
onClose()
|
||||
},
|
||||
)
|
||||
@ -177,6 +187,14 @@ fun AllRelayListView(
|
||||
}
|
||||
renderDMItems(dmFeedState, dmViewModel, accountViewModel, onClose, nav)
|
||||
|
||||
item {
|
||||
SettingsCategory(
|
||||
stringResource(R.string.private_outbox_section),
|
||||
stringResource(R.string.private_outbox_section_explainer),
|
||||
)
|
||||
}
|
||||
renderPrivateOutboxItems(privateOutboxFeedState, privateOutboxViewModel, accountViewModel, onClose, nav)
|
||||
|
||||
item {
|
||||
SettingsCategoryWithButton(
|
||||
stringResource(R.string.search_section),
|
||||
|
@ -59,7 +59,7 @@ fun LazyListScope.renderLocalItems(
|
||||
onClose: () -> Unit,
|
||||
nav: (String) -> Unit,
|
||||
) {
|
||||
itemsIndexed(feedState, key = { _, item -> "DM" + item.url }) { index, item ->
|
||||
itemsIndexed(feedState, key = { _, item -> "Local" + item.url }) { index, item ->
|
||||
BasicRelaySetupInfoDialog(
|
||||
item,
|
||||
onDelete = { postViewModel.deleteRelay(item) },
|
||||
|
@ -0,0 +1,77 @@
|
||||
/**
|
||||
* Copyright (c) 2024 Vitor Pamplona
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
* this software and associated documentation files (the "Software"), to deal in
|
||||
* the Software without restriction, including without limitation the rights to use,
|
||||
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
|
||||
* Software, and to permit persons to whom the Software is furnished to do so,
|
||||
* subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
|
||||
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
package com.vitorpamplona.amethyst.ui.actions.relays
|
||||
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.LazyListScope
|
||||
import androidx.compose.foundation.lazy.itemsIndexed
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
||||
import com.vitorpamplona.amethyst.ui.theme.FeedPadding
|
||||
import com.vitorpamplona.amethyst.ui.theme.StdVertSpacer
|
||||
|
||||
@Composable
|
||||
fun PrivateOutboxRelayList(
|
||||
postViewModel: PrivateOutboxRelayListViewModel,
|
||||
accountViewModel: AccountViewModel,
|
||||
onClose: () -> Unit,
|
||||
nav: (String) -> Unit,
|
||||
) {
|
||||
val feedState by postViewModel.relays.collectAsStateWithLifecycle()
|
||||
|
||||
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||
LazyColumn(
|
||||
contentPadding = FeedPadding,
|
||||
) {
|
||||
renderPrivateOutboxItems(feedState, postViewModel, accountViewModel, onClose, nav)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun LazyListScope.renderPrivateOutboxItems(
|
||||
feedState: List<BasicRelaySetupInfo>,
|
||||
postViewModel: PrivateOutboxRelayListViewModel,
|
||||
accountViewModel: AccountViewModel,
|
||||
onClose: () -> Unit,
|
||||
nav: (String) -> Unit,
|
||||
) {
|
||||
itemsIndexed(feedState, key = { _, item -> "Outbox" + item.url }) { index, item ->
|
||||
BasicRelaySetupInfoDialog(
|
||||
item,
|
||||
onDelete = { postViewModel.deleteRelay(item) },
|
||||
accountViewModel = accountViewModel,
|
||||
) {
|
||||
onClose()
|
||||
nav(it)
|
||||
}
|
||||
}
|
||||
|
||||
item {
|
||||
Spacer(modifier = StdVertSpacer)
|
||||
RelayUrlEditField { postViewModel.addRelay(it) }
|
||||
}
|
||||
}
|
@ -0,0 +1,109 @@
|
||||
/**
|
||||
* Copyright (c) 2024 Vitor Pamplona
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
* this software and associated documentation files (the "Software"), to deal in
|
||||
* the Software without restriction, including without limitation the rights to use,
|
||||
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
|
||||
* Software, and to permit persons to whom the Software is furnished to do so,
|
||||
* subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
|
||||
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
package com.vitorpamplona.amethyst.ui.actions.relays
|
||||
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import com.vitorpamplona.amethyst.model.Account
|
||||
import com.vitorpamplona.amethyst.service.Nip11CachedRetriever
|
||||
import com.vitorpamplona.amethyst.service.relays.RelayPool
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.asStateFlow
|
||||
import kotlinx.coroutines.flow.update
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
class PrivateOutboxRelayListViewModel : ViewModel() {
|
||||
private lateinit var account: Account
|
||||
|
||||
private val _relays = MutableStateFlow<List<BasicRelaySetupInfo>>(emptyList())
|
||||
val relays = _relays.asStateFlow()
|
||||
|
||||
fun load(account: Account) {
|
||||
this.account = account
|
||||
clear()
|
||||
loadRelayDocuments()
|
||||
}
|
||||
|
||||
fun create() {
|
||||
viewModelScope.launch(Dispatchers.IO) {
|
||||
account.savePrivateOutboxRelayList(_relays.value.map { it.url })
|
||||
clear()
|
||||
}
|
||||
}
|
||||
|
||||
fun loadRelayDocuments() {
|
||||
viewModelScope.launch(Dispatchers.IO) {
|
||||
_relays.value.forEach { item ->
|
||||
Nip11CachedRetriever.loadRelayInfo(
|
||||
dirtyUrl = item.url,
|
||||
onInfo = {
|
||||
togglePaidRelay(item, it.limitation?.payment_required ?: false)
|
||||
},
|
||||
onError = { url, errorCode, exceptionMessage -> },
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun clear() {
|
||||
_relays.update {
|
||||
val relayList = account.getPrivateOutboxRelayList()?.relays() ?: emptyList()
|
||||
|
||||
relayList.map { relayUrl ->
|
||||
val liveRelay = RelayPool.getRelay(relayUrl)
|
||||
val errorCounter = liveRelay?.errorCounter ?: 0
|
||||
val eventDownloadCounter = liveRelay?.eventDownloadCounterInBytes ?: 0
|
||||
val eventUploadCounter = liveRelay?.eventUploadCounterInBytes ?: 0
|
||||
val spamCounter = liveRelay?.spamCounter ?: 0
|
||||
|
||||
BasicRelaySetupInfo(
|
||||
relayUrl,
|
||||
errorCounter,
|
||||
eventDownloadCounter,
|
||||
eventUploadCounter,
|
||||
spamCounter,
|
||||
)
|
||||
}.distinctBy { it.url }.sortedBy { it.downloadCountInBytes }.reversed()
|
||||
}
|
||||
}
|
||||
|
||||
fun addRelay(relay: BasicRelaySetupInfo) {
|
||||
if (relays.value.any { it.url == relay.url }) return
|
||||
|
||||
_relays.update { it.plus(relay) }
|
||||
}
|
||||
|
||||
fun deleteRelay(relay: BasicRelaySetupInfo) {
|
||||
_relays.update { it.minus(relay) }
|
||||
}
|
||||
|
||||
fun deleteAll() {
|
||||
_relays.update { relays -> emptyList() }
|
||||
}
|
||||
|
||||
fun togglePaidRelay(
|
||||
relay: BasicRelaySetupInfo,
|
||||
paid: Boolean,
|
||||
) {
|
||||
_relays.update { it.updated(relay, relay.copy(paidRelay = paid)) }
|
||||
}
|
||||
}
|
@ -825,6 +825,8 @@
|
||||
<string name="public_notif_section_explainer">This relay type receives all replies, comments, likes and zaps to your posts. Insert between 1–3 relays and make sure they accept posts from anyone.</string>
|
||||
<string name="private_inbox_section">DM Inbox Relays</string>
|
||||
<string name="private_inbox_section_explainer">Insert between 1–3 relays to serve as your private inbox. Others will use these relays to send DMs to you. DM Inbox relays should accept any message from anyone, but only allow you to download them. Good options are:\n - inbox.nostr.wine (paid)\n - you.nostr1.com (personal relays - paid)</string>
|
||||
<string name="private_outbox_section">Private Relays</string>
|
||||
<string name="private_outbox_section_explainer">Insert between 1–3 relays to store events no one else can see, like your Drafts and/or app settings. Ideally, these relays are either local or require authentication before downloading each user\'s content.</string>
|
||||
<string name="kind_3_section">General Relays</string>
|
||||
<string name="kind_3_section_description">Amethyst uses these relays to download posts for you.</string>
|
||||
<string name="search_section">Search Relays</string>
|
||||
|
@ -124,6 +124,7 @@ class EventFactory {
|
||||
PinListEvent.KIND -> PinListEvent(id, pubKey, createdAt, tags, content, sig)
|
||||
PollNoteEvent.KIND -> PollNoteEvent(id, pubKey, createdAt, tags, content, sig)
|
||||
PrivateDmEvent.KIND -> PrivateDmEvent(id, pubKey, createdAt, tags, content, sig)
|
||||
PrivateOutboxRelayListEvent.KIND -> PrivateOutboxRelayListEvent(id, pubKey, createdAt, tags, content, sig)
|
||||
ReactionEvent.KIND -> ReactionEvent(id, pubKey, createdAt, tags, content, sig)
|
||||
RecommendRelayEvent.KIND -> RecommendRelayEvent(id, pubKey, createdAt, tags, content, sig)
|
||||
RelayAuthEvent.KIND -> RelayAuthEvent(id, pubKey, createdAt, tags, content, sig)
|
||||
|
@ -0,0 +1,169 @@
|
||||
/**
|
||||
* Copyright (c) 2024 Vitor Pamplona
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
* this software and associated documentation files (the "Software"), to deal in
|
||||
* the Software without restriction, including without limitation the rights to use,
|
||||
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
|
||||
* Software, and to permit persons to whom the Software is furnished to do so,
|
||||
* subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
|
||||
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
package com.vitorpamplona.quartz.events
|
||||
|
||||
import android.util.Log
|
||||
import androidx.compose.runtime.Immutable
|
||||
import com.fasterxml.jackson.module.kotlin.readValue
|
||||
import com.vitorpamplona.quartz.encoders.ATag
|
||||
import com.vitorpamplona.quartz.encoders.HexKey
|
||||
import com.vitorpamplona.quartz.signers.NostrSigner
|
||||
import com.vitorpamplona.quartz.utils.TimeUtils
|
||||
|
||||
@Immutable
|
||||
class PrivateOutboxRelayListEvent(
|
||||
id: HexKey,
|
||||
pubKey: HexKey,
|
||||
createdAt: Long,
|
||||
tags: Array<Array<String>>,
|
||||
content: String,
|
||||
sig: HexKey,
|
||||
) : BaseAddressableEvent(id, pubKey, createdAt, KIND, tags, content, sig) {
|
||||
@Transient private var privateTagsCache: Array<Array<String>>? = null
|
||||
|
||||
override fun dTag() = FIXED_D_TAG
|
||||
|
||||
fun relays(): List<String>? {
|
||||
return tags.mapNotNull {
|
||||
if (it.size > 1 && it[0] == "relay") {
|
||||
it[1]
|
||||
} else {
|
||||
null
|
||||
}
|
||||
}.plus(
|
||||
privateTagsCache?.mapNotNull {
|
||||
if (it.size > 1 && it[0] == "relay") {
|
||||
it[1]
|
||||
} else {
|
||||
null
|
||||
}
|
||||
} ?: emptyList(),
|
||||
).ifEmpty { null }
|
||||
}
|
||||
|
||||
fun cachedPrivateTags(): Array<Array<String>>? {
|
||||
return privateTagsCache
|
||||
}
|
||||
|
||||
fun privateTags(
|
||||
signer: NostrSigner,
|
||||
onReady: (Array<Array<String>>) -> Unit,
|
||||
) {
|
||||
if (content.isEmpty()) {
|
||||
onReady(emptyArray())
|
||||
return
|
||||
}
|
||||
|
||||
privateTagsCache?.let {
|
||||
onReady(it)
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
signer.nip44Decrypt(content, pubKey) {
|
||||
privateTagsCache = mapper.readValue<Array<Array<String>>>(it)
|
||||
privateTagsCache?.let { onReady(it) }
|
||||
}
|
||||
} catch (e: Throwable) {
|
||||
Log.w("GeneralList", "Error parsing the JSON ${e.message}")
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
const val KIND = 10013
|
||||
const val FIXED_D_TAG = ""
|
||||
val TAGS = arrayOf(arrayOf("alt", "Relay list to store private content from this author"))
|
||||
|
||||
fun createAddressATag(pubKey: HexKey): ATag {
|
||||
return ATag(KIND, pubKey, FIXED_D_TAG, null)
|
||||
}
|
||||
|
||||
fun createAddressTag(pubKey: HexKey): String {
|
||||
return ATag.assembleATag(KIND, pubKey, FIXED_D_TAG)
|
||||
}
|
||||
|
||||
fun encryptTags(
|
||||
privateTags: Array<Array<String>>? = null,
|
||||
signer: NostrSigner,
|
||||
onReady: (String) -> Unit,
|
||||
) {
|
||||
val msg = mapper.writeValueAsString(privateTags)
|
||||
|
||||
signer.nip44Encrypt(
|
||||
msg,
|
||||
signer.pubKey,
|
||||
onReady,
|
||||
)
|
||||
}
|
||||
|
||||
fun createTagArray(relays: List<String>): Array<Array<String>> {
|
||||
return relays.map {
|
||||
arrayOf("relay", it)
|
||||
}.toTypedArray()
|
||||
}
|
||||
|
||||
fun updateRelayList(
|
||||
earlierVersion: PrivateOutboxRelayListEvent,
|
||||
relays: List<String>,
|
||||
signer: NostrSigner,
|
||||
createdAt: Long = TimeUtils.now(),
|
||||
onReady: (PrivateOutboxRelayListEvent) -> Unit,
|
||||
) {
|
||||
val tags =
|
||||
earlierVersion.privateTagsCache?.filter { it[0] != "relay" }?.plus(
|
||||
relays.map {
|
||||
arrayOf("relay", it)
|
||||
},
|
||||
)?.toTypedArray() ?: emptyArray()
|
||||
|
||||
encryptTags(tags, signer) {
|
||||
signer.sign<PrivateOutboxRelayListEvent>(createdAt, KIND, TAGS, it) {
|
||||
it.privateTagsCache = tags
|
||||
onReady(it)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun createFromScratch(
|
||||
relays: List<String>,
|
||||
signer: NostrSigner,
|
||||
createdAt: Long = TimeUtils.now(),
|
||||
onReady: (PrivateOutboxRelayListEvent) -> Unit,
|
||||
) {
|
||||
create(relays, signer, createdAt, onReady)
|
||||
}
|
||||
|
||||
fun create(
|
||||
relays: List<String>,
|
||||
signer: NostrSigner,
|
||||
createdAt: Long = TimeUtils.now(),
|
||||
onReady: (PrivateOutboxRelayListEvent) -> Unit,
|
||||
) {
|
||||
val privateTagArray = createTagArray(relays)
|
||||
encryptTags(privateTagArray, signer) { privateTags ->
|
||||
signer.sign<PrivateOutboxRelayListEvent>(createdAt, KIND, TAGS, privateTags) {
|
||||
it.privateTagsCache = privateTagArray
|
||||
onReady(it)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user