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
@@ -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 { fun isAcceptable(user: User): Boolean {
return user !in hiddenUsers() // if user hasn't hided this author return user !in hiddenUsers() // if user hasn't hided this author
@@ -72,8 +72,6 @@ object LocalCache {
// new event // new event
val oldUser = getOrCreateUser(event.pubKey.toHexKey()) val oldUser = getOrCreateUser(event.pubKey.toHexKey())
if (event.createdAt > oldUser.updatedMetadataAt) { if (event.createdAt > oldUser.updatedMetadataAt) {
//Log.d("MT", "New User ${users.size} ${event.contactMetaData.name}")
val newUser = try { val newUser = try {
metadataParser.readValue<UserMetadata>(ByteArrayInputStream(event.content.toByteArray(Charsets.UTF_8)), UserMetadata::class.java) metadataParser.readValue<UserMetadata>(ByteArrayInputStream(event.content.toByteArray(Charsets.UTF_8)), UserMetadata::class.java)
} catch (e: Exception) { } catch (e: Exception) {
@@ -84,6 +82,8 @@ object LocalCache {
oldUser.updateUserInfo(newUser, event.createdAt) oldUser.updateUserInfo(newUser, event.createdAt)
oldUser.latestMetadata = event oldUser.latestMetadata = event
//Log.d("MT", "New User Metadata ${oldUser.pubkeyDisplayHex} ${oldUser.toBestDisplayName()}")
} else { } else {
//Log.d("MT","Relay sent a previous Metadata Event ${oldUser.toBestDisplayName()} ${formattedDateTime(event.createdAt)} > ${formattedDateTime(oldUser.updatedAt)}") //Log.d("MT","Relay sent a previous Metadata Event ${oldUser.toBestDisplayName()} ${formattedDateTime(event.createdAt)} > ${formattedDateTime(oldUser.updatedAt)}")
} }
@@ -43,6 +43,8 @@ class Note(val idHex: String) {
var channel: Channel? = null var channel: Channel? = null
var lastDownloadTime: Long? = null
fun loadEvent(event: Event, author: User, mentions: List<User>, replyTo: MutableList<Note>) { fun loadEvent(event: Event, author: User, mentions: List<User>, replyTo: MutableList<Note>) {
this.event = event this.event = event
this.author = author this.author = author
@@ -128,7 +130,7 @@ class Note(val idHex: String) {
val returningList = mutableSetOf<User>() val returningList = mutableSetOf<User>()
while (matcher.find()) { while (matcher.find()) {
try { 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") { if (tag != null && tag[0] == "p") {
returningList.add(LocalCache.getOrCreateUser(tag[1])) returningList.add(LocalCache.getOrCreateUser(tag[1]))
} }
@@ -64,8 +64,8 @@ class User(val pubkeyHex: String) {
follows.add(user) follows.add(user)
user.followers.add(this) user.followers.add(this)
invalidateData() invalidateData(liveFollows)
user.invalidateData() user.invalidateData(liveFollows)
listeners.forEach { listeners.forEach {
it.onFollowsChange() it.onFollowsChange()
@@ -75,8 +75,8 @@ class User(val pubkeyHex: String) {
follows.remove(user) follows.remove(user)
user.followers.remove(this) user.followers.remove(this)
invalidateData() invalidateData(liveFollows)
user.invalidateData() user.invalidateData(liveFollows)
updateSubscribers { updateSubscribers {
it.onFollowsChange() it.onFollowsChange()
@@ -91,7 +91,7 @@ class User(val pubkeyHex: String) {
fun addReport(note: Note) { fun addReport(note: Note) {
if (reports.add(note)) { if (reports.add(note)) {
updateSubscribers { it.onNewReports() } updateSubscribers { it.onNewReports() }
invalidateData() invalidateData(liveReports)
} }
} }
@@ -118,7 +118,7 @@ class User(val pubkeyHex: String) {
fun addMessage(user: User, msg: Note) { fun addMessage(user: User, msg: Note) {
getOrCreateChannel(user).add(msg) getOrCreateChannel(user).add(msg)
invalidateData() invalidateData(liveMessages)
updateSubscribers { it.onNewMessage() } updateSubscribers { it.onNewMessage() }
} }
@@ -140,6 +140,7 @@ class User(val pubkeyHex: String) {
} }
updateSubscribers { it.onNewRelayInfo() } updateSubscribers { it.onNewRelayInfo() }
invalidateData(liveRelayInfo)
} }
fun updateFollows(newFollows: Set<User>, updateAt: Long) { 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) { fun updateUserInfo(newUserInfo: UserMetadata, updateAt: Long) {
info = newUserInfo info = newUserInfo
updatedMetadataAt = updateAt updatedMetadataAt = updateAt
invalidateData(liveMetadata)
invalidateData()
} }
fun isFollowing(user: User): Boolean { fun isFollowing(user: User): Boolean {
@@ -242,16 +242,21 @@ class User(val pubkeyHex: String) {
} }
// UI Observers line up here. // 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. // Refreshes observers in batches.
var handlerWaiting = false var handlerWaiting = false
@Synchronized @Synchronized
fun invalidateData() { fun invalidateData(live: UserLiveData) {
if (handlerWaiting) return if (handlerWaiting) return
handlerWaiting = true handlerWaiting = true
val scope = CoroutineScope(Job() + Dispatchers.Default) val scope = CoroutineScope(Job() + Dispatchers.Main)
scope.launch { scope.launch {
delay(100) delay(100)
live.refresh() live.refresh()
@@ -7,4 +7,9 @@ data class Channel (
val id: String = UUID.randomUUID().toString().substring(0,4) val id: String = UUID.randomUUID().toString().substring(0,4)
) { ) {
var filter: List<JsonFilter>? = null // Inactive when null var filter: List<JsonFilter>? = null // Inactive when null
private var lastEOSE: Long? = null
fun updateEOSE(l: Long) {
lastEOSE = l
}
} }
@@ -16,6 +16,7 @@ import com.vitorpamplona.amethyst.service.model.RepostEvent
import com.vitorpamplona.amethyst.service.relays.Client import com.vitorpamplona.amethyst.service.relays.Client
import com.vitorpamplona.amethyst.service.relays.Relay import com.vitorpamplona.amethyst.service.relays.Relay
import java.util.Collections import java.util.Collections
import java.util.Date
import java.util.UUID import java.util.UUID
import kotlin.time.ExperimentalTime import kotlin.time.ExperimentalTime
import kotlin.time.measureTimedValue import kotlin.time.measureTimedValue
@@ -90,7 +91,7 @@ abstract class NostrDataSource<T>(val debugName: String) {
//Log.e("ERROR", "Relay ${relay.url}: ${error.message}") //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) { //Log.d("RELAY", "Relay ${relay.url} ${when (type) {
// Relay.Type.CONNECT -> "connected." // Relay.Type.CONNECT -> "connected."
// Relay.Type.DISCONNECT -> "disconnected." // Relay.Type.DISCONNECT -> "disconnected."
@@ -98,11 +99,10 @@ abstract class NostrDataSource<T>(val debugName: String) {
// Relay.Type.EOSE -> "sent all events it had stored." // Relay.Type.EOSE -> "sent all events it had stored."
//}}") //}}")
/* if (type == Relay.Type.EOSE && channel != null) {
if (type == Relay.Type.EOSE) { // updates a per subscripton since date
// One everything is loaded, if new users are found, update filters channels.filter { it.id == channel }.firstOrNull()?.updateEOSE(Date().time / 1000)
resetFilters() }
}*/
} }
override fun onSendResponse(eventId: String, success: Boolean, message: String, relay: Relay) { override fun onSendResponse(eventId: String, success: Boolean, message: String, relay: Relay) {
@@ -14,7 +14,7 @@ object NostrSingleUserDataSource: NostrDataSource<User>("SingleUserFeed") {
fun createUserFilter(): List<JsonFilter>? { fun createUserFilter(): List<JsonFilter>? {
if (usersToWatch.isEmpty()) return null if (usersToWatch.isEmpty()) return null
return usersToWatch.map { return usersToWatch.filter { LocalCache.getOrCreateUser(it).latestMetadata == null }.map {
JsonFilter( JsonFilter(
kinds = listOf(MetadataEvent.kind), kinds = listOf(MetadataEvent.kind),
authors = listOf(it), authors = listOf(it),
@@ -72,8 +72,8 @@ object Client: RelayPool.Listener {
listeners.forEach { it.onError(error, subscriptionId, relay) } listeners.forEach { it.onError(error, subscriptionId, relay) }
} }
override fun onRelayStateChange(type: Relay.Type, relay: Relay) { override fun onRelayStateChange(type: Relay.Type, relay: Relay, channel: String?) {
listeners.forEach { it.onRelayStateChange(type, relay) } listeners.forEach { it.onRelayStateChange(type, relay, channel) }
} }
override fun onSendResponse(eventId: String, success: Boolean, message: String, relay: Relay) { 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 * 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. * When an relay saves or rejects a new event.
@@ -2,11 +2,8 @@ package com.vitorpamplona.amethyst.service.relays
import android.util.Log import android.util.Log
import com.google.gson.JsonElement import com.google.gson.JsonElement
import com.vitorpamplona.amethyst.model.LocalCache
import java.util.Date import java.util.Date
import nostr.postr.events.ContactListEvent
import nostr.postr.events.Event import nostr.postr.events.Event
import nostr.postr.toNpub
import okhttp3.OkHttpClient import okhttp3.OkHttpClient
import okhttp3.Request import okhttp3.Request
import okhttp3.Response import okhttp3.Response
@@ -50,7 +47,7 @@ class Relay(
Client.allSubscriptions().forEach { Client.allSubscriptions().forEach {
sendFilter(requestId = it) 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) { override fun onMessage(webSocket: WebSocket, text: String) {
@@ -65,7 +62,7 @@ class Relay(
listeners.forEach { it.onEvent(this@Relay, channel, event) } listeners.forEach { it.onEvent(this@Relay, channel, event) }
} }
"EOSE" -> listeners.forEach { "EOSE" -> listeners.forEach {
it.onRelayStateChange(this@Relay, Type.EOSE) it.onRelayStateChange(this@Relay, Type.EOSE, channel)
} }
"NOTICE" -> listeners.forEach { "NOTICE" -> listeners.forEach {
// "channel" being the second string in the string array ... // "channel" being the second string in the string array ...
@@ -91,13 +88,17 @@ class Relay(
} }
override fun onClosing(webSocket: WebSocket, code: Int, reason: String) { 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) { override fun onClosed(webSocket: WebSocket, code: Int, reason: String) {
socket = null socket = null
closingTime = Date().time / 1000 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?) { 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 * @param type is 0 for disconnect and 1 for connect
*/ */
fun onRelayStateChange(relay: Relay, type: Type) fun onRelayStateChange(relay: Relay, type: Type, channel: String?)
} }
} }
@@ -2,7 +2,6 @@ package com.vitorpamplona.amethyst.service.relays
import androidx.lifecycle.LiveData import androidx.lifecycle.LiveData
import com.vitorpamplona.amethyst.service.Constants import com.vitorpamplona.amethyst.service.Constants
import java.util.Collections
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job import kotlinx.coroutines.Job
@@ -88,7 +87,7 @@ object RelayPool: Relay.Listener {
fun onError(error: Error, subscriptionId: String, relay: Relay) 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) fun onSendResponse(eventId: String, success: Boolean, message: String, relay: Relay)
} }
@@ -103,8 +102,8 @@ object RelayPool: Relay.Listener {
refreshObservers() refreshObservers()
} }
override fun onRelayStateChange(relay: Relay, type: Relay.Type) { override fun onRelayStateChange(relay: Relay, type: Relay.Type, channel: String?) {
listeners.forEach { it.onRelayStateChange(type, relay) } listeners.forEach { it.onRelayStateChange(type, relay, channel) }
refreshObservers() refreshObservers()
} }
@@ -24,7 +24,8 @@ fun ClickableRoute(
) { ) {
if (nip19.type == Nip19.Type.USER) { if (nip19.type == Nip19.Type.USER) {
val userBase = LocalCache.getOrCreateUser(nip19.hex) val userBase = LocalCache.getOrCreateUser(nip19.hex)
val userState by userBase.live.observeAsState()
val userState by userBase.liveMetadata.observeAsState()
val user = userState?.user ?: return val user = userState?.user ?: return
val route = "User/${nip19.hex}" val route = "User/${nip19.hex}"
@@ -15,7 +15,7 @@ fun ClickableUserTag(
user: User, user: User,
navController: NavController navController: NavController
) { ) {
val innerUserState by user.live.observeAsState() val innerUserState by user.liveMetadata.observeAsState()
ClickableText( ClickableText(
text = AnnotatedString("@${innerUserState?.user?.toBestDisplayName()} "), text = AnnotatedString("@${innerUserState?.user?.toBestDisplayName()} "),
onClick = { navController.navigate("User/${innerUserState?.user?.pubkeyHex}") }, onClick = { navController.navigate("User/${innerUserState?.user?.pubkeyHex}") },
@@ -77,7 +77,7 @@ fun MainTopBar(scaffoldState: ScaffoldState, accountViewModel: AccountViewModel)
val accountState by accountViewModel.accountLiveData.observeAsState() val accountState by accountViewModel.accountLiveData.observeAsState()
val account = accountState?.account ?: return val account = accountState?.account ?: return
val accountUserState by account.userProfile().live.observeAsState() val accountUserState by account.userProfile().liveMetadata.observeAsState()
val accountUser = accountUserState?.user val accountUser = accountUserState?.user
val relayViewModel: RelayPoolViewModel = viewModel { RelayPoolViewModel() } val relayViewModel: RelayPoolViewModel = viewModel { RelayPoolViewModel() }
@@ -81,52 +81,26 @@ fun DrawerContent(navController: NavHostController,
val accountState by accountViewModel.accountLiveData.observeAsState() val accountState by accountViewModel.accountLiveData.observeAsState()
val account = accountState?.account ?: return val account = accountState?.account ?: return
val accountUserState by account.userProfile().live.observeAsState()
val accountUser = accountUserState?.user ?: return
Surface( Surface(
modifier = Modifier.fillMaxWidth(), modifier = Modifier.fillMaxWidth(),
color = MaterialTheme.colors.background color = MaterialTheme.colors.background
) { ) {
Column() { Column() {
Box { ProfileContent(
val banner = accountUser?.info?.banner account.userProfile(),
if (banner != null && banner.isNotBlank()) { modifier = Modifier
AsyncImage( .fillMaxWidth()
model = banner, .padding(horizontal = 25.dp)
contentDescription = "Profile Image", .padding(top = 100.dp),
contentScale = ContentScale.FillWidth, scaffoldState,
modifier = Modifier navController
.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
)
}
Divider( Divider(
thickness = 0.25.dp, thickness = 0.25.dp,
modifier = Modifier.padding(top = 20.dp) modifier = Modifier.padding(top = 20.dp)
) )
ListContent( ListContent(
accountUser, account.userProfile(),
navController, navController,
scaffoldState, scaffoldState,
modifier = Modifier modifier = Modifier
@@ -135,50 +109,79 @@ fun DrawerContent(navController: NavHostController,
accountStateViewModel accountStateViewModel
) )
BottomContent(accountUser, scaffoldState, navController) BottomContent(account.userProfile(), scaffoldState, navController)
} }
} }
} }
@Composable @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() val coroutineScope = rememberCoroutineScope()
Column(modifier = modifier) { val accountUserState by baseAccountUser.liveMetadata.observeAsState()
AsyncImage( val accountUser = accountUserState?.user ?: return
model = accountUser?.profilePicture() ?: "https://robohash.org/ohno.png",
contentDescription = "Profile Image", val accountUserFollowsState by baseAccountUser.liveFollows.observeAsState()
placeholder = rememberAsyncImagePainter("https://robohash.org/${accountUser?.pubkeyHex}.png"), val accountUserFollows = accountUserFollowsState?.user ?: return
modifier = Modifier
.width(100.dp) Box {
.height(100.dp) val banner = accountUser.info.banner
.clip(shape = CircleShape) if (banner != null && banner.isNotBlank()) {
.border(3.dp, MaterialTheme.colors.background, CircleShape) AsyncImage(
.background(MaterialTheme.colors.background) model = banner,
.clickable(onClick = { contentDescription = "Profile Image",
accountUser?.let { contentScale = ContentScale.FillWidth,
navController.navigate("User/${it.pubkeyHex}") modifier = Modifier
} .fillMaxWidth()
coroutineScope.launch { .height(150.dp)
scaffoldState.drawerState.close() )
} } else {
}) Image(
) painter = painterResource(R.drawable.profile_banner),
Text( contentDescription = "Profile Banner",
accountUser?.bestDisplayName() ?: "", contentScale = ContentScale.FillWidth,
modifier = Modifier.padding(top = 7.dp), modifier = Modifier
fontWeight = FontWeight.Bold, .fillMaxWidth()
fontSize = 18.sp .height(150.dp)
) )
Text(" @${accountUser?.bestUsername()}", color = Color.LightGray) }
Row(modifier = Modifier.padding(top = 15.dp)) {
Row() { Column(modifier = modifier) {
Text("${accountUser?.follows?.size ?: "--"}", fontWeight = FontWeight.Bold) AsyncImage(
Text(" Following") model = accountUser.profilePicture(),
} contentDescription = "Profile Image",
Row(modifier = Modifier.padding(start = 10.dp)) { placeholder = rememberAsyncImagePainter("https://robohash.org/${accountUser.pubkeyHex}.png"),
Text("${accountUser?.followers?.size ?: "--"}", fontWeight = FontWeight.Bold) modifier = Modifier
Text(" Followers") .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")
}
} }
} }
} }
@@ -53,9 +53,6 @@ fun ChatroomCompose(baseNote: Note, accountViewModel: AccountViewModel, navContr
val accountState by accountViewModel.accountLiveData.observeAsState() val accountState by accountViewModel.accountLiveData.observeAsState()
val account = accountState?.account ?: return val account = accountState?.account ?: return
val accountUserState by account.userProfile().live.observeAsState()
val accountUser = accountUserState?.user ?: return
val notificationCacheState = NotificationCache.live.observeAsState() val notificationCacheState = NotificationCache.live.observeAsState()
val notificationCache = notificationCacheState.value ?: return val notificationCache = notificationCacheState.value ?: return
@@ -64,7 +61,7 @@ fun ChatroomCompose(baseNote: Note, accountViewModel: AccountViewModel, navContr
if (note?.event == null) { if (note?.event == null) {
BlankNote(Modifier) BlankNote(Modifier)
} else if (note.channel != null) { } else if (note.channel != null) {
val authorState by note.author!!.live.observeAsState() val authorState by note.author!!.liveMetadata.observeAsState()
val author = authorState?.user val author = authorState?.user
val channelState by note.channel!!.live.observeAsState() val channelState by note.channel!!.live.observeAsState()
@@ -108,25 +105,19 @@ fun ChatroomCompose(baseNote: Note, accountViewModel: AccountViewModel, navContr
} }
} else { } else {
val authorState by note.author!!.live.observeAsState()
val author = authorState?.user
val replyAuthorBase = note.mentions?.first() val replyAuthorBase = note.mentions?.first()
var userToComposeOn = author var userToComposeOn = note.author!!
if ( replyAuthorBase != null ) { if (replyAuthorBase != null) {
val replyAuthorState by replyAuthorBase.live.observeAsState() if (note.author == account.userProfile()) {
val replyAuthor = replyAuthorState?.user userToComposeOn = replyAuthorBase
if (author == accountUser) {
userToComposeOn = replyAuthor
} }
} }
val noteEvent = note.event val noteEvent = note.event
userToComposeOn?.let { user -> userToComposeOn.let { user ->
val hasNewMessages = val hasNewMessages =
if (noteEvent != null) if (noteEvent != null)
noteEvent.createdAt > notificationCache.cache.load("Room/${userToComposeOn.pubkeyHex}", context) noteEvent.createdAt > notificationCache.cache.load("Room/${userToComposeOn.pubkeyHex}", context)
@@ -134,8 +125,8 @@ fun ChatroomCompose(baseNote: Note, accountViewModel: AccountViewModel, navContr
false false
ChannelName( ChannelName(
channelPicture = { UserPicture(user, accountUser, size = 55.dp) }, channelPicture = { UserPicture(userToComposeOn, account.userProfile(), size = 55.dp) },
channelTitle = { UsernameDisplay(user, it) }, channelTitle = { UsernameDisplay(userToComposeOn, it) },
channelLastTime = noteEvent?.createdAt, channelLastTime = noteEvent?.createdAt,
channelLastContent = accountViewModel.decrypt(note), channelLastContent = accountViewModel.decrypt(note),
hasNewMessages = hasNewMessages, hasNewMessages = hasNewMessages,
@@ -59,8 +59,7 @@ fun ChatroomMessageCompose(baseNote: Note, routeForLastRead: String?, innerQuote
val accountState by accountViewModel.accountLiveData.observeAsState() val accountState by accountViewModel.accountLiveData.observeAsState()
val account = accountState?.account ?: return val account = accountState?.account ?: return
val accountUserState by account.userProfile().live.observeAsState() val accountUser = account.userProfile()
val accountUser = accountUserState?.user
var popupExpanded by remember { mutableStateOf(false) } var popupExpanded by remember { mutableStateOf(false) }
@@ -69,14 +68,11 @@ fun ChatroomMessageCompose(baseNote: Note, routeForLastRead: String?, innerQuote
if (note?.event == null) { if (note?.event == null) {
BlankNote(Modifier) BlankNote(Modifier)
} else { } else {
val authorState by note.author!!.live.observeAsState()
val author = authorState?.user
var backgroundBubbleColor: Color var backgroundBubbleColor: Color
var alignment: Arrangement.Horizontal var alignment: Arrangement.Horizontal
var shape: Shape var shape: Shape
if (author == accountUser) { if (note.author == accountUser) {
backgroundBubbleColor = MaterialTheme.colors.primary.copy(alpha = 0.32f) backgroundBubbleColor = MaterialTheme.colors.primary.copy(alpha = 0.32f)
alignment = Arrangement.End alignment = Arrangement.End
shape = ChatBubbleShapeMe 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), 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) { if (innerQuote || author != accountUser && note.event is ChannelMessageEvent) {
Row( Row(
verticalAlignment = Alignment.CenterVertically, verticalAlignment = Alignment.CenterVertically,
@@ -339,10 +339,7 @@ fun UserPicture(
pictureModifier: Modifier = Modifier, pictureModifier: Modifier = Modifier,
onClick: ((User) -> Unit)? = null onClick: ((User) -> Unit)? = null
) { ) {
val accountState by baseUserAccount.live.observeAsState() val userState by baseUser.liveMetadata.observeAsState()
val accountUser = accountState?.user ?: return
val userState by baseUser.live.observeAsState()
val user = userState?.user ?: return val user = userState?.user ?: return
Box( Box(
@@ -367,6 +364,9 @@ fun UserPicture(
) )
val accountState by baseUserAccount.liveFollows.observeAsState()
val accountUser = accountState?.user ?: return
if (accountUser.isFollowing(user) || user == accountUser) { if (accountUser.isFollowing(user) || user == accountUser) {
Box( Box(
Modifier Modifier
@@ -44,7 +44,7 @@ fun ReplyInformation(replyTo: MutableList<Note>?, mentions: List<User>?, prefix:
val mentionSet = mentions.toSet() val mentionSet = mentions.toSet()
mentionSet.toSet().forEachIndexed { idx, user -> mentionSet.toSet().forEachIndexed { idx, user ->
val innerUserState by user.live.observeAsState() val innerUserState by user.liveMetadata.observeAsState()
val innerUser = innerUserState?.user val innerUser = innerUserState?.user
innerUser?.let { myUser -> innerUser?.let { myUser ->
@@ -123,7 +123,7 @@ fun ReplyInformationChannel(replyTo: MutableList<Note>?,
val mentionSet = mentions.toSet() val mentionSet = mentions.toSet()
mentionSet.forEachIndexed { idx, user -> mentionSet.forEachIndexed { idx, user ->
val innerUserState by user.live.observeAsState() val innerUserState by user.liveMetadata.observeAsState()
val innerUser = innerUserState?.user val innerUser = innerUserState?.user
innerUser?.let { myUser -> innerUser?.let { myUser ->
@@ -34,14 +34,11 @@ fun UserCompose(baseUser: User, accountViewModel: AccountViewModel, navControlle
val accountState by accountViewModel.accountLiveData.observeAsState() val accountState by accountViewModel.accountLiveData.observeAsState()
val account = accountState?.account ?: return val account = accountState?.account ?: return
val userState by baseUser.live.observeAsState()
val user = userState?.user ?: return
val ctx = LocalContext.current.applicationContext val ctx = LocalContext.current.applicationContext
Column(modifier = Column(modifier =
Modifier.clickable( Modifier.clickable(
onClick = { navController.navigate("User/${user.pubkeyHex}") } onClick = { navController.navigate("User/${baseUser.pubkeyHex}") }
) )
) { ) {
Row( Row(
@@ -59,6 +56,9 @@ fun UserCompose(baseUser: User, accountViewModel: AccountViewModel, navControlle
UsernameDisplay(baseUser) UsernameDisplay(baseUser)
} }
val userState by baseUser.liveMetadata.observeAsState()
val user = userState?.user ?: return
Text( Text(
user.info.about ?: "", user.info.about ?: "",
color = MaterialTheme.colors.onSurface.copy(alpha = 0.32f), 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)) { Column(modifier = Modifier.padding(start = 10.dp)) {
if (account?.isHidden(user) == false) { if (account.isHidden(baseUser)) {
ShowUserButton { ShowUserButton {
account.showUser(user.pubkeyHex) account.showUser(baseUser.pubkeyHex)
LocalPreferences(ctx).saveToEncryptedStorage(account) LocalPreferences(ctx).saveToEncryptedStorage(account)
} }
} else if (account?.userProfile()?.isFollowing(user) == true) { } else if (account.userProfile().isFollowing(baseUser)) {
UnfollowButton { account.unfollow(user) } UnfollowButton { account.unfollow(baseUser) }
} else { } else {
FollowButton { account?.follow(user) } FollowButton { account.follow(baseUser) }
} }
} }
} }
@@ -26,7 +26,7 @@ fun NoteUsernameDisplay(baseNote: Note, weight: Modifier = Modifier) {
@Composable @Composable
fun UsernameDisplay(baseUser: User, weight: Modifier = Modifier) { fun UsernameDisplay(baseUser: User, weight: Modifier = Modifier) {
val userState by baseUser.live.observeAsState() val userState by baseUser.liveMetadata.observeAsState()
val user = userState?.user ?: return val user = userState?.user ?: return
if (user.bestUsername() != null || user.bestDisplayName() != null) { if (user.bestUsername() != null || user.bestDisplayName() != null) {
@@ -172,9 +172,6 @@ fun NoteMaster(baseNote: Note, accountViewModel: AccountViewModel, navController
} else if (!account.isAcceptable(noteForReports)) { } else if (!account.isAcceptable(noteForReports)) {
HiddenNote() HiddenNote()
} else { } else {
val authorState by note.author!!.live.observeAsState()
val author = authorState?.user
Column( Column(
Modifier Modifier
.fillMaxWidth() .fillMaxWidth()
@@ -182,7 +179,7 @@ fun NoteMaster(baseNote: Note, accountViewModel: AccountViewModel, navController
Row(modifier = Modifier Row(modifier = Modifier
.padding(start = 12.dp, end = 12.dp) .padding(start = 12.dp, end = 12.dp)
.clickable(onClick = { .clickable(onClick = {
author?.let { note.author?.let {
navController.navigate("User/${it.pubkeyHex}") navController.navigate("User/${it.pubkeyHex}")
} }
}) })
@@ -41,6 +41,7 @@ import coil.compose.rememberAsyncImagePainter
import com.vitorpamplona.amethyst.model.User import com.vitorpamplona.amethyst.model.User
import com.vitorpamplona.amethyst.service.NostrChatRoomDataSource import com.vitorpamplona.amethyst.service.NostrChatRoomDataSource
import com.vitorpamplona.amethyst.ui.actions.PostButton 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.note.UsernameDisplay
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
@@ -70,13 +71,18 @@ fun ChatroomScreen(userId: String?, accountViewModel: AccountViewModel, navContr
} }
Column( 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}") ChatroomFeedView(feedViewModel, accountViewModel, navController, "Room/${userId}")
} }
//LAST ROW //LAST ROW
Row(modifier = Modifier.padding(10.dp).fillMaxWidth(), Row(modifier = Modifier
.padding(10.dp)
.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceBetween, horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically verticalAlignment = Alignment.CenterVertically
) { ) {
@@ -118,22 +124,23 @@ fun ChatroomScreen(userId: String?, accountViewModel: AccountViewModel, navContr
@Composable @Composable
fun ChatroomHeader(baseUser: User, accountViewModel: AccountViewModel, navController: NavController) { fun ChatroomHeader(baseUser: User, accountViewModel: AccountViewModel, navController: NavController) {
val authorState by baseUser.live.observeAsState()
val author = authorState?.user
Column(modifier = Modifier.clickable( Column(modifier = Modifier.clickable(
onClick = { navController.navigate("User/${author?.pubkeyHex}") } onClick = { navController.navigate("User/${baseUser.pubkeyHex}") }
) )
) { ) {
Column(modifier = Modifier.padding(12.dp)) { Column(modifier = Modifier.padding(12.dp)) {
Row(verticalAlignment = Alignment.CenterVertically) { Row(verticalAlignment = Alignment.CenterVertically) {
val authorState by baseUser.liveMetadata.observeAsState()
val author = authorState?.user
AsyncImage( AsyncImage(
model = author?.profilePicture(), model = author?.profilePicture(),
placeholder = rememberAsyncImagePainter("https://robohash.org/${author?.pubkeyHex}.png"), placeholder = rememberAsyncImagePainter("https://robohash.org/${author?.pubkeyHex}.png"),
contentDescription = "Profile Image", contentDescription = "Profile Image",
modifier = Modifier modifier = Modifier
.width(35.dp).height(35.dp) .width(35.dp)
.height(35.dp)
.clip(shape = CircleShape) .clip(shape = CircleShape)
) )
@@ -94,86 +94,92 @@ fun ProfileScreen(userId: String?, accountViewModel: AccountViewModel, navContro
val accountState by accountViewModel.accountLiveData.observeAsState() val accountState by accountViewModel.accountLiveData.observeAsState()
val account = accountState?.account ?: return val account = accountState?.account ?: return
val accountUserState by account.userProfile().live.observeAsState() if (userId == null) return
val accountUser = accountUserState?.user
if (userId != null && accountUser != null) { DisposableEffect(account) {
DisposableEffect(account) { NostrUserProfileDataSource.loadUserProfile(userId)
NostrUserProfileDataSource.loadUserProfile(userId) NostrUserProfileFollowersDataSource.loadUserProfile(userId)
NostrUserProfileFollowersDataSource.loadUserProfile(userId) NostrUserProfileFollowsDataSource.loadUserProfile(userId)
NostrUserProfileFollowsDataSource.loadUserProfile(userId)
onDispose { onDispose {
NostrUserProfileDataSource.stop() NostrUserProfileDataSource.stop()
NostrUserProfileFollowsDataSource.stop() NostrUserProfileFollowsDataSource.stop()
NostrUserProfileFollowersDataSource.stop() NostrUserProfileFollowersDataSource.stop()
}
} }
}
val baseUser = NostrUserProfileDataSource.user ?: return val baseUser = NostrUserProfileDataSource.user ?: return
val userState by baseUser.live.observeAsState() Surface(
val user = userState?.user ?: return modifier = Modifier.fillMaxWidth(),
color = MaterialTheme.colors.background
) {
Column() {
ProfileHeader(baseUser, navController, account, accountViewModel)
Surface( val pagerState = rememberPagerState()
modifier = Modifier.fillMaxWidth(), val coroutineScope = rememberCoroutineScope()
color = MaterialTheme.colors.background
) {
Column() {
ProfileHeader(user, navController, account, accountUser, accountViewModel)
val pagerState = rememberPagerState() Column(modifier = Modifier.padding()) {
val coroutineScope = rememberCoroutineScope() TabRow(
selectedTabIndex = pagerState.currentPage,
Column(modifier = Modifier.padding()) { indicator = { tabPositions ->
TabRow( TabRowDefaults.Indicator(
selectedTabIndex = pagerState.currentPage, Modifier.pagerTabIndicatorOffset(pagerState, tabPositions),
indicator = { tabPositions -> color = MaterialTheme.colors.primary
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")
}
) )
},
Tab( ) {
selected = pagerState.currentPage == 1, Tab(
onClick = { coroutineScope.launch { pagerState.animateScrollToPage(1) } }, selected = pagerState.currentPage == 0,
text = { onClick = { coroutineScope.launch { pagerState.animateScrollToPage(0) } },
Text(text = "${user.follows?.size ?: "--"} Follows") text = {
} Text(text = "Notes")
)
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 == 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 @Composable
private fun ProfileHeader( private fun ProfileHeader(
user: User, baseUser: User,
navController: NavController, navController: NavController,
account: Account, account: Account,
accountUser: User,
accountViewModel: AccountViewModel accountViewModel: AccountViewModel
) { ) {
val ctx = LocalContext.current.applicationContext val ctx = LocalContext.current.applicationContext
var popupExpanded by remember { mutableStateOf(false) } var popupExpanded by remember { mutableStateOf(false) }
val accountUserState by account.userProfile().liveFollows.observeAsState()
val accountUser = accountUserState?.user ?: return
Box { Box {
val banner = user.info.banner DrawBanner(baseUser)
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)
)
}
Box(modifier = Modifier Box(modifier = Modifier
.padding(horizontal = 10.dp) .padding(horizontal = 10.dp)
@@ -237,7 +226,7 @@ private fun ProfileHeader(
contentDescription = "More Options", contentDescription = "More Options",
) )
UserProfileDropDownMenu(user, popupExpanded, { popupExpanded = false }, accountViewModel) UserProfileDropDownMenu(baseUser, popupExpanded, { popupExpanded = false }, accountViewModel)
} }
} }
@@ -255,7 +244,7 @@ private fun ProfileHeader(
) { ) {
UserPicture( UserPicture(
user, navController, account.userProfile(), 100.dp, baseUser, navController, account.userProfile(), 100.dp,
pictureModifier = Modifier.border( pictureModifier = Modifier.border(
3.dp, 3.dp,
MaterialTheme.colors.background, MaterialTheme.colors.background,
@@ -268,52 +257,88 @@ private fun ProfileHeader(
Row(modifier = Modifier Row(modifier = Modifier
.height(35.dp) .height(35.dp)
.padding(bottom = 3.dp)) { .padding(bottom = 3.dp)) {
MessageButton(user, navController) MessageButton(baseUser, navController)
if (accountUser == user && account.isWriteable()) { if (accountUser == baseUser && account.isWriteable()) {
NSecCopyButton(account) NSecCopyButton(account)
} }
NPubCopyButton(user) NPubCopyButton(baseUser)
if (accountUser == user) { if (accountUser == baseUser) {
EditButton(account) EditButton(account)
} else { } else {
if (!account.isHidden(user)) { if (account.isHidden(baseUser)) {
ShowUserButton { ShowUserButton {
account.showUser(user.pubkeyHex) account.showUser(baseUser.pubkeyHex)
LocalPreferences(ctx).saveToEncryptedStorage(account) LocalPreferences(ctx).saveToEncryptedStorage(account)
} }
} else if (accountUser.isFollowing(user)) { } else if (accountUser.isFollowing(baseUser)) {
UnfollowButton { account.unfollow(user) } UnfollowButton { account.unfollow(baseUser) }
} else { } else {
FollowButton { account.follow(user) } FollowButton { account.follow(baseUser) }
} }
} }
} }
} }
Text( DrawAdditionalInfo(baseUser)
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)
)
Divider(modifier = Modifier.padding(top = 6.dp)) 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 @Composable
fun TabNotes(user: User, accountViewModel: AccountViewModel, navController: NavController) { fun TabNotes(user: User, accountViewModel: AccountViewModel, navController: NavController) {
val accountState by accountViewModel.accountLiveData.observeAsState() val accountState by accountViewModel.accountLiveData.observeAsState()
@@ -560,7 +585,7 @@ fun UserProfileDropDownMenu(user: User, popupExpanded: Boolean, onDismiss: () ->
if ( account.userProfile() != user) { if ( account.userProfile() != user) {
Divider() Divider()
if (!account.isHidden(user)) { if (account.isHidden(user)) {
DropdownMenuItem(onClick = { DropdownMenuItem(onClick = {
user.let { user.let {
accountViewModel.show( accountViewModel.show(
@@ -210,9 +210,6 @@ fun UserLine(
account: Account, account: Account,
onClick: () -> Unit onClick: () -> Unit
) { ) {
val userState by baseUser.live.observeAsState()
val user = userState?.user ?: return
Column(modifier = Modifier Column(modifier = Modifier
.fillMaxWidth() .fillMaxWidth()
.clickable(onClick = onClick) .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( Column(
modifier = Modifier modifier = Modifier
@@ -237,6 +234,9 @@ fun UserLine(
UsernameDisplay(baseUser) UsernameDisplay(baseUser)
} }
val userState by baseUser.liveMetadata.observeAsState()
val user = userState?.user ?: return
Text( Text(
user.info.about?.take(100) ?: "", user.info.about?.take(100) ?: "",
color = MaterialTheme.colors.onSurface.copy(alpha = 0.32f), color = MaterialTheme.colors.onSurface.copy(alpha = 0.32f),