diff --git a/app/src/main/java/com/vitorpamplona/amethyst/model/Account.kt b/app/src/main/java/com/vitorpamplona/amethyst/model/Account.kt index 702cdd4eb..e94401fb6 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/model/Account.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/model/Account.kt @@ -227,7 +227,7 @@ class Account( privateKey = loggedIn.privKey!! ) Client.send(signedEvent) - LocalCache.consume(signedEvent) + LocalCache.consume(signedEvent, null) } fun sendPrivateMeesage(message: String, toUser: String, replyingTo: Note? = null) { diff --git a/app/src/main/java/com/vitorpamplona/amethyst/model/LocalCache.kt b/app/src/main/java/com/vitorpamplona/amethyst/model/LocalCache.kt index 910447ecb..a56d43723 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/model/LocalCache.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/model/LocalCache.kt @@ -348,7 +348,7 @@ object LocalCache { } } - fun consume(event: ChannelMessageEvent) { + fun consume(event: ChannelMessageEvent, relay: Relay?) { if (event.channel.isNullOrBlank()) return val channel = getOrCreateChannel(event.channel) @@ -356,6 +356,11 @@ object LocalCache { val note = getOrCreateNote(event.id.toHex()) channel.addNote(note) + if (relay != null) { + note.author?.addRelay(relay, event.createdAt) + note.addRelay(relay) + } + // Already processed this event. if (note.event != null) return diff --git a/app/src/main/java/com/vitorpamplona/amethyst/service/NostrDataSource.kt b/app/src/main/java/com/vitorpamplona/amethyst/service/NostrDataSource.kt index 54706c5ee..50dde8f72 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/service/NostrDataSource.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/service/NostrDataSource.kt @@ -70,7 +70,7 @@ abstract class NostrDataSource(val debugName: String) { ChannelCreateEvent.kind -> LocalCache.consume(ChannelCreateEvent(event.id, event.pubKey, event.createdAt, event.tags, event.content, event.sig)) ChannelMetadataEvent.kind -> LocalCache.consume(ChannelMetadataEvent(event.id, event.pubKey, event.createdAt, event.tags, event.content, event.sig)) - ChannelMessageEvent.kind -> LocalCache.consume(ChannelMessageEvent(event.id, event.pubKey, event.createdAt, event.tags, event.content, event.sig)) + ChannelMessageEvent.kind -> LocalCache.consume(ChannelMessageEvent(event.id, event.pubKey, event.createdAt, event.tags, event.content, event.sig), relay) ChannelHideMessageEvent.kind -> LocalCache.consume(ChannelHideMessageEvent(event.id, event.pubKey, event.createdAt, event.tags, event.content, event.sig)) ChannelMuteUserEvent.kind -> LocalCache.consume(ChannelMuteUserEvent(event.id, event.pubKey, event.createdAt, event.tags, event.content, event.sig)) } diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/note/ChatroomMessageCompose.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/ChatroomMessageCompose.kt index 827e45860..949714cf5 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/note/ChatroomMessageCompose.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/ChatroomMessageCompose.kt @@ -1,25 +1,34 @@ package com.vitorpamplona.amethyst.ui.note import androidx.compose.foundation.ExperimentalFoundationApi +import androidx.compose.foundation.background import androidx.compose.foundation.border import androidx.compose.foundation.clickable import androidx.compose.foundation.combinedClickable import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.width import androidx.compose.foundation.layout.widthIn import androidx.compose.foundation.layout.wrapContentWidth import androidx.compose.foundation.shape.CircleShape import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material.Icon +import androidx.compose.material.IconButton import androidx.compose.material.LocalTextStyle import androidx.compose.material.MaterialTheme import androidx.compose.material.Surface import androidx.compose.material.Text +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.ChevronRight +import androidx.compose.material.icons.filled.ExpandMore import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.compose.runtime.livedata.observeAsState @@ -30,8 +39,11 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.ColorFilter +import androidx.compose.ui.graphics.ColorMatrix import androidx.compose.ui.graphics.Shape import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.platform.LocalUriHandler import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.style.TextDirection import androidx.compose.ui.unit.dp @@ -39,6 +51,7 @@ import androidx.compose.ui.unit.sp import androidx.navigation.NavController import coil.compose.AsyncImage import coil.compose.rememberAsyncImagePainter +import com.google.accompanist.flowlayout.FlowRow import com.vitorpamplona.amethyst.NotificationCache import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.service.model.ChannelCreateEvent @@ -235,6 +248,8 @@ fun ChatroomMessageCompose(baseNote: Note, routeForLastRead: String?, innerQuote color = MaterialTheme.colors.onSurface.copy(alpha = 0.32f), fontSize = 12.sp ) + + RelayBadges(note) } } } @@ -247,3 +262,51 @@ fun ChatroomMessageCompose(baseNote: Note, routeForLastRead: String?, innerQuote } } + + +@Composable +private fun RelayBadges(baseNote: Note) { + val noteRelaysState by baseNote.liveRelays.observeAsState() + val noteRelays = noteRelaysState?.note?.relays ?: emptySet() + + var expanded by remember { mutableStateOf(false) } + + val relaysToDisplay = if (expanded) noteRelays else noteRelays.take(3) + + val uri = LocalUriHandler.current + + FlowRow(Modifier.padding(start = 10.dp)) { + relaysToDisplay.forEach { + val url = it.removePrefix("wss://") + Box(Modifier.size(15.dp).padding(1.dp)) { + AsyncImage( + model = "https://${url}/favicon.ico", + placeholder = rememberAsyncImagePainter("https://robohash.org/$url.png"), + fallback = rememberAsyncImagePainter("https://robohash.org/$url.png"), + error = rememberAsyncImagePainter("https://robohash.org/$url.png"), + contentDescription = "Relay Icon", + colorFilter = ColorFilter.colorMatrix(ColorMatrix().apply { setToSaturation(0f) }), + modifier = Modifier + .fillMaxSize(1f) + .clip(shape = CircleShape) + .background(MaterialTheme.colors.background) + .clickable(onClick = { uri.openUri("https://" + url) } ) + ) + } + } + + if (noteRelays.size > 3 && !expanded) { + IconButton( + modifier = Modifier.then(Modifier.size(15.dp)), + onClick = { expanded = true } + ) { + Icon( + imageVector = Icons.Default.ChevronRight, + null, + modifier = Modifier.size(15.dp), + tint = MaterialTheme.colors.onSurface.copy(alpha = 0.32f), + ) + } + } + } +} diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/AccountStateViewModel.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/AccountStateViewModel.kt index 16adaf0dd..51369ecf2 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/AccountStateViewModel.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/AccountStateViewModel.kt @@ -2,34 +2,18 @@ package com.vitorpamplona.amethyst.ui.screen import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope -import androidx.security.crypto.EncryptedSharedPreferences import com.vitorpamplona.amethyst.LocalPreferences import com.vitorpamplona.amethyst.ServiceManager import com.vitorpamplona.amethyst.model.Account -import com.vitorpamplona.amethyst.model.DefaultChannels -import com.vitorpamplona.amethyst.model.toByteArray -import com.vitorpamplona.amethyst.service.NostrAccountDataSource -import com.vitorpamplona.amethyst.service.NostrChatroomListDataSource -import com.vitorpamplona.amethyst.service.NostrGlobalDataSource -import com.vitorpamplona.amethyst.service.NostrHomeDataSource -import com.vitorpamplona.amethyst.service.NostrNotificationDataSource -import com.vitorpamplona.amethyst.service.NostrSingleEventDataSource -import com.vitorpamplona.amethyst.service.NostrSingleUserDataSource -import com.vitorpamplona.amethyst.service.NostrThreadDataSource -import com.vitorpamplona.amethyst.service.relays.Client -import com.vitorpamplona.amethyst.ui.MainActivity import fr.acinq.secp256k1.Hex import java.util.regex.Pattern -import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.Job import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch import nostr.postr.Persona import nostr.postr.bechToBytes -import nostr.postr.toHex class AccountStateViewModel(private val localPreferences: LocalPreferences): ViewModel() { private val _accountContent = MutableStateFlow(AccountState.LoggedOff)