Moving observable classes to the leaf nodes.

This commit is contained in:
Vitor Pamplona 2023-02-02 14:34:34 -05:00
parent b3ce10c2de
commit fb73308995
24 changed files with 323 additions and 288 deletions

View File

@ -369,7 +369,7 @@ class Account(
}
}
fun isHidden(user: User) = user !in hiddenUsers()
fun isHidden(user: User) = user in hiddenUsers()
fun isAcceptable(user: User): Boolean {
return user !in hiddenUsers() // if user hasn't hided this author

View File

@ -72,8 +72,6 @@ object LocalCache {
// new event
val oldUser = getOrCreateUser(event.pubKey.toHexKey())
if (event.createdAt > oldUser.updatedMetadataAt) {
//Log.d("MT", "New User ${users.size} ${event.contactMetaData.name}")
val newUser = try {
metadataParser.readValue<UserMetadata>(ByteArrayInputStream(event.content.toByteArray(Charsets.UTF_8)), UserMetadata::class.java)
} catch (e: Exception) {
@ -84,6 +82,8 @@ object LocalCache {
oldUser.updateUserInfo(newUser, event.createdAt)
oldUser.latestMetadata = event
//Log.d("MT", "New User Metadata ${oldUser.pubkeyDisplayHex} ${oldUser.toBestDisplayName()}")
} else {
//Log.d("MT","Relay sent a previous Metadata Event ${oldUser.toBestDisplayName()} ${formattedDateTime(event.createdAt)} > ${formattedDateTime(oldUser.updatedAt)}")
}

View File

@ -43,6 +43,8 @@ class Note(val idHex: String) {
var channel: Channel? = null
var lastDownloadTime: Long? = null
fun loadEvent(event: Event, author: User, mentions: List<User>, replyTo: MutableList<Note>) {
this.event = event
this.author = author
@ -128,7 +130,7 @@ class Note(val idHex: String) {
val returningList = mutableSetOf<User>()
while (matcher.find()) {
try {
val tag = event?.tags?.get(matcher.group(1).toInt())
val tag = matcher.group(1)?.let { event?.tags?.get(it.toInt()) }
if (tag != null && tag[0] == "p") {
returningList.add(LocalCache.getOrCreateUser(tag[1]))
}

View File

@ -64,8 +64,8 @@ class User(val pubkeyHex: String) {
follows.add(user)
user.followers.add(this)
invalidateData()
user.invalidateData()
invalidateData(liveFollows)
user.invalidateData(liveFollows)
listeners.forEach {
it.onFollowsChange()
@ -75,8 +75,8 @@ class User(val pubkeyHex: String) {
follows.remove(user)
user.followers.remove(this)
invalidateData()
user.invalidateData()
invalidateData(liveFollows)
user.invalidateData(liveFollows)
updateSubscribers {
it.onFollowsChange()
@ -91,7 +91,7 @@ class User(val pubkeyHex: String) {
fun addReport(note: Note) {
if (reports.add(note)) {
updateSubscribers { it.onNewReports() }
invalidateData()
invalidateData(liveReports)
}
}
@ -118,7 +118,7 @@ class User(val pubkeyHex: String) {
fun addMessage(user: User, msg: Note) {
getOrCreateChannel(user).add(msg)
invalidateData()
invalidateData(liveMessages)
updateSubscribers { it.onNewMessage() }
}
@ -140,6 +140,7 @@ class User(val pubkeyHex: String) {
}
updateSubscribers { it.onNewRelayInfo() }
invalidateData(liveRelayInfo)
}
fun updateFollows(newFollows: Set<User>, updateAt: Long) {
@ -167,15 +168,14 @@ class User(val pubkeyHex: String) {
}
}
invalidateData()
invalidateData(liveRelays)
}
fun updateUserInfo(newUserInfo: UserMetadata, updateAt: Long) {
info = newUserInfo
updatedMetadataAt = updateAt
invalidateData()
invalidateData(liveMetadata)
}
fun isFollowing(user: User): Boolean {
@ -242,16 +242,21 @@ class User(val pubkeyHex: String) {
}
// UI Observers line up here.
val live: UserLiveData = UserLiveData(this)
val liveFollows: UserLiveData = UserLiveData(this)
val liveReports: UserLiveData = UserLiveData(this)
val liveMessages: UserLiveData = UserLiveData(this)
val liveRelays: UserLiveData = UserLiveData(this)
val liveRelayInfo: UserLiveData = UserLiveData(this)
val liveMetadata: UserLiveData = UserLiveData(this)
// Refreshes observers in batches.
var handlerWaiting = false
@Synchronized
fun invalidateData() {
fun invalidateData(live: UserLiveData) {
if (handlerWaiting) return
handlerWaiting = true
val scope = CoroutineScope(Job() + Dispatchers.Default)
val scope = CoroutineScope(Job() + Dispatchers.Main)
scope.launch {
delay(100)
live.refresh()

View File

@ -7,4 +7,9 @@ data class Channel (
val id: String = UUID.randomUUID().toString().substring(0,4)
) {
var filter: List<JsonFilter>? = null // Inactive when null
private var lastEOSE: Long? = null
fun updateEOSE(l: Long) {
lastEOSE = l
}
}

View File

@ -16,6 +16,7 @@ import com.vitorpamplona.amethyst.service.model.RepostEvent
import com.vitorpamplona.amethyst.service.relays.Client
import com.vitorpamplona.amethyst.service.relays.Relay
import java.util.Collections
import java.util.Date
import java.util.UUID
import kotlin.time.ExperimentalTime
import kotlin.time.measureTimedValue
@ -90,7 +91,7 @@ abstract class NostrDataSource<T>(val debugName: String) {
//Log.e("ERROR", "Relay ${relay.url}: ${error.message}")
}
override fun onRelayStateChange(type: Relay.Type, relay: Relay) {
override fun onRelayStateChange(type: Relay.Type, relay: Relay, channel: String?) {
//Log.d("RELAY", "Relay ${relay.url} ${when (type) {
// Relay.Type.CONNECT -> "connected."
// Relay.Type.DISCONNECT -> "disconnected."
@ -98,11 +99,10 @@ abstract class NostrDataSource<T>(val debugName: String) {
// Relay.Type.EOSE -> "sent all events it had stored."
//}}")
/*
if (type == Relay.Type.EOSE) {
// One everything is loaded, if new users are found, update filters
resetFilters()
}*/
if (type == Relay.Type.EOSE && channel != null) {
// updates a per subscripton since date
channels.filter { it.id == channel }.firstOrNull()?.updateEOSE(Date().time / 1000)
}
}
override fun onSendResponse(eventId: String, success: Boolean, message: String, relay: Relay) {

View File

@ -14,7 +14,7 @@ object NostrSingleUserDataSource: NostrDataSource<User>("SingleUserFeed") {
fun createUserFilter(): List<JsonFilter>? {
if (usersToWatch.isEmpty()) return null
return usersToWatch.map {
return usersToWatch.filter { LocalCache.getOrCreateUser(it).latestMetadata == null }.map {
JsonFilter(
kinds = listOf(MetadataEvent.kind),
authors = listOf(it),

View File

@ -72,8 +72,8 @@ object Client: RelayPool.Listener {
listeners.forEach { it.onError(error, subscriptionId, relay) }
}
override fun onRelayStateChange(type: Relay.Type, relay: Relay) {
listeners.forEach { it.onRelayStateChange(type, relay) }
override fun onRelayStateChange(type: Relay.Type, relay: Relay, channel: String?) {
listeners.forEach { it.onRelayStateChange(type, relay, channel) }
}
override fun onSendResponse(eventId: String, success: Boolean, message: String, relay: Relay) {
@ -112,7 +112,7 @@ object Client: RelayPool.Listener {
/**
* Connected to or disconnected from a relay
*/
open fun onRelayStateChange(type: Relay.Type, relay: Relay) = Unit
open fun onRelayStateChange(type: Relay.Type, relay: Relay, channel: String?) = Unit
/**
* When an relay saves or rejects a new event.

View File

@ -2,11 +2,8 @@ package com.vitorpamplona.amethyst.service.relays
import android.util.Log
import com.google.gson.JsonElement
import com.vitorpamplona.amethyst.model.LocalCache
import java.util.Date
import nostr.postr.events.ContactListEvent
import nostr.postr.events.Event
import nostr.postr.toNpub
import okhttp3.OkHttpClient
import okhttp3.Request
import okhttp3.Response
@ -50,7 +47,7 @@ class Relay(
Client.allSubscriptions().forEach {
sendFilter(requestId = it)
}
listeners.forEach { it.onRelayStateChange(this@Relay, Type.CONNECT) }
listeners.forEach { it.onRelayStateChange(this@Relay, Type.CONNECT, null) }
}
override fun onMessage(webSocket: WebSocket, text: String) {
@ -65,7 +62,7 @@ class Relay(
listeners.forEach { it.onEvent(this@Relay, channel, event) }
}
"EOSE" -> listeners.forEach {
it.onRelayStateChange(this@Relay, Type.EOSE)
it.onRelayStateChange(this@Relay, Type.EOSE, channel)
}
"NOTICE" -> listeners.forEach {
// "channel" being the second string in the string array ...
@ -91,13 +88,17 @@ class Relay(
}
override fun onClosing(webSocket: WebSocket, code: Int, reason: String) {
listeners.forEach { it.onRelayStateChange(this@Relay, Type.DISCONNECTING) }
listeners.forEach { it.onRelayStateChange(
this@Relay,
Type.DISCONNECTING,
null
) }
}
override fun onClosed(webSocket: WebSocket, code: Int, reason: String) {
socket = null
closingTime = Date().time / 1000
listeners.forEach { it.onRelayStateChange(this@Relay, Type.DISCONNECT) }
listeners.forEach { it.onRelayStateChange(this@Relay, Type.DISCONNECT, null) }
}
override fun onFailure(webSocket: WebSocket, t: Throwable, response: Response?) {
@ -192,6 +193,6 @@ class Relay(
*
* @param type is 0 for disconnect and 1 for connect
*/
fun onRelayStateChange(relay: Relay, type: Type)
fun onRelayStateChange(relay: Relay, type: Type, channel: String?)
}
}

View File

@ -2,7 +2,6 @@ package com.vitorpamplona.amethyst.service.relays
import androidx.lifecycle.LiveData
import com.vitorpamplona.amethyst.service.Constants
import java.util.Collections
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
@ -88,7 +87,7 @@ object RelayPool: Relay.Listener {
fun onError(error: Error, subscriptionId: String, relay: Relay)
fun onRelayStateChange(type: Relay.Type, relay: Relay)
fun onRelayStateChange(type: Relay.Type, relay: Relay, channel: String?)
fun onSendResponse(eventId: String, success: Boolean, message: String, relay: Relay)
}
@ -103,8 +102,8 @@ object RelayPool: Relay.Listener {
refreshObservers()
}
override fun onRelayStateChange(relay: Relay, type: Relay.Type) {
listeners.forEach { it.onRelayStateChange(type, relay) }
override fun onRelayStateChange(relay: Relay, type: Relay.Type, channel: String?) {
listeners.forEach { it.onRelayStateChange(type, relay, channel) }
refreshObservers()
}

View File

@ -24,7 +24,8 @@ fun ClickableRoute(
) {
if (nip19.type == Nip19.Type.USER) {
val userBase = LocalCache.getOrCreateUser(nip19.hex)
val userState by userBase.live.observeAsState()
val userState by userBase.liveMetadata.observeAsState()
val user = userState?.user ?: return
val route = "User/${nip19.hex}"

View File

@ -15,7 +15,7 @@ fun ClickableUserTag(
user: User,
navController: NavController
) {
val innerUserState by user.live.observeAsState()
val innerUserState by user.liveMetadata.observeAsState()
ClickableText(
text = AnnotatedString("@${innerUserState?.user?.toBestDisplayName()} "),
onClick = { navController.navigate("User/${innerUserState?.user?.pubkeyHex}") },

View File

@ -77,7 +77,7 @@ fun MainTopBar(scaffoldState: ScaffoldState, accountViewModel: AccountViewModel)
val accountState by accountViewModel.accountLiveData.observeAsState()
val account = accountState?.account ?: return
val accountUserState by account.userProfile().live.observeAsState()
val accountUserState by account.userProfile().liveMetadata.observeAsState()
val accountUser = accountUserState?.user
val relayViewModel: RelayPoolViewModel = viewModel { RelayPoolViewModel() }

View File

@ -81,52 +81,26 @@ fun DrawerContent(navController: NavHostController,
val accountState by accountViewModel.accountLiveData.observeAsState()
val account = accountState?.account ?: return
val accountUserState by account.userProfile().live.observeAsState()
val accountUser = accountUserState?.user ?: return
Surface(
modifier = Modifier.fillMaxWidth(),
color = MaterialTheme.colors.background
) {
Column() {
Box {
val banner = accountUser?.info?.banner
if (banner != null && banner.isNotBlank()) {
AsyncImage(
model = banner,
contentDescription = "Profile Image",
contentScale = ContentScale.FillWidth,
modifier = Modifier
.fillMaxWidth()
.height(150.dp)
)
} else {
Image(
painter = painterResource(R.drawable.profile_banner),
contentDescription = "Profile Banner",
contentScale = ContentScale.FillWidth,
modifier = Modifier
.fillMaxWidth()
.height(150.dp)
)
}
ProfileContent(
accountUser,
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 25.dp)
.padding(top = 100.dp),
scaffoldState,
navController
)
}
ProfileContent(
account.userProfile(),
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 25.dp)
.padding(top = 100.dp),
scaffoldState,
navController
)
Divider(
thickness = 0.25.dp,
modifier = Modifier.padding(top = 20.dp)
)
ListContent(
accountUser,
account.userProfile(),
navController,
scaffoldState,
modifier = Modifier
@ -135,50 +109,79 @@ fun DrawerContent(navController: NavHostController,
accountStateViewModel
)
BottomContent(accountUser, scaffoldState, navController)
BottomContent(account.userProfile(), scaffoldState, navController)
}
}
}
@Composable
fun ProfileContent(accountUser: User?, modifier: Modifier = Modifier, scaffoldState: ScaffoldState, navController: NavController) {
fun ProfileContent(baseAccountUser: User, modifier: Modifier = Modifier, scaffoldState: ScaffoldState, navController: NavController) {
val coroutineScope = rememberCoroutineScope()
Column(modifier = modifier) {
AsyncImage(
model = accountUser?.profilePicture() ?: "https://robohash.org/ohno.png",
contentDescription = "Profile Image",
placeholder = rememberAsyncImagePainter("https://robohash.org/${accountUser?.pubkeyHex}.png"),
modifier = Modifier
.width(100.dp)
.height(100.dp)
.clip(shape = CircleShape)
.border(3.dp, MaterialTheme.colors.background, CircleShape)
.background(MaterialTheme.colors.background)
.clickable(onClick = {
accountUser?.let {
navController.navigate("User/${it.pubkeyHex}")
}
coroutineScope.launch {
scaffoldState.drawerState.close()
}
})
)
Text(
accountUser?.bestDisplayName() ?: "",
modifier = Modifier.padding(top = 7.dp),
fontWeight = FontWeight.Bold,
fontSize = 18.sp
)
Text(" @${accountUser?.bestUsername()}", color = Color.LightGray)
Row(modifier = Modifier.padding(top = 15.dp)) {
Row() {
Text("${accountUser?.follows?.size ?: "--"}", fontWeight = FontWeight.Bold)
Text(" Following")
}
Row(modifier = Modifier.padding(start = 10.dp)) {
Text("${accountUser?.followers?.size ?: "--"}", fontWeight = FontWeight.Bold)
Text(" Followers")
val accountUserState by baseAccountUser.liveMetadata.observeAsState()
val accountUser = accountUserState?.user ?: return
val accountUserFollowsState by baseAccountUser.liveFollows.observeAsState()
val accountUserFollows = accountUserFollowsState?.user ?: return
Box {
val banner = accountUser.info.banner
if (banner != null && banner.isNotBlank()) {
AsyncImage(
model = banner,
contentDescription = "Profile Image",
contentScale = ContentScale.FillWidth,
modifier = Modifier
.fillMaxWidth()
.height(150.dp)
)
} else {
Image(
painter = painterResource(R.drawable.profile_banner),
contentDescription = "Profile Banner",
contentScale = ContentScale.FillWidth,
modifier = Modifier
.fillMaxWidth()
.height(150.dp)
)
}
Column(modifier = modifier) {
AsyncImage(
model = accountUser.profilePicture(),
contentDescription = "Profile Image",
placeholder = rememberAsyncImagePainter("https://robohash.org/${accountUser.pubkeyHex}.png"),
modifier = Modifier
.width(100.dp)
.height(100.dp)
.clip(shape = CircleShape)
.border(3.dp, MaterialTheme.colors.background, CircleShape)
.background(MaterialTheme.colors.background)
.clickable(onClick = {
accountUser.let {
navController.navigate("User/${it.pubkeyHex}")
}
coroutineScope.launch {
scaffoldState.drawerState.close()
}
})
)
Text(
accountUser.bestDisplayName() ?: "",
modifier = Modifier.padding(top = 7.dp),
fontWeight = FontWeight.Bold,
fontSize = 18.sp
)
Text(" @${accountUser.bestUsername()}", color = Color.LightGray)
Row(modifier = Modifier.padding(top = 15.dp)) {
Row() {
Text("${accountUserFollows.follows?.size ?: "--"}", fontWeight = FontWeight.Bold)
Text(" Following")
}
Row(modifier = Modifier.padding(start = 10.dp)) {
Text("${accountUserFollows.followers?.size ?: "--"}", fontWeight = FontWeight.Bold)
Text(" Followers")
}
}
}
}

View File

@ -53,9 +53,6 @@ fun ChatroomCompose(baseNote: Note, accountViewModel: AccountViewModel, navContr
val accountState by accountViewModel.accountLiveData.observeAsState()
val account = accountState?.account ?: return
val accountUserState by account.userProfile().live.observeAsState()
val accountUser = accountUserState?.user ?: return
val notificationCacheState = NotificationCache.live.observeAsState()
val notificationCache = notificationCacheState.value ?: return
@ -64,7 +61,7 @@ fun ChatroomCompose(baseNote: Note, accountViewModel: AccountViewModel, navContr
if (note?.event == null) {
BlankNote(Modifier)
} else if (note.channel != null) {
val authorState by note.author!!.live.observeAsState()
val authorState by note.author!!.liveMetadata.observeAsState()
val author = authorState?.user
val channelState by note.channel!!.live.observeAsState()
@ -108,25 +105,19 @@ fun ChatroomCompose(baseNote: Note, accountViewModel: AccountViewModel, navContr
}
} else {
val authorState by note.author!!.live.observeAsState()
val author = authorState?.user
val replyAuthorBase = note.mentions?.first()
var userToComposeOn = author
var userToComposeOn = note.author!!
if ( replyAuthorBase != null ) {
val replyAuthorState by replyAuthorBase.live.observeAsState()
val replyAuthor = replyAuthorState?.user
if (author == accountUser) {
userToComposeOn = replyAuthor
if (replyAuthorBase != null) {
if (note.author == account.userProfile()) {
userToComposeOn = replyAuthorBase
}
}
val noteEvent = note.event
userToComposeOn?.let { user ->
userToComposeOn.let { user ->
val hasNewMessages =
if (noteEvent != null)
noteEvent.createdAt > notificationCache.cache.load("Room/${userToComposeOn.pubkeyHex}", context)
@ -134,8 +125,8 @@ fun ChatroomCompose(baseNote: Note, accountViewModel: AccountViewModel, navContr
false
ChannelName(
channelPicture = { UserPicture(user, accountUser, size = 55.dp) },
channelTitle = { UsernameDisplay(user, it) },
channelPicture = { UserPicture(userToComposeOn, account.userProfile(), size = 55.dp) },
channelTitle = { UsernameDisplay(userToComposeOn, it) },
channelLastTime = noteEvent?.createdAt,
channelLastContent = accountViewModel.decrypt(note),
hasNewMessages = hasNewMessages,

View File

@ -59,8 +59,7 @@ fun ChatroomMessageCompose(baseNote: Note, routeForLastRead: String?, innerQuote
val accountState by accountViewModel.accountLiveData.observeAsState()
val account = accountState?.account ?: return
val accountUserState by account.userProfile().live.observeAsState()
val accountUser = accountUserState?.user
val accountUser = account.userProfile()
var popupExpanded by remember { mutableStateOf(false) }
@ -69,14 +68,11 @@ fun ChatroomMessageCompose(baseNote: Note, routeForLastRead: String?, innerQuote
if (note?.event == null) {
BlankNote(Modifier)
} else {
val authorState by note.author!!.live.observeAsState()
val author = authorState?.user
var backgroundBubbleColor: Color
var alignment: Arrangement.Horizontal
var shape: Shape
if (author == accountUser) {
if (note.author == accountUser) {
backgroundBubbleColor = MaterialTheme.colors.primary.copy(alpha = 0.32f)
alignment = Arrangement.End
shape = ChatBubbleShapeMe
@ -125,6 +121,9 @@ fun ChatroomMessageCompose(baseNote: Note, routeForLastRead: String?, innerQuote
modifier = Modifier.padding(start = 10.dp, end = 10.dp, bottom = 5.dp),
) {
val authorState by note.author!!.liveMetadata.observeAsState()
val author = authorState?.user
if (innerQuote || author != accountUser && note.event is ChannelMessageEvent) {
Row(
verticalAlignment = Alignment.CenterVertically,

View File

@ -339,10 +339,7 @@ fun UserPicture(
pictureModifier: Modifier = Modifier,
onClick: ((User) -> Unit)? = null
) {
val accountState by baseUserAccount.live.observeAsState()
val accountUser = accountState?.user ?: return
val userState by baseUser.live.observeAsState()
val userState by baseUser.liveMetadata.observeAsState()
val user = userState?.user ?: return
Box(
@ -367,6 +364,9 @@ fun UserPicture(
)
val accountState by baseUserAccount.liveFollows.observeAsState()
val accountUser = accountState?.user ?: return
if (accountUser.isFollowing(user) || user == accountUser) {
Box(
Modifier

View File

@ -44,7 +44,7 @@ fun ReplyInformation(replyTo: MutableList<Note>?, mentions: List<User>?, prefix:
val mentionSet = mentions.toSet()
mentionSet.toSet().forEachIndexed { idx, user ->
val innerUserState by user.live.observeAsState()
val innerUserState by user.liveMetadata.observeAsState()
val innerUser = innerUserState?.user
innerUser?.let { myUser ->
@ -123,7 +123,7 @@ fun ReplyInformationChannel(replyTo: MutableList<Note>?,
val mentionSet = mentions.toSet()
mentionSet.forEachIndexed { idx, user ->
val innerUserState by user.live.observeAsState()
val innerUserState by user.liveMetadata.observeAsState()
val innerUser = innerUserState?.user
innerUser?.let { myUser ->

View File

@ -34,14 +34,11 @@ fun UserCompose(baseUser: User, accountViewModel: AccountViewModel, navControlle
val accountState by accountViewModel.accountLiveData.observeAsState()
val account = accountState?.account ?: return
val userState by baseUser.live.observeAsState()
val user = userState?.user ?: return
val ctx = LocalContext.current.applicationContext
Column(modifier =
Modifier.clickable(
onClick = { navController.navigate("User/${user.pubkeyHex}") }
onClick = { navController.navigate("User/${baseUser.pubkeyHex}") }
)
) {
Row(
@ -59,6 +56,9 @@ fun UserCompose(baseUser: User, accountViewModel: AccountViewModel, navControlle
UsernameDisplay(baseUser)
}
val userState by baseUser.liveMetadata.observeAsState()
val user = userState?.user ?: return
Text(
user.info.about ?: "",
color = MaterialTheme.colors.onSurface.copy(alpha = 0.32f),
@ -68,15 +68,15 @@ fun UserCompose(baseUser: User, accountViewModel: AccountViewModel, navControlle
}
Column(modifier = Modifier.padding(start = 10.dp)) {
if (account?.isHidden(user) == false) {
if (account.isHidden(baseUser)) {
ShowUserButton {
account.showUser(user.pubkeyHex)
account.showUser(baseUser.pubkeyHex)
LocalPreferences(ctx).saveToEncryptedStorage(account)
}
} else if (account?.userProfile()?.isFollowing(user) == true) {
UnfollowButton { account.unfollow(user) }
} else if (account.userProfile().isFollowing(baseUser)) {
UnfollowButton { account.unfollow(baseUser) }
} else {
FollowButton { account?.follow(user) }
FollowButton { account.follow(baseUser) }
}
}
}

View File

@ -26,7 +26,7 @@ fun NoteUsernameDisplay(baseNote: Note, weight: Modifier = Modifier) {
@Composable
fun UsernameDisplay(baseUser: User, weight: Modifier = Modifier) {
val userState by baseUser.live.observeAsState()
val userState by baseUser.liveMetadata.observeAsState()
val user = userState?.user ?: return
if (user.bestUsername() != null || user.bestDisplayName() != null) {

View File

@ -172,9 +172,6 @@ fun NoteMaster(baseNote: Note, accountViewModel: AccountViewModel, navController
} else if (!account.isAcceptable(noteForReports)) {
HiddenNote()
} else {
val authorState by note.author!!.live.observeAsState()
val author = authorState?.user
Column(
Modifier
.fillMaxWidth()
@ -182,7 +179,7 @@ fun NoteMaster(baseNote: Note, accountViewModel: AccountViewModel, navController
Row(modifier = Modifier
.padding(start = 12.dp, end = 12.dp)
.clickable(onClick = {
author?.let {
note.author?.let {
navController.navigate("User/${it.pubkeyHex}")
}
})

View File

@ -41,6 +41,7 @@ import coil.compose.rememberAsyncImagePainter
import com.vitorpamplona.amethyst.model.User
import com.vitorpamplona.amethyst.service.NostrChatRoomDataSource
import com.vitorpamplona.amethyst.ui.actions.PostButton
import com.vitorpamplona.amethyst.ui.note.UserPicture
import com.vitorpamplona.amethyst.ui.note.UsernameDisplay
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
@ -70,13 +71,18 @@ fun ChatroomScreen(userId: String?, accountViewModel: AccountViewModel, navContr
}
Column(
modifier = Modifier.fillMaxHeight().padding(vertical = 0.dp).weight(1f, true)
modifier = Modifier
.fillMaxHeight()
.padding(vertical = 0.dp)
.weight(1f, true)
) {
ChatroomFeedView(feedViewModel, accountViewModel, navController, "Room/${userId}")
}
//LAST ROW
Row(modifier = Modifier.padding(10.dp).fillMaxWidth(),
Row(modifier = Modifier
.padding(10.dp)
.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically
) {
@ -118,22 +124,23 @@ fun ChatroomScreen(userId: String?, accountViewModel: AccountViewModel, navContr
@Composable
fun ChatroomHeader(baseUser: User, accountViewModel: AccountViewModel, navController: NavController) {
val authorState by baseUser.live.observeAsState()
val author = authorState?.user
Column(modifier = Modifier.clickable(
onClick = { navController.navigate("User/${author?.pubkeyHex}") }
onClick = { navController.navigate("User/${baseUser.pubkeyHex}") }
)
) {
Column(modifier = Modifier.padding(12.dp)) {
Row(verticalAlignment = Alignment.CenterVertically) {
val authorState by baseUser.liveMetadata.observeAsState()
val author = authorState?.user
AsyncImage(
model = author?.profilePicture(),
placeholder = rememberAsyncImagePainter("https://robohash.org/${author?.pubkeyHex}.png"),
contentDescription = "Profile Image",
modifier = Modifier
.width(35.dp).height(35.dp)
.width(35.dp)
.height(35.dp)
.clip(shape = CircleShape)
)

View File

@ -94,86 +94,92 @@ fun ProfileScreen(userId: String?, accountViewModel: AccountViewModel, navContro
val accountState by accountViewModel.accountLiveData.observeAsState()
val account = accountState?.account ?: return
val accountUserState by account.userProfile().live.observeAsState()
val accountUser = accountUserState?.user
if (userId == null) return
if (userId != null && accountUser != null) {
DisposableEffect(account) {
NostrUserProfileDataSource.loadUserProfile(userId)
NostrUserProfileFollowersDataSource.loadUserProfile(userId)
NostrUserProfileFollowsDataSource.loadUserProfile(userId)
DisposableEffect(account) {
NostrUserProfileDataSource.loadUserProfile(userId)
NostrUserProfileFollowersDataSource.loadUserProfile(userId)
NostrUserProfileFollowsDataSource.loadUserProfile(userId)
onDispose {
NostrUserProfileDataSource.stop()
NostrUserProfileFollowsDataSource.stop()
NostrUserProfileFollowersDataSource.stop()
}
onDispose {
NostrUserProfileDataSource.stop()
NostrUserProfileFollowsDataSource.stop()
NostrUserProfileFollowersDataSource.stop()
}
}
val baseUser = NostrUserProfileDataSource.user ?: return
val baseUser = NostrUserProfileDataSource.user ?: return
val userState by baseUser.live.observeAsState()
val user = userState?.user ?: return
Surface(
modifier = Modifier.fillMaxWidth(),
color = MaterialTheme.colors.background
) {
Column() {
ProfileHeader(baseUser, navController, account, accountViewModel)
Surface(
modifier = Modifier.fillMaxWidth(),
color = MaterialTheme.colors.background
) {
Column() {
ProfileHeader(user, navController, account, accountUser, accountViewModel)
val pagerState = rememberPagerState()
val coroutineScope = rememberCoroutineScope()
val pagerState = rememberPagerState()
val coroutineScope = rememberCoroutineScope()
Column(modifier = Modifier.padding()) {
TabRow(
selectedTabIndex = pagerState.currentPage,
indicator = { tabPositions ->
TabRowDefaults.Indicator(
Modifier.pagerTabIndicatorOffset(pagerState, tabPositions),
color = MaterialTheme.colors.primary
)
},
) {
Tab(
selected = pagerState.currentPage == 0,
onClick = { coroutineScope.launch { pagerState.animateScrollToPage(0) } },
text = {
Text(text = "Notes")
}
Column(modifier = Modifier.padding()) {
TabRow(
selectedTabIndex = pagerState.currentPage,
indicator = { tabPositions ->
TabRowDefaults.Indicator(
Modifier.pagerTabIndicatorOffset(pagerState, tabPositions),
color = MaterialTheme.colors.primary
)
Tab(
selected = pagerState.currentPage == 1,
onClick = { coroutineScope.launch { pagerState.animateScrollToPage(1) } },
text = {
Text(text = "${user.follows?.size ?: "--"} Follows")
}
)
Tab(
selected = pagerState.currentPage == 2,
onClick = { coroutineScope.launch { pagerState.animateScrollToPage(2) } },
text = {
Text(text = "${user.followers?.size ?: "--"} Followers")
}
)
Tab(
selected = pagerState.currentPage == 3,
onClick = { coroutineScope.launch { pagerState.animateScrollToPage(3) } },
text = {
Text(text = "${user.relaysBeingUsed.size ?: "--"} / ${user.relays?.size ?: "--"} Relays")
}
)
}
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(baseUser, accountViewModel, navController)
},
) {
Tab(
selected = pagerState.currentPage == 0,
onClick = { coroutineScope.launch { pagerState.animateScrollToPage(0) } },
text = {
Text(text = "Notes")
}
)
Tab(
selected = pagerState.currentPage == 1,
onClick = { coroutineScope.launch { pagerState.animateScrollToPage(1) } },
text = {
val userState by baseUser.liveFollows.observeAsState()
val userFollows = userState?.user?.follows?.size ?: "--"
Text(text = "$userFollows Follows")
}
)
Tab(
selected = pagerState.currentPage == 2,
onClick = { coroutineScope.launch { pagerState.animateScrollToPage(2) } },
text = {
val userState by baseUser.liveFollows.observeAsState()
val userFollows = userState?.user?.followers?.size ?: "--"
Text(text = "$userFollows Followers")
}
)
Tab(
selected = pagerState.currentPage == 3,
onClick = { coroutineScope.launch { pagerState.animateScrollToPage(3) } },
text = {
val userState by baseUser.liveRelays.observeAsState()
val userRelaysBeingUsed = userState?.user?.relaysBeingUsed?.size ?: "--"
val userStateRelayInfo by baseUser.liveRelayInfo.observeAsState()
val userRelays = userStateRelayInfo?.user?.relays?.size ?: "--"
Text(text = "$userRelaysBeingUsed / $userRelays Relays")
}
)
}
HorizontalPager(count = 4, state = pagerState) {
when (pagerState.currentPage) {
0 -> TabNotes(baseUser, accountViewModel, navController)
1 -> TabFollows(baseUser, accountViewModel, navController)
2 -> TabFollowers(baseUser, accountViewModel, navController)
3 -> TabRelays(baseUser, accountViewModel, navController)
}
}
}
@ -183,36 +189,19 @@ fun ProfileScreen(userId: String?, accountViewModel: AccountViewModel, navContro
@Composable
private fun ProfileHeader(
user: User,
baseUser: User,
navController: NavController,
account: Account,
accountUser: User,
accountViewModel: AccountViewModel
) {
val ctx = LocalContext.current.applicationContext
var popupExpanded by remember { mutableStateOf(false) }
val accountUserState by account.userProfile().liveFollows.observeAsState()
val accountUser = accountUserState?.user ?: return
Box {
val banner = user.info.banner
if (banner != null && banner.isNotBlank()) {
AsyncImage(
model = banner,
contentDescription = "Profile Image",
contentScale = ContentScale.FillWidth,
modifier = Modifier
.fillMaxWidth()
.height(125.dp)
)
} else {
Image(
painter = painterResource(R.drawable.profile_banner),
contentDescription = "Profile Banner",
contentScale = ContentScale.FillWidth,
modifier = Modifier
.fillMaxWidth()
.height(125.dp)
)
}
DrawBanner(baseUser)
Box(modifier = Modifier
.padding(horizontal = 10.dp)
@ -237,7 +226,7 @@ private fun ProfileHeader(
contentDescription = "More Options",
)
UserProfileDropDownMenu(user, popupExpanded, { popupExpanded = false }, accountViewModel)
UserProfileDropDownMenu(baseUser, popupExpanded, { popupExpanded = false }, accountViewModel)
}
}
@ -255,7 +244,7 @@ private fun ProfileHeader(
) {
UserPicture(
user, navController, account.userProfile(), 100.dp,
baseUser, navController, account.userProfile(), 100.dp,
pictureModifier = Modifier.border(
3.dp,
MaterialTheme.colors.background,
@ -268,52 +257,88 @@ private fun ProfileHeader(
Row(modifier = Modifier
.height(35.dp)
.padding(bottom = 3.dp)) {
MessageButton(user, navController)
MessageButton(baseUser, navController)
if (accountUser == user && account.isWriteable()) {
if (accountUser == baseUser && account.isWriteable()) {
NSecCopyButton(account)
}
NPubCopyButton(user)
NPubCopyButton(baseUser)
if (accountUser == user) {
if (accountUser == baseUser) {
EditButton(account)
} else {
if (!account.isHidden(user)) {
if (account.isHidden(baseUser)) {
ShowUserButton {
account.showUser(user.pubkeyHex)
account.showUser(baseUser.pubkeyHex)
LocalPreferences(ctx).saveToEncryptedStorage(account)
}
} else if (accountUser.isFollowing(user)) {
UnfollowButton { account.unfollow(user) }
} else if (accountUser.isFollowing(baseUser)) {
UnfollowButton { account.unfollow(baseUser) }
} else {
FollowButton { account.follow(user) }
FollowButton { account.follow(baseUser) }
}
}
}
}
Text(
user.bestDisplayName() ?: "",
modifier = Modifier.padding(top = 7.dp),
fontWeight = FontWeight.Bold,
fontSize = 25.sp
)
Text(
" @${user.bestUsername()}",
color = MaterialTheme.colors.onSurface.copy(alpha = 0.32f)
)
Text(
"${user.info.about}",
color = MaterialTheme.colors.onSurface,
modifier = Modifier.padding(top = 5.dp, bottom = 5.dp)
)
DrawAdditionalInfo(baseUser)
Divider(modifier = Modifier.padding(top = 6.dp))
}
}
}
@Composable
private fun DrawAdditionalInfo(baseUser: User) {
val userState by baseUser.liveMetadata.observeAsState()
val user = userState?.user ?: return
Text(
user.bestDisplayName() ?: "",
modifier = Modifier.padding(top = 7.dp),
fontWeight = FontWeight.Bold,
fontSize = 25.sp
)
Text(
" @${user.bestUsername()}",
color = MaterialTheme.colors.onSurface.copy(alpha = 0.32f)
)
Text(
"${user.info.about}",
color = MaterialTheme.colors.onSurface,
modifier = Modifier.padding(top = 5.dp, bottom = 5.dp)
)
}
@Composable
private fun DrawBanner(baseUser: User) {
val userState by baseUser.liveMetadata.observeAsState()
val user = userState?.user ?: return
val banner = user.info.banner
if (banner != null && banner.isNotBlank()) {
AsyncImage(
model = banner,
contentDescription = "Profile Image",
contentScale = ContentScale.FillWidth,
modifier = Modifier
.fillMaxWidth()
.height(125.dp)
)
} else {
Image(
painter = painterResource(R.drawable.profile_banner),
contentDescription = "Profile Banner",
contentScale = ContentScale.FillWidth,
modifier = Modifier
.fillMaxWidth()
.height(125.dp)
)
}
}
@Composable
fun TabNotes(user: User, accountViewModel: AccountViewModel, navController: NavController) {
val accountState by accountViewModel.accountLiveData.observeAsState()
@ -560,7 +585,7 @@ fun UserProfileDropDownMenu(user: User, popupExpanded: Boolean, onDismiss: () ->
if ( account.userProfile() != user) {
Divider()
if (!account.isHidden(user)) {
if (account.isHidden(user)) {
DropdownMenuItem(onClick = {
user.let {
accountViewModel.show(

View File

@ -210,9 +210,6 @@ fun UserLine(
account: Account,
onClick: () -> Unit
) {
val userState by baseUser.live.observeAsState()
val user = userState?.user ?: return
Column(modifier = Modifier
.fillMaxWidth()
.clickable(onClick = onClick)
@ -226,7 +223,7 @@ fun UserLine(
)
) {
UserPicture(user, account.userProfile(), 55.dp, Modifier, null)
UserPicture(baseUser, account.userProfile(), 55.dp, Modifier, null)
Column(
modifier = Modifier
@ -237,6 +234,9 @@ fun UserLine(
UsernameDisplay(baseUser)
}
val userState by baseUser.liveMetadata.observeAsState()
val user = userState?.user ?: return
Text(
user.info.about?.take(100) ?: "",
color = MaterialTheme.colors.onSurface.copy(alpha = 0.32f),