Adds support for RelaySet event types.

This commit is contained in:
Vitor Pamplona
2023-06-07 17:16:48 -04:00
parent ac0103a53e
commit 89bdc9dd58
15 changed files with 251 additions and 26 deletions
@@ -300,6 +300,19 @@ object LocalCache {
} }
} }
private fun consume(event: RelaySetEvent) {
val note = getOrCreateAddressableNote(event.address())
val author = getOrCreateUser(event.pubKey)
if (note.event?.id() == event.id()) return
if (event.createdAt > (note.createdAt() ?: 0)) {
note.loadEvent(event, author, emptyList())
refreshObservers(note)
}
}
private fun consume(event: AudioTrackEvent) { private fun consume(event: AudioTrackEvent) {
val note = getOrCreateAddressableNote(event.address()) val note = getOrCreateAddressableNote(event.address())
val author = getOrCreateUser(event.pubKey) val author = getOrCreateUser(event.pubKey)
@@ -1015,7 +1028,7 @@ object LocalCache {
live.invalidateData(newNote) live.invalidateData(newNote)
} }
fun consume(event: Event, relay: Relay?) { fun verifyAndConsume(event: Event, relay: Relay?) {
checkNotInMainThread() checkNotInMainThread()
if (!event.hasValidSignature()) { if (!event.hasValidSignature()) {
@@ -1050,7 +1063,7 @@ object LocalCache {
is HighlightEvent -> consume(event, relay) is HighlightEvent -> consume(event, relay)
is LnZapEvent -> { is LnZapEvent -> {
event.zapRequest?.let { event.zapRequest?.let {
consume(it, relay) verifyAndConsume(it, relay)
} }
consume(event) consume(event)
} }
@@ -1064,10 +1077,11 @@ object LocalCache {
is PeopleListEvent -> consume(event) is PeopleListEvent -> consume(event)
is ReactionEvent -> consume(event) is ReactionEvent -> consume(event)
is RecommendRelayEvent -> consume(event) is RecommendRelayEvent -> consume(event)
is RelaySetEvent -> consume(event)
is ReportEvent -> consume(event, relay) is ReportEvent -> consume(event, relay)
is RepostEvent -> { is RepostEvent -> {
event.containedPost()?.let { event.containedPost()?.let {
consume(it, relay) verifyAndConsume(it, relay)
} }
consume(event) consume(event)
} }
@@ -39,7 +39,7 @@ abstract class NostrDataSource(val debugName: String) {
eventCounter = eventCounter + Pair(key, Counter(1)) eventCounter = eventCounter + Pair(key, Counter(1))
} }
LocalCache.consume(event, relay) LocalCache.verifyAndConsume(event, relay)
} }
} }
@@ -258,6 +258,7 @@ open class Event(
PrivateDmEvent.kind -> PrivateDmEvent(id, pubKey, createdAt, tags, content, sig) PrivateDmEvent.kind -> PrivateDmEvent(id, pubKey, createdAt, tags, content, sig)
ReactionEvent.kind -> ReactionEvent(id, pubKey, createdAt, tags, content, sig) ReactionEvent.kind -> ReactionEvent(id, pubKey, createdAt, tags, content, sig)
RecommendRelayEvent.kind -> RecommendRelayEvent(id, pubKey, createdAt, tags, content, sig, lenient) RecommendRelayEvent.kind -> RecommendRelayEvent(id, pubKey, createdAt, tags, content, sig, lenient)
RelaySetEvent.kind -> RelaySetEvent(id, pubKey, createdAt, tags, content, sig)
ReportEvent.kind -> ReportEvent(id, pubKey, createdAt, tags, content, sig) ReportEvent.kind -> ReportEvent(id, pubKey, createdAt, tags, content, sig)
RepostEvent.kind -> RepostEvent(id, pubKey, createdAt, tags, content, sig) RepostEvent.kind -> RepostEvent(id, pubKey, createdAt, tags, content, sig)
TextNoteEvent.kind -> TextNoteEvent(id, pubKey, createdAt, tags, content, sig) TextNoteEvent.kind -> TextNoteEvent(id, pubKey, createdAt, tags, content, sig)
@@ -0,0 +1,44 @@
package com.vitorpamplona.amethyst.service.model
import androidx.compose.runtime.Immutable
import com.vitorpamplona.amethyst.model.HexKey
import com.vitorpamplona.amethyst.model.toHexKey
import nostr.postr.Utils
import java.util.Date
@Immutable
class RelaySetEvent(
id: HexKey,
pubKey: HexKey,
createdAt: Long,
tags: List<List<String>>,
content: String,
sig: HexKey
) : Event(id, pubKey, createdAt, kind, tags, content, sig), AddressableEvent {
override fun dTag() = tags.firstOrNull { it.size > 1 && it[0] == "d" }?.get(1) ?: ""
override fun address() = ATag(kind, pubKey, dTag(), null)
fun relays() = tags.filter { it.size > 1 && it[0] == "r" }.map { it[1] }
fun description() = tags.firstOrNull() { it.size > 1 && it[0] == "description" }?.get(1)
companion object {
const val kind = 30022
fun create(
relays: List<String>,
privateKey: ByteArray,
createdAt: Long = Date().time / 1000
): RelaySetEvent {
val tags = mutableListOf<List<String>>()
relays.forEach {
tags.add(listOf("r", it))
}
val pubKey = Utils.pubkeyCreate(privateKey).toHexKey()
val id = generateId(pubKey, createdAt, kind, tags, "")
val sig = Utils.sign(id, privateKey)
return RelaySetEvent(id.toHexKey(), pubKey, createdAt, tags, "", sig.toHexKey())
}
}
}
@@ -23,7 +23,7 @@ class EventNotificationConsumer(private val applicationContext: Context) {
val scope = CoroutineScope(Job() + Dispatchers.IO) val scope = CoroutineScope(Job() + Dispatchers.IO)
scope.launch { scope.launch {
// adds to database // adds to database
LocalCache.consume(event, null) LocalCache.verifyAndConsume(event, null)
when (event) { when (event) {
is PrivateDmEvent -> notify(event) is PrivateDmEvent -> notify(event)
@@ -97,7 +97,7 @@ fun ExpandableRichTextViewer(
} }
@Composable @Composable
private fun ShowMoreButton(onClick: () -> Unit) { public fun ShowMoreButton(onClick: () -> Unit) {
Button( Button(
modifier = Modifier.padding(top = 10.dp), modifier = Modifier.padding(top = 10.dp),
onClick = onClick, onClick = onClick,
@@ -372,10 +372,9 @@ private fun aspectRatio(dim: String?): Float? {
@Composable @Composable
private fun DisplayUrlWithLoadingSymbol(content: ZoomableContent) { private fun DisplayUrlWithLoadingSymbol(content: ZoomableContent) {
var cnt by remember { mutableStateOf<ZoomableContent?>(null) } var cnt by remember { mutableStateOf<ZoomableContent?>(null) }
val scope = rememberCoroutineScope()
LaunchedEffect(Unit) { LaunchedEffect(Unit) {
scope.launch(Dispatchers.IO) { launch(Dispatchers.IO) {
delay(200) delay(200)
cnt = content cnt = content
} }
@@ -70,7 +70,6 @@ import com.vitorpamplona.amethyst.ui.theme.RelayIconFilter
import kotlinx.collections.immutable.toImmutableSet import kotlinx.collections.immutable.toImmutableSet
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
val ChatBubbleShapeMe = RoundedCornerShape(15.dp, 15.dp, 3.dp, 15.dp) val ChatBubbleShapeMe = RoundedCornerShape(15.dp, 15.dp, 3.dp, 15.dp)
val ChatBubbleShapeThem = RoundedCornerShape(3.dp, 15.dp, 15.dp, 15.dp) val ChatBubbleShapeThem = RoundedCornerShape(3.dp, 15.dp, 15.dp, 15.dp)
@@ -348,7 +347,7 @@ fun ChatTimeAgo(time: Long) {
var timeStr by remember { mutableStateOf("") } var timeStr by remember { mutableStateOf("") }
LaunchedEffect(key1 = time) { LaunchedEffect(key1 = time) {
withContext(Dispatchers.IO) { launch(Dispatchers.IO) {
timeStr = timeAgoShort(time, context = context) timeStr = timeAgoShort(time, context = context)
} }
} }
@@ -57,6 +57,7 @@ import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.setValue import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Alignment.Companion.CenterHorizontally
import androidx.compose.ui.Alignment.Companion.CenterVertically import androidx.compose.ui.Alignment.Companion.CenterVertically
import androidx.compose.ui.Alignment.Companion.TopEnd import androidx.compose.ui.Alignment.Companion.TopEnd
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
@@ -109,10 +110,12 @@ import com.vitorpamplona.amethyst.service.model.PinListEvent
import com.vitorpamplona.amethyst.service.model.PollNoteEvent import com.vitorpamplona.amethyst.service.model.PollNoteEvent
import com.vitorpamplona.amethyst.service.model.PrivateDmEvent import com.vitorpamplona.amethyst.service.model.PrivateDmEvent
import com.vitorpamplona.amethyst.service.model.ReactionEvent import com.vitorpamplona.amethyst.service.model.ReactionEvent
import com.vitorpamplona.amethyst.service.model.RelaySetEvent
import com.vitorpamplona.amethyst.service.model.ReportEvent import com.vitorpamplona.amethyst.service.model.ReportEvent
import com.vitorpamplona.amethyst.service.model.RepostEvent import com.vitorpamplona.amethyst.service.model.RepostEvent
import com.vitorpamplona.amethyst.service.model.TextNoteEvent import com.vitorpamplona.amethyst.service.model.TextNoteEvent
import com.vitorpamplona.amethyst.ui.actions.ImmutableListOfLists import com.vitorpamplona.amethyst.ui.actions.ImmutableListOfLists
import com.vitorpamplona.amethyst.ui.actions.NewRelayListView
import com.vitorpamplona.amethyst.ui.actions.toImmutableListOfLists import com.vitorpamplona.amethyst.ui.actions.toImmutableListOfLists
import com.vitorpamplona.amethyst.ui.components.ClickableUrl import com.vitorpamplona.amethyst.ui.components.ClickableUrl
import com.vitorpamplona.amethyst.ui.components.CreateClickableTextWithEmoji import com.vitorpamplona.amethyst.ui.components.CreateClickableTextWithEmoji
@@ -123,6 +126,7 @@ import com.vitorpamplona.amethyst.ui.components.ResizeImage
import com.vitorpamplona.amethyst.ui.components.RobohashAsyncImage import com.vitorpamplona.amethyst.ui.components.RobohashAsyncImage
import com.vitorpamplona.amethyst.ui.components.RobohashAsyncImageProxy import com.vitorpamplona.amethyst.ui.components.RobohashAsyncImageProxy
import com.vitorpamplona.amethyst.ui.components.SensitivityWarning import com.vitorpamplona.amethyst.ui.components.SensitivityWarning
import com.vitorpamplona.amethyst.ui.components.ShowMoreButton
import com.vitorpamplona.amethyst.ui.components.TranslatableRichTextViewer import com.vitorpamplona.amethyst.ui.components.TranslatableRichTextViewer
import com.vitorpamplona.amethyst.ui.components.VideoView import com.vitorpamplona.amethyst.ui.components.VideoView
import com.vitorpamplona.amethyst.ui.components.ZoomableContent import com.vitorpamplona.amethyst.ui.components.ZoomableContent
@@ -510,6 +514,10 @@ fun NormalNote(
RenderPeopleList(baseNote, backgroundColor, accountViewModel, nav) RenderPeopleList(baseNote, backgroundColor, accountViewModel, nav)
} }
is RelaySetEvent -> {
RelaySetList(baseNote, backgroundColor, accountViewModel, nav)
}
is AudioTrackEvent -> { is AudioTrackEvent -> {
RenderAudioTrack(baseNote, accountViewModel, nav) RenderAudioTrack(baseNote, accountViewModel, nav)
} }
@@ -958,6 +966,23 @@ private fun RenderPrivateMessage(
) )
} }
@Composable
fun RelaySetList(
baseNote: Note,
backgroundColor: Color,
accountViewModel: AccountViewModel,
nav: (String) -> Unit
) {
DisplayRelaySet(baseNote, backgroundColor, accountViewModel, nav)
ReactionsRow(baseNote, accountViewModel, nav)
Divider(
modifier = Modifier.padding(top = 10.dp),
thickness = 0.25.dp
)
}
@Composable @Composable
fun RenderPeopleList( fun RenderPeopleList(
baseNote: Note, baseNote: Note,
@@ -975,6 +1000,139 @@ fun RenderPeopleList(
) )
} }
@Composable
fun DisplayRelaySet(
baseNote: Note,
backgroundColor: Color,
accountViewModel: AccountViewModel,
nav: (String) -> Unit
) {
val noteEvent = baseNote.event as? RelaySetEvent ?: return
val relays by remember {
mutableStateOf<ImmutableList<String>>(
noteEvent.relays().toImmutableList()
)
}
var expanded by remember {
mutableStateOf(false)
}
val toMembersShow = if (expanded) {
relays
} else {
relays.take(3)
}
val relayListName by remember {
derivedStateOf {
"#${noteEvent.dTag()}"
}
}
val relayDescription by remember {
derivedStateOf {
noteEvent.description()
}
}
Text(
text = relayListName,
fontWeight = FontWeight.Bold,
maxLines = 1,
overflow = TextOverflow.Ellipsis,
modifier = Modifier
.fillMaxWidth()
.padding(5.dp),
textAlign = TextAlign.Center
)
relayDescription?.let {
Text(
text = it,
maxLines = 3,
overflow = TextOverflow.Ellipsis,
modifier = Modifier
.fillMaxWidth()
.padding(5.dp),
textAlign = TextAlign.Center,
color = Color.Gray
)
}
Box {
Column(modifier = Modifier.padding(top = 5.dp)) {
toMembersShow.forEach { relay ->
Row(modifier = Modifier.fillMaxWidth(), verticalAlignment = CenterVertically) {
Text(
relay.trim().removePrefix("wss://").removePrefix("ws://").removeSuffix("/"),
fontWeight = FontWeight.Bold,
maxLines = 1,
overflow = TextOverflow.Ellipsis,
modifier = Modifier
.padding(start = 10.dp, bottom = 5.dp)
.weight(1f)
)
Column(modifier = Modifier.padding(start = 10.dp)) {
RelayOptionsAction(relay, accountViewModel)
}
}
}
}
if (relays.size > 3 && !expanded) {
Row(
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.Center,
modifier = Modifier
.align(Alignment.BottomCenter)
.fillMaxWidth()
.background(
brush = Brush.verticalGradient(
colors = listOf(
backgroundColor.copy(alpha = 0f),
backgroundColor
)
)
)
) {
ShowMoreButton {
expanded = !expanded
}
}
}
}
}
@Composable
private fun RelayOptionsAction(
relay: String,
accountViewModel: AccountViewModel
) {
val userStateRelayInfo by accountViewModel.account.userProfile().live().relayInfo.observeAsState()
val isCurrentlyOnTheUsersList by remember(userStateRelayInfo) {
derivedStateOf {
userStateRelayInfo?.user?.latestContactList?.relays()?.none { it.key == relay } == true
}
}
var wantsToAddRelay by remember {
mutableStateOf("")
}
if (wantsToAddRelay.isNotEmpty()) {
NewRelayListView({ wantsToAddRelay = "" }, accountViewModel, wantsToAddRelay)
}
if (isCurrentlyOnTheUsersList) {
AddRelayButton { wantsToAddRelay = relay }
} else {
RemoveRelayButton { wantsToAddRelay = relay }
}
}
@OptIn(ExperimentalLayoutApi::class) @OptIn(ExperimentalLayoutApi::class)
@Composable @Composable
fun DisplayPeopleList( fun DisplayPeopleList(
@@ -60,6 +60,7 @@ import com.vitorpamplona.amethyst.service.model.LongTextNoteEvent
import com.vitorpamplona.amethyst.service.model.PeopleListEvent import com.vitorpamplona.amethyst.service.model.PeopleListEvent
import com.vitorpamplona.amethyst.service.model.PinListEvent import com.vitorpamplona.amethyst.service.model.PinListEvent
import com.vitorpamplona.amethyst.service.model.PollNoteEvent import com.vitorpamplona.amethyst.service.model.PollNoteEvent
import com.vitorpamplona.amethyst.service.model.RelaySetEvent
import com.vitorpamplona.amethyst.ui.actions.ImmutableListOfLists import com.vitorpamplona.amethyst.ui.actions.ImmutableListOfLists
import com.vitorpamplona.amethyst.ui.actions.toImmutableListOfLists import com.vitorpamplona.amethyst.ui.actions.toImmutableListOfLists
import com.vitorpamplona.amethyst.ui.components.ObserveDisplayNip05Status import com.vitorpamplona.amethyst.ui.components.ObserveDisplayNip05Status
@@ -374,6 +375,13 @@ fun NoteMaster(
accountViewModel, accountViewModel,
nav nav
) )
} else if (noteEvent is RelaySetEvent) {
DisplayRelaySet(
baseNote,
MaterialTheme.colors.background,
accountViewModel,
nav
)
} else if (noteEvent is AppDefinitionEvent) { } else if (noteEvent is AppDefinitionEvent) {
RenderAppDefinition(baseNote, accountViewModel, nav) RenderAppDefinition(baseNote, accountViewModel, nav)
} else if (noteEvent is HighlightEvent) { } else if (noteEvent is HighlightEvent) {
@@ -45,6 +45,7 @@ import com.vitorpamplona.amethyst.ui.screen.ChatroomListFeedView
import com.vitorpamplona.amethyst.ui.screen.FeedViewModel import com.vitorpamplona.amethyst.ui.screen.FeedViewModel
import com.vitorpamplona.amethyst.ui.screen.NostrChatroomListKnownFeedViewModel import com.vitorpamplona.amethyst.ui.screen.NostrChatroomListKnownFeedViewModel
import com.vitorpamplona.amethyst.ui.screen.NostrChatroomListNewFeedViewModel import com.vitorpamplona.amethyst.ui.screen.NostrChatroomListNewFeedViewModel
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
@OptIn(ExperimentalFoundationApi::class) @OptIn(ExperimentalFoundationApi::class)
@@ -150,9 +151,11 @@ fun ChatroomListScreen(
@Composable @Composable
fun WatchAccountForListScreen(knownFeedViewModel: NostrChatroomListKnownFeedViewModel, newFeedViewModel: NostrChatroomListNewFeedViewModel, accountViewModel: AccountViewModel) { fun WatchAccountForListScreen(knownFeedViewModel: NostrChatroomListKnownFeedViewModel, newFeedViewModel: NostrChatroomListNewFeedViewModel, accountViewModel: AccountViewModel) {
LaunchedEffect(accountViewModel) { LaunchedEffect(accountViewModel) {
NostrChatroomListDataSource.start() launch(Dispatchers.IO) {
knownFeedViewModel.invalidateData(true) NostrChatroomListDataSource.start()
newFeedViewModel.invalidateData(true) knownFeedViewModel.invalidateData(true)
newFeedViewModel.invalidateData(true)
}
} }
} }
@@ -51,7 +51,7 @@ fun ChatroomScreen(
var userRoom by remember { mutableStateOf<User?>(null) } var userRoom by remember { mutableStateOf<User?>(null) }
LaunchedEffect(userId) { LaunchedEffect(userId) {
withContext(Dispatchers.IO) { launch(Dispatchers.IO) {
val newUser = LocalCache.checkGetOrCreateUser(userId) val newUser = LocalCache.checkGetOrCreateUser(userId)
if (newUser != userRoom) { if (newUser != userRoom) {
userRoom = newUser userRoom = newUser
@@ -105,10 +105,10 @@ fun ChatroomScreen(
val lifeCycleOwner = LocalLifecycleOwner.current val lifeCycleOwner = LocalLifecycleOwner.current
LaunchedEffect(baseUser, accountViewModel) { LaunchedEffect(baseUser, accountViewModel) {
NostrChatroomDataSource.start()
feedViewModel.invalidateData()
launch(Dispatchers.IO) { launch(Dispatchers.IO) {
NostrChatroomDataSource.start()
feedViewModel.invalidateData()
newPostModel.imageUploadingError.collect { error -> newPostModel.imageUploadingError.collect { error ->
withContext(Dispatchers.Main) { withContext(Dispatchers.Main) {
Toast.makeText(context, error, Toast.LENGTH_SHORT).show() Toast.makeText(context, error, Toast.LENGTH_SHORT).show()
@@ -138,17 +138,15 @@ fun WatchAccountForHomeScreen(
accountViewModel: AccountViewModel accountViewModel: AccountViewModel
) { ) {
val accountState by accountViewModel.accountLiveData.observeAsState() val accountState by accountViewModel.accountLiveData.observeAsState()
val account = remember(accountState) { accountState?.account } ?: return
val scope = rememberCoroutineScope()
var firstTime by remember(accountViewModel) { mutableStateOf(true) } var firstTime by remember(accountViewModel) { mutableStateOf(true) }
LaunchedEffect(accountViewModel, account.defaultHomeFollowList) { LaunchedEffect(accountViewModel, accountState?.account?.defaultHomeFollowList) {
// Only invalidate when things change. Not in the first run // Only invalidate when things change. Not in the first run
if (firstTime) { if (firstTime) {
firstTime = false firstTime = false
} else { } else {
scope.launch(Dispatchers.IO) { launch(Dispatchers.IO) {
NostrHomeDataSource.invalidateFilters() NostrHomeDataSource.invalidateFilters()
homeFeedViewModel.invalidateDataAndSendToTop(true) homeFeedViewModel.invalidateDataAndSendToTop(true)
repliesFeedViewModel.invalidateDataAndSendToTop(true) repliesFeedViewModel.invalidateDataAndSendToTop(true)
@@ -26,7 +26,6 @@ import com.vitorpamplona.amethyst.service.model.ChannelCreateEvent
import com.vitorpamplona.amethyst.service.model.PrivateDmEvent import com.vitorpamplona.amethyst.service.model.PrivateDmEvent
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
@Composable @Composable
fun LoadRedirectScreen(eventId: String?, navController: NavController) { fun LoadRedirectScreen(eventId: String?, navController: NavController) {
@@ -42,7 +41,7 @@ fun LoadRedirectScreen(eventId: String?, navController: NavController) {
} }
LaunchedEffect(eventId) { LaunchedEffect(eventId) {
withContext(Dispatchers.IO) { launch(Dispatchers.IO) {
val newNoteBase = LocalCache.checkGetOrCreateNote(eventId) val newNoteBase = LocalCache.checkGetOrCreateNote(eventId)
if (newNoteBase != noteBase) { if (newNoteBase != noteBase) {
noteBase = newNoteBase noteBase = newNoteBase
@@ -145,9 +145,11 @@ fun SearchScreen(
@Composable @Composable
fun WatchAccountForSearchScreen(searchFeedViewModel: NostrGlobalFeedViewModel, accountViewModel: AccountViewModel) { fun WatchAccountForSearchScreen(searchFeedViewModel: NostrGlobalFeedViewModel, accountViewModel: AccountViewModel) {
LaunchedEffect(accountViewModel) { LaunchedEffect(accountViewModel) {
NostrGlobalDataSource.resetFilters() launch(Dispatchers.IO) {
NostrSearchEventOrUserDataSource.start() NostrGlobalDataSource.resetFilters()
searchFeedViewModel.invalidateData(true) NostrSearchEventOrUserDataSource.start()
searchFeedViewModel.invalidateData(true)
}
} }
} }