mirror of
https://github.com/vitorpamplona/amethyst.git
synced 2025-06-28 00:20:53 +02:00
User Relay information in Profiles
This commit is contained in:
parent
3c52ff6e8d
commit
d94f35de0e
@ -98,13 +98,14 @@ object LocalCache {
|
||||
fun consume(event: TextNoteEvent, relay: Relay? = null) {
|
||||
val note = getOrCreateNote(event.id.toHex())
|
||||
|
||||
val author = getOrCreateUser(event.pubKey)
|
||||
|
||||
if (relay != null)
|
||||
note.addRelay(relay)
|
||||
author.addRelay(relay, event.createdAt)
|
||||
|
||||
// Already processed this event.
|
||||
if (note.event != null) return
|
||||
|
||||
val author = getOrCreateUser(event.pubKey)
|
||||
val mentions = Collections.synchronizedList(event.mentions.map { getOrCreateUser(decodePublicKey(it)) })
|
||||
val replyTo = Collections.synchronizedList(event.replyTos.map { getOrCreateNote(it) }.toMutableList())
|
||||
|
||||
|
@ -1,7 +1,5 @@
|
||||
package com.vitorpamplona.amethyst.model
|
||||
|
||||
import android.os.Handler
|
||||
import android.os.Looper
|
||||
import androidx.lifecycle.LiveData
|
||||
import com.vitorpamplona.amethyst.service.NostrSingleEventDataSource
|
||||
import com.vitorpamplona.amethyst.ui.note.toShortenHex
|
||||
@ -10,7 +8,6 @@ import java.time.Instant
|
||||
import java.time.ZoneId
|
||||
import java.time.format.DateTimeFormatter
|
||||
import java.util.Collections
|
||||
import java.util.Date
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.Job
|
||||
@ -40,8 +37,6 @@ class Note(val idHex: String) {
|
||||
|
||||
var channel: Channel? = null
|
||||
|
||||
val relays = Collections.synchronizedSet(mutableSetOf<Relay>())
|
||||
|
||||
fun loadEvent(event: Event, author: User, mentions: List<User>, replyTo: MutableList<Note>) {
|
||||
this.event = event
|
||||
this.author = author
|
||||
@ -93,11 +88,6 @@ class Note(val idHex: String) {
|
||||
invalidateData(liveReactions)
|
||||
}
|
||||
|
||||
fun addRelay(relay: Relay) {
|
||||
if (relays.add(relay))
|
||||
invalidateData(liveRelays)
|
||||
}
|
||||
|
||||
fun addReport(note: Note) {
|
||||
if (reports.add(note))
|
||||
invalidateData(liveReports)
|
||||
@ -134,7 +124,6 @@ class Note(val idHex: String) {
|
||||
val liveBoosts: NoteLiveData = NoteLiveData(this)
|
||||
val liveReplies: NoteLiveData = NoteLiveData(this)
|
||||
val liveReports: NoteLiveData = NoteLiveData(this)
|
||||
val liveRelays: NoteLiveData = NoteLiveData(this)
|
||||
|
||||
// Refreshes observers in batches.
|
||||
var handlerWaiting = false
|
||||
|
@ -40,6 +40,7 @@ class User(val pubkey: ByteArray) {
|
||||
val messages = ConcurrentHashMap<User, MutableSet<Note>>()
|
||||
|
||||
val reports = Collections.synchronizedSet(mutableSetOf<Note>())
|
||||
val relaysBeingUsed = mutableMapOf<String, RelayInfo>()
|
||||
|
||||
fun toBestDisplayName(): String {
|
||||
return bestDisplayName() ?: bestUsername() ?: pubkeyDisplayHex
|
||||
@ -120,6 +121,26 @@ class User(val pubkey: ByteArray) {
|
||||
updateSubscribers { it.onNewMessage() }
|
||||
}
|
||||
|
||||
data class RelayInfo (
|
||||
val url: String,
|
||||
var lastEvent: Long,
|
||||
var counter: Long
|
||||
)
|
||||
|
||||
fun addRelay(relay: Relay, eventTime: Long) {
|
||||
val here = relaysBeingUsed.get(relay.url)
|
||||
if (here == null) {
|
||||
relaysBeingUsed.put(relay.url, RelayInfo(relay.url, eventTime, 1) )
|
||||
} else {
|
||||
if (eventTime > here.lastEvent) {
|
||||
here.lastEvent = eventTime
|
||||
}
|
||||
here.counter++
|
||||
}
|
||||
|
||||
updateSubscribers { it.onNewRelayInfo() }
|
||||
}
|
||||
|
||||
fun updateFollows(newFollows: Set<User>, updateAt: Long) {
|
||||
val toBeAdded = synchronized(follows) {
|
||||
newFollows - follows
|
||||
@ -191,6 +212,7 @@ class User(val pubkey: ByteArray) {
|
||||
open fun onFollowsChange() = Unit
|
||||
open fun onNewPosts() = Unit
|
||||
open fun onNewMessage() = Unit
|
||||
open fun onNewRelayInfo() = Unit
|
||||
open fun onNewReports() = Unit
|
||||
}
|
||||
|
||||
|
@ -60,7 +60,7 @@ import java.lang.Math.round
|
||||
|
||||
@OptIn(ExperimentalComposeUiApi::class)
|
||||
@Composable
|
||||
fun NewRelayListView(onClose: () -> Unit, account: Account) {
|
||||
fun NewRelayListView(onClose: () -> Unit, account: Account, relayToAdd: String = "") {
|
||||
val postViewModel: NewRelayListViewModel = viewModel()
|
||||
|
||||
val feedState by postViewModel.relays.collectAsState()
|
||||
@ -131,7 +131,7 @@ fun NewRelayListView(onClose: () -> Unit, account: Account) {
|
||||
|
||||
Spacer(modifier = Modifier.height(10.dp))
|
||||
|
||||
EditableServerConfig() {
|
||||
EditableServerConfig(relayToAdd) {
|
||||
postViewModel.addRelay(it)
|
||||
}
|
||||
}
|
||||
@ -295,8 +295,8 @@ fun ServerConfig(
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun EditableServerConfig(onNewRelay: (NewRelayListViewModel.Relay) -> Unit) {
|
||||
var url by remember { mutableStateOf<String>("") }
|
||||
fun EditableServerConfig(relayToAdd: String, onNewRelay: (NewRelayListViewModel.Relay) -> Unit) {
|
||||
var url by remember { mutableStateOf<String>(relayToAdd) }
|
||||
var read by remember { mutableStateOf(true) }
|
||||
var write by remember { mutableStateOf(true) }
|
||||
|
||||
|
@ -0,0 +1,130 @@
|
||||
package com.vitorpamplona.amethyst.ui.note
|
||||
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.PaddingValues
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material.Button
|
||||
import androidx.compose.material.ButtonDefaults
|
||||
import androidx.compose.material.Divider
|
||||
import androidx.compose.material.MaterialTheme
|
||||
import androidx.compose.material.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.livedata.observeAsState
|
||||
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.text.font.FontWeight
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.navigation.NavController
|
||||
import com.vitorpamplona.amethyst.model.User
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
||||
import java.time.Instant
|
||||
import java.time.ZoneId
|
||||
import java.time.format.DateTimeFormatter
|
||||
|
||||
@Composable
|
||||
fun RelayCompose(
|
||||
relay: User.RelayInfo,
|
||||
accountViewModel: AccountViewModel,
|
||||
navController: NavController,
|
||||
onAddRelay: () -> Unit,
|
||||
onRemoveRelay: () -> Unit
|
||||
) {
|
||||
val accountState by accountViewModel.accountLiveData.observeAsState()
|
||||
val account = accountState?.account ?: return
|
||||
|
||||
val ctx = LocalContext.current.applicationContext
|
||||
|
||||
Column() {
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.padding(start = 12.dp, end = 12.dp, top = 10.dp)
|
||||
) {
|
||||
|
||||
//UserPicture(user, navController, account.userProfile(), 55.dp)
|
||||
|
||||
Column(modifier = Modifier
|
||||
.padding(start = 10.dp)
|
||||
.weight(1f)) {
|
||||
Row(verticalAlignment = Alignment.CenterVertically, modifier = Modifier.fillMaxWidth()) {
|
||||
Text(
|
||||
relay.url.trim().removePrefix("wss://"),
|
||||
fontWeight = FontWeight.Bold,
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis
|
||||
)
|
||||
|
||||
Text(
|
||||
timeAgo(relay.lastEvent),
|
||||
maxLines = 1
|
||||
)
|
||||
}
|
||||
|
||||
Text(
|
||||
"${relay.counter} events received",
|
||||
color = MaterialTheme.colors.onSurface.copy(alpha = 0.32f),
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis
|
||||
)
|
||||
}
|
||||
|
||||
Column(modifier = Modifier.padding(start = 10.dp)) {
|
||||
if (account.activeRelays()?.filter { it.url == relay.url }?.isEmpty() == true) {
|
||||
AddRelayButton { onAddRelay() }
|
||||
} else {
|
||||
RemoveRelayButton {
|
||||
onRemoveRelay()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Divider(
|
||||
modifier = Modifier.padding(top = 10.dp),
|
||||
thickness = 0.25.dp
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun AddRelayButton(onClick: () -> Unit) {
|
||||
Button(
|
||||
modifier = Modifier.padding(horizontal = 3.dp),
|
||||
onClick = onClick,
|
||||
shape = RoundedCornerShape(20.dp),
|
||||
colors = ButtonDefaults
|
||||
.buttonColors(
|
||||
backgroundColor = MaterialTheme.colors.primary
|
||||
),
|
||||
contentPadding = PaddingValues(vertical = 6.dp, horizontal = 16.dp)
|
||||
) {
|
||||
Text(text = "Add", color = Color.White)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun RemoveRelayButton(onClick: () -> Unit) {
|
||||
Button(
|
||||
modifier = Modifier.padding(horizontal = 3.dp),
|
||||
onClick = onClick,
|
||||
shape = RoundedCornerShape(20.dp),
|
||||
colors = ButtonDefaults
|
||||
.buttonColors(
|
||||
backgroundColor = MaterialTheme.colors.primary
|
||||
),
|
||||
contentPadding = PaddingValues(vertical = 6.dp, horizontal = 16.dp)
|
||||
) {
|
||||
Text(text = "Remove", color = Color.White)
|
||||
}
|
||||
}
|
||||
|
||||
fun formattedDateTime(timestamp: Long): String {
|
||||
return Instant.ofEpochSecond(timestamp).atZone(ZoneId.systemDefault())
|
||||
.format(DateTimeFormatter.ofPattern("MMM d, uuuu hh:mm a"))
|
||||
}
|
@ -4,6 +4,7 @@ import android.text.format.DateUtils
|
||||
|
||||
fun timeAgo(mills: Long?): String {
|
||||
if (mills == null) return " "
|
||||
if (mills == 0L) return " • never"
|
||||
|
||||
var humanReadable = DateUtils.getRelativeTimeSpanString(
|
||||
mills * 1000,
|
||||
|
@ -0,0 +1,130 @@
|
||||
package com.vitorpamplona.amethyst.ui.screen
|
||||
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.PaddingValues
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.itemsIndexed
|
||||
import androidx.compose.foundation.lazy.rememberLazyListState
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.collectAsState
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.livedata.observeAsState
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import androidx.navigation.NavController
|
||||
import com.google.accompanist.swiperefresh.SwipeRefresh
|
||||
import com.google.accompanist.swiperefresh.rememberSwipeRefreshState
|
||||
import com.vitorpamplona.amethyst.model.User
|
||||
import com.vitorpamplona.amethyst.ui.actions.NewRelayListView
|
||||
import com.vitorpamplona.amethyst.ui.note.RelayCompose
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.asStateFlow
|
||||
import kotlinx.coroutines.flow.update
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
|
||||
class RelayFeedViewModel: ViewModel() {
|
||||
val order = compareByDescending<User.RelayInfo> { it.lastEvent }.thenByDescending { it.counter }.thenBy { it.url }
|
||||
|
||||
private val _feedContent = MutableStateFlow<List<User.RelayInfo>>(emptyList())
|
||||
val feedContent = _feedContent.asStateFlow()
|
||||
|
||||
var currentUser: User? = null
|
||||
|
||||
fun refresh() {
|
||||
val beingUsed = currentUser?.relaysBeingUsed?.values?.toList() ?: emptyList()
|
||||
val beingUsedSet = currentUser?.relaysBeingUsed?.keys ?: emptySet()
|
||||
|
||||
val newRelaysFromRecord = currentUser?.relays?.entries?.mapNotNull {
|
||||
if (it.key !in beingUsedSet) {
|
||||
User.RelayInfo(it.key, 0, 0)
|
||||
} else {
|
||||
null
|
||||
}
|
||||
} ?: emptyList()
|
||||
|
||||
viewModelScope.launch {
|
||||
withContext(Dispatchers.Main) {
|
||||
_feedContent.update { (beingUsed + newRelaysFromRecord).sortedWith(order) }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
inner class CacheListener: User.Listener() {
|
||||
override fun onNewRelayInfo() { refresh() }
|
||||
override fun onRelayChange() { refresh() }
|
||||
}
|
||||
|
||||
val listener = CacheListener()
|
||||
|
||||
fun subscribeTo(user: User) {
|
||||
currentUser = user
|
||||
user.subscribe(listener)
|
||||
refresh()
|
||||
}
|
||||
|
||||
fun unsubscribeTo(user: User) {
|
||||
user.unsubscribe(listener)
|
||||
currentUser = null
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun RelayFeedView(viewModel: RelayFeedViewModel, accountViewModel: AccountViewModel, navController: NavController) {
|
||||
val accountState by accountViewModel.accountLiveData.observeAsState()
|
||||
val account = accountState?.account ?: return
|
||||
|
||||
val feedState by viewModel.feedContent.collectAsState()
|
||||
|
||||
var isRefreshing by remember { mutableStateOf(false) }
|
||||
val swipeRefreshState = rememberSwipeRefreshState(isRefreshing)
|
||||
|
||||
var wantsToAddRelay by remember {
|
||||
mutableStateOf( "")
|
||||
}
|
||||
|
||||
if (wantsToAddRelay.isNotEmpty())
|
||||
NewRelayListView({ wantsToAddRelay = "" }, account, wantsToAddRelay)
|
||||
|
||||
LaunchedEffect(isRefreshing) {
|
||||
if (isRefreshing) {
|
||||
viewModel.refresh()
|
||||
isRefreshing = false
|
||||
}
|
||||
}
|
||||
|
||||
SwipeRefresh(
|
||||
state = swipeRefreshState,
|
||||
onRefresh = {
|
||||
isRefreshing = true
|
||||
},
|
||||
) {
|
||||
Column() {
|
||||
val listState = rememberLazyListState()
|
||||
|
||||
LazyColumn(
|
||||
contentPadding = PaddingValues(
|
||||
top = 10.dp,
|
||||
bottom = 10.dp
|
||||
),
|
||||
state = listState
|
||||
) {
|
||||
itemsIndexed(feedState, key = { _, item -> item.url }) { index, item ->
|
||||
RelayCompose(item,
|
||||
accountViewModel = accountViewModel,
|
||||
navController = navController,
|
||||
onAddRelay = { wantsToAddRelay = item.url },
|
||||
onRemoveRelay = { wantsToAddRelay = item.url }
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -78,6 +78,7 @@ import com.vitorpamplona.amethyst.service.NostrUserProfileFollowersDataSource
|
||||
import com.vitorpamplona.amethyst.service.NostrUserProfileFollowsDataSource
|
||||
import com.vitorpamplona.amethyst.service.model.ReportEvent
|
||||
import com.vitorpamplona.amethyst.ui.actions.NewChannelView
|
||||
import com.vitorpamplona.amethyst.ui.actions.NewRelayListView
|
||||
import com.vitorpamplona.amethyst.ui.actions.NewUserMetadataView
|
||||
import com.vitorpamplona.amethyst.ui.note.UserPicture
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
||||
@ -144,7 +145,7 @@ fun ProfileScreen(userId: String?, accountViewModel: AccountViewModel, navContro
|
||||
selected = pagerState.currentPage == 1,
|
||||
onClick = { coroutineScope.launch { pagerState.animateScrollToPage(1) } },
|
||||
text = {
|
||||
Text(text = "${user.follows?.size ?: "--"} Following")
|
||||
Text(text = "${user.follows?.size ?: "--"} Follows")
|
||||
}
|
||||
)
|
||||
|
||||
@ -152,15 +153,24 @@ fun ProfileScreen(userId: String?, accountViewModel: AccountViewModel, navContro
|
||||
selected = pagerState.currentPage == 2,
|
||||
onClick = { coroutineScope.launch { pagerState.animateScrollToPage(2) } },
|
||||
text = {
|
||||
Text(text = "${user.followers?.size ?: "--"} Followers")
|
||||
Text(text = "${user.followers?.size ?: "--"} Follower")
|
||||
}
|
||||
)
|
||||
|
||||
Tab(
|
||||
selected = pagerState.currentPage == 3,
|
||||
onClick = { coroutineScope.launch { pagerState.animateScrollToPage(3) } },
|
||||
text = {
|
||||
Text(text = "${user.relaysBeingUsed.size ?: "--"} / ${user.relays?.size ?: "--"} Relays")
|
||||
}
|
||||
)
|
||||
}
|
||||
HorizontalPager(count = 3, state = pagerState) {
|
||||
HorizontalPager(count = 4, state = pagerState) {
|
||||
when (pagerState.currentPage) {
|
||||
0 -> TabNotes(user, accountViewModel, navController)
|
||||
1 -> TabFollows(user, accountViewModel, navController)
|
||||
2 -> TabFollowers(user, accountViewModel, navController)
|
||||
3 -> TabRelays(user, accountViewModel, navController)
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -344,6 +354,26 @@ fun TabFollowers(user: User, accountViewModel: AccountViewModel, navController:
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun TabRelays(user: User, accountViewModel: AccountViewModel, navController: NavController) {
|
||||
val feedViewModel: RelayFeedViewModel = viewModel()
|
||||
|
||||
DisposableEffect(key1 = user) {
|
||||
feedViewModel.subscribeTo(user)
|
||||
onDispose {
|
||||
feedViewModel.unsubscribeTo(user)
|
||||
}
|
||||
}
|
||||
|
||||
Column(Modifier.fillMaxHeight()) {
|
||||
Column(
|
||||
modifier = Modifier.padding(vertical = 0.dp)
|
||||
) {
|
||||
RelayFeedView(feedViewModel, accountViewModel, navController)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun NSecCopyButton(
|
||||
account: Account
|
||||
|
Loading…
x
Reference in New Issue
Block a user