Support for User Profiles

pull/3/head
Vitor Pamplona 2023-01-15 21:52:59 -05:00
rodzic 5791511bd6
commit 4543a68615
40 zmienionych plików z 891 dodań i 106 usunięć

Wyświetl plik

@ -99,6 +99,10 @@ dependencies {
// view videos
implementation 'com.google.android.exoplayer:exoplayer:2.18.2'
// tabs for user profiles
implementation "com.google.accompanist:accompanist-pager:$accompanist_version" // Pager
implementation "com.google.accompanist:accompanist-pager-indicators:$accompanist_version" // Pager Indicators
testImplementation 'junit:junit:4.13.2'
androidTestImplementation 'androidx.test.ext:junit:1.1.5'
androidTestImplementation 'androidx.test.espresso:espresso-core:3.5.1'

Wyświetl plik

@ -1,11 +1,14 @@
package com.vitorpamplona.amethyst.model
import androidx.lifecycle.LiveData
import com.vitorpamplona.amethyst.service.Constants
import com.vitorpamplona.amethyst.service.model.ReactionEvent
import com.vitorpamplona.amethyst.service.model.RepostEvent
import com.vitorpamplona.amethyst.service.relays.Client
import nostr.postr.Contact
import nostr.postr.Persona
import nostr.postr.Utils
import nostr.postr.events.ContactListEvent
import nostr.postr.events.PrivateDmEvent
import nostr.postr.events.TextNoteEvent
import nostr.postr.toHex
@ -52,6 +55,32 @@ class Account(val loggedIn: Persona) {
}
}
fun follow(user: User) {
if (!isWriteable()) return
val lastestContactList = userProfile().lastestContactList
val event = if (lastestContactList != null) {
ContactListEvent.create(lastestContactList.follows.plus(Contact(user.pubkeyHex, null)), lastestContactList.relayUse, loggedIn.privKey!!)
} else {
val relays = Constants.defaultRelays.associate { it.url to ContactListEvent.ReadWrite(it.read, it.write) }
ContactListEvent.create(listOf(Contact(user.pubkeyHex, null)), relays, loggedIn.privKey!!)
}
Client.send(event)
LocalCache.consume(event)
}
fun unfollow(user: User) {
if (!isWriteable()) return
val lastestContactList = userProfile().lastestContactList
if (lastestContactList != null) {
val event = ContactListEvent.create(lastestContactList.follows.filter { it.pubKeyHex != user.pubkeyHex }, lastestContactList.relayUse, loggedIn.privKey!!)
Client.send(event)
LocalCache.consume(event)
}
}
fun sendPost(message: String, replyingTo: Note?) {
if (!isWriteable()) return
@ -99,12 +128,15 @@ class Account(val loggedIn: Persona) {
val event = note.event
return if (event is PrivateDmEvent && loggedIn.privKey != null) {
var pubkeyToUse = event.pubKey
if (note.author == userProfile())
pubkeyToUse = event.recipientPubKey!!
val sharedSecret = Utils.getSharedSecret(loggedIn.privKey!!, pubkeyToUse)
val recepientPK = event.recipientPubKey
if (note.author == userProfile() && recepientPK != null)
pubkeyToUse = recepientPK
return try {
val sharedSecret = Utils.getSharedSecret(loggedIn.privKey!!, pubkeyToUse)
val retVal = Utils.decrypt(event.content, sharedSecret)
if (retVal.startsWith(PrivateDmEvent.nip18Advertisement)) {

Wyświetl plik

@ -116,7 +116,7 @@ object LocalCache {
val user = getOrCreateUser(event.pubKey)
if (event.createdAt > user.updatedFollowsAt) {
//Log.d("CL", "${user.toBestDisplayName()} ${event.follows.size}")
Log.d("CL", "AAA ${user.toBestDisplayName()} ${event.follows.size}")
user.updateFollows(
event.follows.map {
try {
@ -131,6 +131,8 @@ object LocalCache {
}.filterNotNull(),
event.createdAt
)
user.lastestContactList = event
}
refreshObservers()

Wyświetl plik

@ -5,6 +5,7 @@ import com.vitorpamplona.amethyst.service.NostrSingleUserDataSource
import com.vitorpamplona.amethyst.ui.note.toDisplayHex
import java.util.Collections
import java.util.concurrent.ConcurrentHashMap
import nostr.postr.events.ContactListEvent
class User(val pubkey: ByteArray) {
val pubkeyHex = pubkey.toHexKey()
@ -15,6 +16,8 @@ class User(val pubkey: ByteArray) {
var updatedMetadataAt: Long = 0;
var updatedFollowsAt: Long = 0;
var lastestContactList: ContactListEvent? = null
val notes = Collections.synchronizedSet(mutableSetOf<Note>())
val follows = Collections.synchronizedSet(mutableSetOf<User>())
val taggedPosts = Collections.synchronizedSet(mutableSetOf<Note>())

Wyświetl plik

@ -10,7 +10,7 @@ import nostr.postr.events.ContactListEvent
import nostr.postr.events.MetadataEvent
import nostr.postr.events.TextNoteEvent
object NostrAccountDataSource: NostrDataSource("AccountData") {
object NostrAccountDataSource: NostrDataSource<Note>("AccountData") {
lateinit var account: Account
private val cacheListener: (UserState) -> Unit = {

Wyświetl plik

@ -7,7 +7,7 @@ import com.vitorpamplona.amethyst.model.User
import nostr.postr.JsonFilter
import nostr.postr.events.PrivateDmEvent
object NostrChatRoomDataSource: NostrDataSource("ChatroomFeed") {
object NostrChatRoomDataSource: NostrDataSource<Note>("ChatroomFeed") {
lateinit var account: Account
var withUser: User? = null

Wyświetl plik

@ -5,7 +5,7 @@ import com.vitorpamplona.amethyst.model.Note
import nostr.postr.JsonFilter
import nostr.postr.events.PrivateDmEvent
object NostrChatroomListDataSource: NostrDataSource("MailBoxFeed") {
object NostrChatroomListDataSource: NostrDataSource<Note>("MailBoxFeed") {
lateinit var account: Account
fun createMessagesToMeFilter() = JsonFilter(

Wyświetl plik

@ -1,7 +1,6 @@
package com.vitorpamplona.amethyst.service
import com.vitorpamplona.amethyst.model.LocalCache
import com.vitorpamplona.amethyst.model.Note
import com.vitorpamplona.amethyst.service.model.ReactionEvent
import com.vitorpamplona.amethyst.service.model.RepostEvent
import com.vitorpamplona.amethyst.service.relays.Client
@ -15,7 +14,7 @@ import nostr.postr.events.PrivateDmEvent
import nostr.postr.events.RecommendRelayEvent
import nostr.postr.events.TextNoteEvent
abstract class NostrDataSource(val debugName: String) {
abstract class NostrDataSource<T>(val debugName: String) {
private val channels = Collections.synchronizedSet(mutableSetOf<Channel>())
private val channelIds = Collections.synchronizedSet(mutableSetOf<String>())
@ -78,7 +77,7 @@ abstract class NostrDataSource(val debugName: String) {
}
}
fun loadTop(): List<Note> {
fun loadTop(): List<T> {
return feed().take(100)
}
@ -135,5 +134,5 @@ abstract class NostrDataSource(val debugName: String) {
}
abstract fun updateChannelFilters()
abstract fun feed(): List<Note>
abstract fun feed(): List<T>
}

Wyświetl plik

@ -1,10 +1,11 @@
package com.vitorpamplona.amethyst.service
import com.vitorpamplona.amethyst.model.LocalCache
import com.vitorpamplona.amethyst.model.Note
import nostr.postr.JsonFilter
import nostr.postr.events.TextNoteEvent
object NostrGlobalDataSource: NostrDataSource("GlobalFeed") {
object NostrGlobalDataSource: NostrDataSource<Note>("GlobalFeed") {
val fifteenMinutes = (60*15) // 15 mins
fun createGlobalFilter() = JsonFilter(

Wyświetl plik

@ -9,7 +9,7 @@ import nostr.postr.JsonFilter
import nostr.postr.events.TextNoteEvent
import nostr.postr.toHex
object NostrHomeDataSource: NostrDataSource("HomeFeed") {
object NostrHomeDataSource: NostrDataSource<Note>("HomeFeed") {
lateinit var account: Account
private val cacheListener: (UserState) -> Unit = {

Wyświetl plik

@ -4,7 +4,7 @@ import com.vitorpamplona.amethyst.model.Account
import com.vitorpamplona.amethyst.model.Note
import nostr.postr.JsonFilter
object NostrNotificationDataSource: NostrDataSource("GlobalFeed") {
object NostrNotificationDataSource: NostrDataSource<Note>("GlobalFeed") {
lateinit var account: Account
fun createGlobalFilter() = JsonFilter(

Wyświetl plik

@ -8,7 +8,7 @@ import java.util.Collections
import nostr.postr.JsonFilter
import nostr.postr.events.TextNoteEvent
object NostrSingleEventDataSource: NostrDataSource("SingleEventFeed") {
object NostrSingleEventDataSource: NostrDataSource<Note>("SingleEventFeed") {
val eventsToWatch = Collections.synchronizedList(mutableListOf<String>())
fun createRepliesAndReactionsFilter(): JsonFilter? {

Wyświetl plik

@ -6,7 +6,7 @@ import java.util.Collections
import nostr.postr.JsonFilter
import nostr.postr.events.MetadataEvent
object NostrSingleUserDataSource: NostrDataSource("SingleUserFeed") {
object NostrSingleUserDataSource: NostrDataSource<Note>("SingleUserFeed") {
val usersToWatch = Collections.synchronizedList(mutableListOf<String>())
fun createUserFilter(): JsonFilter? {

Wyświetl plik

@ -5,7 +5,7 @@ import com.vitorpamplona.amethyst.model.Note
import java.util.Collections
import nostr.postr.JsonFilter
object NostrThreadDataSource: NostrDataSource("SingleThreadFeed") {
object NostrThreadDataSource: NostrDataSource<Note>("SingleThreadFeed") {
val eventsToWatch = Collections.synchronizedList(mutableListOf<String>())
fun createRepliesAndReactionsFilter(): JsonFilter? {

Wyświetl plik

@ -0,0 +1,45 @@
package com.vitorpamplona.amethyst.service
import com.vitorpamplona.amethyst.model.LocalCache
import com.vitorpamplona.amethyst.model.Note
import com.vitorpamplona.amethyst.model.User
import nostr.postr.JsonFilter
import nostr.postr.events.MetadataEvent
import nostr.postr.events.TextNoteEvent
object NostrUserProfileDataSource: NostrDataSource<Note>("UserProfileFeed") {
var user: User? = null
fun loadUserProfile(userId: String) {
user = LocalCache.users[userId]
resetFilters()
}
fun createUserInfoFilter(): JsonFilter {
return JsonFilter(
kinds = listOf(MetadataEvent.kind),
authors = listOf(user!!.pubkeyHex),
since = System.currentTimeMillis() / 1000 - (60 * 60 * 24 * 7)
)
}
fun createUserPostsFilter(): JsonFilter {
return JsonFilter(
kinds = listOf(TextNoteEvent.kind),
authors = listOf(user!!.pubkeyHex),
since = System.currentTimeMillis() / 1000 - (60 * 60 * 24 * 4)
)
}
val userInfoChannel = requestNewChannel()
val notesChannel = requestNewChannel()
override fun feed(): List<Note> {
return user?.notes?.sortedBy { it.event?.createdAt }?.reversed() ?: emptyList()
}
override fun updateChannelFilters() {
userInfoChannel.filter = createUserInfoFilter()
notesChannel.filter = createUserPostsFilter()
}
}

Wyświetl plik

@ -0,0 +1,31 @@
package com.vitorpamplona.amethyst.service
import com.vitorpamplona.amethyst.model.LocalCache
import com.vitorpamplona.amethyst.model.User
import nostr.postr.JsonFilter
import nostr.postr.events.ContactListEvent
object NostrUserProfileFollowersDataSource: NostrDataSource<User>("UserProfileFollowerFeed") {
var user: User? = null
fun loadUserProfile(userId: String) {
user = LocalCache.users[userId]
resetFilters()
}
fun createFollowersFilter() = JsonFilter(
kinds = listOf(ContactListEvent.kind),
since = System.currentTimeMillis() / 1000 - (60 * 60 * 24 * 7), // 7 days
tags = mapOf("p" to listOf(user!!.pubkeyHex).filterNotNull())
)
val followerChannel = requestNewChannel()
override fun feed(): List<User> {
return user?.followers?.toList() ?: emptyList()
}
override fun updateChannelFilters() {
followerChannel.filter = createFollowersFilter()
}
}

Wyświetl plik

@ -0,0 +1,33 @@
package com.vitorpamplona.amethyst.service
import com.vitorpamplona.amethyst.model.LocalCache
import com.vitorpamplona.amethyst.model.User
import nostr.postr.JsonFilter
import nostr.postr.events.ContactListEvent
object NostrUserProfileFollowsDataSource: NostrDataSource<User>("UserProfileFollowsFeed") {
var user: User? = null
fun loadUserProfile(userId: String) {
user = LocalCache.users[userId]
resetFilters()
}
fun createFollowFilter(): JsonFilter {
return JsonFilter(
kinds = listOf(ContactListEvent.kind),
authors = listOf(user!!.pubkeyHex),
since = System.currentTimeMillis() / 1000 - (60 * 60 * 24 * 7), // 4 days
)
}
val followChannel = requestNewChannel()
override fun feed(): List<User> {
return user?.follows?.toList() ?: emptyList()
}
override fun updateChannelFilters() {
followChannel.filter = createFollowFilter()
}
}

Wyświetl plik

@ -17,6 +17,9 @@ import com.vitorpamplona.amethyst.service.NostrNotificationDataSource
import com.vitorpamplona.amethyst.service.NostrSingleEventDataSource
import com.vitorpamplona.amethyst.service.NostrSingleUserDataSource
import com.vitorpamplona.amethyst.service.NostrThreadDataSource
import com.vitorpamplona.amethyst.service.NostrUserProfileDataSource
import com.vitorpamplona.amethyst.service.NostrUserProfileFollowersDataSource
import com.vitorpamplona.amethyst.service.NostrUserProfileFollowsDataSource
import com.vitorpamplona.amethyst.service.relays.Client
import com.vitorpamplona.amethyst.ui.screen.AccountScreen
import com.vitorpamplona.amethyst.ui.screen.AccountStateViewModel
@ -52,6 +55,9 @@ class MainActivity : ComponentActivity() {
NostrAccountDataSource.stop()
NostrHomeDataSource.stop()
NostrChatroomListDataSource.stop()
NostrUserProfileDataSource.stop()
NostrUserProfileFollowersDataSource.stop()
NostrUserProfileFollowsDataSource.stop()
NostrGlobalDataSource.stop()
NostrNotificationDataSource.stop()

Wyświetl plik

@ -10,7 +10,6 @@ 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.ExperimentalComposeUiApi
import androidx.compose.ui.Modifier
import androidx.compose.ui.text.AnnotatedString
import androidx.compose.ui.unit.dp

Wyświetl plik

@ -38,14 +38,13 @@ import kotlinx.coroutines.launch
@Composable
fun AppTopBar(navController: NavHostController, scaffoldState: ScaffoldState, accountViewModel: AccountViewModel) {
when (currentRoute(navController)) {
Route.Profile.route,
Route.Lists.route,
Route.Topics.route,
Route.Bookmarks.route,
Route.Moments.route -> TopBarWithBackButton(navController)
//Route.Profile.route -> TopBarWithBackButton(navController)
else -> MainTopBar(scaffoldState, accountViewModel)
}
}
@Composable

Wyświetl plik

@ -13,7 +13,6 @@ import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.material.Divider
import androidx.compose.material.Icon
@ -35,6 +34,7 @@ import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.font.FontWeight.Companion.W500
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.navigation.NavController
import androidx.navigation.NavHostController
import coil.compose.AsyncImage
import com.vitorpamplona.amethyst.R
@ -43,14 +43,6 @@ import com.vitorpamplona.amethyst.ui.screen.AccountStateViewModel
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
import kotlinx.coroutines.launch
val bottomNavigations = listOf(
Route.Profile,
Route.Lists,
//Route.Topics,
Route.Bookmarks,
//Route.Moments
)
@Composable
fun DrawerContent(navController: NavHostController,
scaffoldState: ScaffoldState,
@ -66,19 +58,31 @@ fun DrawerContent(navController: NavHostController,
) {
Column() {
Box {
Image(
painter = painterResource(R.drawable.profile_banner),
contentDescription = "Profile Banner",
contentScale = ContentScale.FillWidth,
modifier = Modifier.fillMaxWidth()
)
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 = 125.dp)
.padding(top = 100.dp),
scaffoldState,
navController
)
}
Divider(
@ -86,6 +90,7 @@ fun DrawerContent(navController: NavHostController,
modifier = Modifier.padding(top = 20.dp)
)
ListContent(
accountUser,
navController,
scaffoldState,
modifier = Modifier
@ -98,7 +103,9 @@ fun DrawerContent(navController: NavHostController,
}
@Composable
fun ProfileContent(accountUser: User?, modifier: Modifier = Modifier) {
fun ProfileContent(accountUser: User?, modifier: Modifier = Modifier, scaffoldState: ScaffoldState, navController: NavController) {
val coroutineScope = rememberCoroutineScope()
Column(modifier = modifier) {
AsyncImage(
model = accountUser?.profilePicture() ?: "https://robohash.org/ohno.png",
@ -108,6 +115,14 @@ fun ProfileContent(accountUser: User?, modifier: Modifier = Modifier) {
.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() ?: "",
@ -131,21 +146,25 @@ fun ProfileContent(accountUser: User?, modifier: Modifier = Modifier) {
@Composable
fun ListContent(
accountUser: User?,
navController: NavHostController,
scaffoldState: ScaffoldState,
modifier: Modifier,
accountViewModel: AccountStateViewModel
) {
Column(
modifier = modifier
) {
Column(modifier = modifier) {
LazyColumn() {
items(items = bottomNavigations) {
NavigationRow(navController, scaffoldState, it)
}
item {
if (accountUser != null)
NavigationRow(navController,
scaffoldState,
"User/${accountUser.pubkeyHex}",
Route.Profile.icon,
"Profile"
)
Divider(
modifier = Modifier.padding(vertical = 15.dp),
modifier = Modifier.padding(bottom = 15.dp),
thickness = 0.25.dp
)
Column(modifier = modifier.padding(horizontal = 25.dp)) {
@ -171,31 +190,36 @@ fun ListContent(
}
@Composable
fun NavigationRow(navController: NavHostController, scaffoldState: ScaffoldState, route: Route) {
fun NavigationRow(navController: NavHostController, scaffoldState: ScaffoldState, route: String, icon: Int, title: String) {
val coroutineScope = rememberCoroutineScope()
val currentRoute = currentRoute(navController)
Row(
modifier = Modifier
.padding(vertical = 15.dp, horizontal = 25.dp)
.clickable(onClick = {
if (currentRoute != route.route) {
navController.navigate(route.route)
}
coroutineScope.launch {
scaffoldState.drawerState.close()
}
}),
verticalAlignment = Alignment.CenterVertically
Row(modifier = Modifier
.fillMaxWidth()
.clickable(onClick = {
if (currentRoute != route) {
navController.navigate(route)
}
coroutineScope.launch {
scaffoldState.drawerState.close()
}
})
) {
Icon(
painter = painterResource(route.icon), null,
modifier = Modifier.size(22.dp),
tint = MaterialTheme.colors.primary
)
Text(
modifier = Modifier.padding(start = 16.dp),
text = route.route,
fontSize = 18.sp,
)
Row(
modifier = Modifier.fillMaxWidth()
.padding(vertical = 15.dp, horizontal = 25.dp),
verticalAlignment = Alignment.CenterVertically
) {
Icon(
painter = painterResource(icon), null,
modifier = Modifier.size(22.dp),
tint = MaterialTheme.colors.primary
)
Text(
modifier = Modifier.padding(start = 16.dp),
text = title,
fontSize = 18.sp,
)
}
}
}

Wyświetl plik

@ -30,11 +30,11 @@ sealed class Route(
object Search : Route("Search", R.drawable.ic_search, buildScreen = { acc, nav -> { _ -> SearchScreen(acc, nav) }})
object Notification : Route("Notification", R.drawable.ic_notifications,buildScreen = { acc, nav -> { _ -> NotificationScreen(acc, nav) }})
object Message : Route("Message", R.drawable.ic_dm, buildScreen = { acc, nav -> { _ -> ChatroomListScreen(acc, nav) }})
object Profile : Route("Profile", R.drawable.ic_profile, buildScreen = { acc, nav -> { _ -> ProfileScreen(acc) }})
object Lists : Route("Lists", R.drawable.ic_lists, buildScreen = { acc, nav -> { _ -> ProfileScreen(acc) }})
object Topics : Route("Topics", R.drawable.ic_topics, buildScreen = { acc, nav -> { _ -> ProfileScreen(acc) }})
object Bookmarks : Route("Bookmarks", R.drawable.ic_bookmarks, buildScreen = { acc, nav -> { _ -> ProfileScreen(acc) }})
object Moments : Route("Moments", R.drawable.ic_moments, buildScreen = { acc, nav -> { _ -> ProfileScreen(acc) }})
object Profile : Route("User/{id}", R.drawable.ic_profile,
arguments = listOf(navArgument("id") { type = NavType.StringType } ),
buildScreen = { acc, nav -> { ProfileScreen(it.arguments?.getString("id"), acc, nav) }}
)
object Note : Route("Note/{id}", R.drawable.ic_moments,
arguments = listOf(navArgument("id") { type = NavType.StringType } ),
@ -56,12 +56,6 @@ val Routes = listOf(
//drawer
Route.Profile,
Route.Lists,
Route.Topics,
Route.Bookmarks,
Route.Moments,
//inner
Route.Note,
Route.Room
)

Wyświetl plik

@ -1,5 +1,6 @@
package com.vitorpamplona.amethyst.ui.note
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
@ -71,6 +72,11 @@ fun BoostSetCompose(likeSetCard: BoostSetCard, isInnerNote: Boolean = false, acc
.width(35.dp)
.height(35.dp)
.clip(shape = CircleShape)
.clickable(onClick = {
userState?.let {
navController.navigate("User/${it.user.pubkeyHex}")
}
})
)
}
}

Wyświetl plik

@ -52,7 +52,7 @@ fun ChatroomCompose(baseNote: Note, accountViewModel: AccountViewModel, navContr
Column(modifier =
Modifier.clickable(
onClick = { navController.navigate("Room/${userToComposeOn?.pubkeyHex}") }
onClick = { navController.navigate("Room/${userToComposeOn?.pubkeyHex}") }
)
) {
Row(

Wyświetl plik

@ -1,5 +1,6 @@
package com.vitorpamplona.amethyst.ui.note
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
@ -71,6 +72,11 @@ fun LikeSetCompose(likeSetCard: LikeSetCard, modifier: Modifier = Modifier, isIn
.width(35.dp).height(35.dp)
.height(35.dp)
.clip(shape = CircleShape)
.clickable(onClick = {
userState?.let {
navController.navigate("User/${it.user.pubkeyHex}")
}
})
)
}
}

Wyświetl plik

@ -3,6 +3,7 @@ package com.vitorpamplona.amethyst.ui.note
import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.background
import androidx.compose.foundation.border
import androidx.compose.foundation.clickable
import androidx.compose.foundation.combinedClickable
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
@ -77,6 +78,11 @@ fun NoteCompose(baseNote: Note, modifier: Modifier = Modifier, isInnerNote: Bool
modifier = Modifier
.width(55.dp).height(55.dp)
.clip(shape = CircleShape)
.clickable(onClick = {
author?.let {
navController.navigate("User/${it.pubkeyHex}")
}
})
)
// boosted picture
@ -91,12 +97,23 @@ fun NoteCompose(baseNote: Note, modifier: Modifier = Modifier, isInnerNote: Bool
.align(Alignment.BottomEnd)
.background(MaterialTheme.colors.background)
.border(2.dp, MaterialTheme.colors.primary, CircleShape)
.clickable(onClick = {
boostedPosts[0].author?.let {
navController.navigate("User/${it.pubkeyHex}")
}
})
)
}
}
}
Column(modifier = Modifier.padding(start = if (!isInnerNote) 10.dp else 0.dp)) {
Column(modifier = Modifier.padding(start = if (!isInnerNote) 10.dp else 0.dp)
.clickable(onClick = {
note?.let {
navController.navigate("Note/${note.idHex}")
}
})
) {
Row(verticalAlignment = Alignment.CenterVertically) {
if (author != null)
UserDisplay(author)

Wyświetl plik

@ -0,0 +1,80 @@
package com.vitorpamplona.amethyst.ui.note
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.shape.CircleShape
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.draw.clip
import androidx.compose.ui.unit.dp
import androidx.navigation.NavController
import coil.compose.AsyncImage
import com.vitorpamplona.amethyst.model.User
import com.vitorpamplona.amethyst.ui.screen.FollowButton
import com.vitorpamplona.amethyst.ui.screen.UnfollowButton
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
@Composable
fun UserCompose(baseUser: User, accountViewModel: AccountViewModel, navController: NavController) {
val accountState by accountViewModel.accountLiveData.observeAsState()
val userState by baseUser.live.observeAsState()
val user = userState?.user ?: return
Column(modifier =
Modifier.clickable(
onClick = { navController.navigate("User/${user.pubkeyHex}") }
)
) {
Row(
modifier = Modifier
.padding(
start = 12.dp,
end = 12.dp,
top = 10.dp)
) {
AsyncImage(
model = user.profilePicture(),
contentDescription = "Profile Image",
modifier = Modifier
.width(55.dp).height(55.dp)
.clip(shape = CircleShape)
)
Column(modifier = Modifier.padding(start = 10.dp).weight(1f)) {
Row(verticalAlignment = Alignment.CenterVertically) {
UserDisplay(user)
}
Text(
user.info.about?.take(100) ?: "",
color = MaterialTheme.colors.onSurface.copy(alpha = 0.32f)
)
}
Column(modifier = Modifier.padding(start = 10.dp)) {
if (accountState?.account?.userProfile()?.follows?.contains(user) == true) {
UnfollowButton { accountState?.account?.unfollow(user) }
} else {
FollowButton { accountState?.account?.follow(user) }
}
}
}
Divider(
modifier = Modifier.padding(top = 10.dp),
thickness = 0.25.dp
)
}
}

Wyświetl plik

@ -14,7 +14,7 @@ import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch
class CardFeedViewModel(val dataSource: NostrDataSource): ViewModel() {
class CardFeedViewModel(val dataSource: NostrDataSource<Note>): ViewModel() {
private val _feedContent = MutableStateFlow<CardFeedState>(CardFeedState.Loading)
val feedContent = _feedContent.asStateFlow()

Wyświetl plik

@ -13,7 +13,7 @@ import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch
class FeedViewModel(val dataSource: NostrDataSource): ViewModel() {
class FeedViewModel(val dataSource: NostrDataSource<Note>): ViewModel() {
private val _feedContent = MutableStateFlow<FeedState>(FeedState.Loading)
val feedContent = _feedContent.asStateFlow()

Wyświetl plik

@ -1,6 +1,7 @@
package com.vitorpamplona.amethyst.ui.screen
import androidx.compose.animation.Crossfade
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Row
@ -164,7 +165,14 @@ fun NoteMaster(baseNote: Note, accountViewModel: AccountViewModel, navController
Modifier
.fillMaxWidth()
.padding(top = 10.dp)) {
Row(modifier = Modifier.padding(start = 12.dp, end = 12.dp)) {
Row(modifier = Modifier
.padding(start = 12.dp, end = 12.dp)
.clickable(onClick = {
author?.let {
navController.navigate("User/${it.pubkeyHex}")
}
})
) {
// Draws the boosted picture outside the boosted card.
AsyncImage(
model = author?.profilePicture(),

Wyświetl plik

@ -0,0 +1,10 @@
package com.vitorpamplona.amethyst.ui.screen
import com.vitorpamplona.amethyst.model.User
sealed class UserFeedState {
object Loading : UserFeedState()
class Loaded(val feed: List<User>) : UserFeedState()
object Empty : UserFeedState()
class FeedError(val errorMessage: String) : UserFeedState()
}

Wyświetl plik

@ -0,0 +1,78 @@
package com.vitorpamplona.amethyst.ui.screen
import androidx.compose.animation.Crossfade
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.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.unit.dp
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import androidx.navigation.NavController
import com.google.accompanist.swiperefresh.SwipeRefresh
import com.google.accompanist.swiperefresh.rememberSwipeRefreshState
import com.vitorpamplona.amethyst.ui.note.UserCompose
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
@Composable
fun UserFeedView(viewModel: UserFeedViewModel, accountViewModel: AccountViewModel, navController: NavController) {
val feedState by viewModel.feedContent.collectAsStateWithLifecycle()
var isRefreshing by remember { mutableStateOf(false) }
val swipeRefreshState = rememberSwipeRefreshState(isRefreshing)
val listState = rememberLazyListState()
LaunchedEffect(isRefreshing) {
if (isRefreshing) {
viewModel.refresh()
isRefreshing = false
}
}
SwipeRefresh(
state = swipeRefreshState,
onRefresh = {
isRefreshing = true
},
) {
Column() {
Crossfade(targetState = feedState) { state ->
when (state) {
is UserFeedState.Empty -> {
FeedEmpty {
isRefreshing = true
}
}
is UserFeedState.FeedError -> {
FeedError(state.errorMessage) {
isRefreshing = true
}
}
is UserFeedState.Loaded -> {
LazyColumn(
contentPadding = PaddingValues(
top = 10.dp,
bottom = 10.dp
),
state = listState
) {
itemsIndexed(state.feed, key = { _, item -> item.pubkeyHex }) { index, item ->
UserCompose(item, accountViewModel = accountViewModel, navController = navController)
}
}
}
UserFeedState.Loading -> {
LoadingFeed()
}
}
}
}
}
}

Wyświetl plik

@ -0,0 +1,79 @@
package com.vitorpamplona.amethyst.ui.screen
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.vitorpamplona.amethyst.model.LocalCache
import com.vitorpamplona.amethyst.model.LocalCacheState
import com.vitorpamplona.amethyst.model.User
import com.vitorpamplona.amethyst.service.NostrDataSource
import com.vitorpamplona.amethyst.service.NostrUserProfileFollowersDataSource
import com.vitorpamplona.amethyst.service.NostrUserProfileFollowsDataSource
import kotlinx.coroutines.cancel
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch
class NostrUserProfileFollowsUserFeedViewModel(): UserFeedViewModel(
NostrUserProfileFollowsDataSource
)
class NostrUserProfileFollowersUserFeedViewModel(): UserFeedViewModel(
NostrUserProfileFollowersDataSource
)
open class UserFeedViewModel(val dataSource: NostrDataSource<User>): ViewModel() {
private val _feedContent = MutableStateFlow<UserFeedState>(UserFeedState.Loading)
val feedContent = _feedContent.asStateFlow()
fun refresh() {
// For some reason, view Model Scope doesn't call
viewModelScope.launch {
refreshSuspend()
}
}
fun refreshSuspend() {
val notes = dataSource.loadTop()
val oldNotesState = feedContent.value
if (oldNotesState is UserFeedState.Loaded) {
if (notes != oldNotesState.feed) {
updateFeed(notes)
}
} else {
updateFeed(notes)
}
}
fun updateFeed(notes: List<User>) {
if (notes.isEmpty()) {
_feedContent.update { UserFeedState.Empty }
} else {
_feedContent.update { UserFeedState.Loaded(notes) }
}
}
fun refreshCurrentList() {
val state = feedContent.value
if (state is UserFeedState.Loaded) {
_feedContent.update { UserFeedState.Loaded(state.feed) }
}
}
private val cacheListener: (LocalCacheState) -> Unit = {
refresh()
}
init {
LocalCache.live.observeForever(cacheListener)
}
override fun onCleared() {
LocalCache.live.removeObserver(cacheListener)
dataSource.stop()
viewModelScope.cancel()
super.onCleared()
}
}

Wyświetl plik

@ -1,5 +1,6 @@
package com.vitorpamplona.amethyst.ui.screen
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
@ -102,9 +103,9 @@ fun ChatroomHeader(baseUser: User, accountViewModel: AccountViewModel, navContro
Column(modifier =
Modifier
.padding(12.dp)
//.clickable(
//onClick = { navController.navigate("User/${author?.pubkeyHex}") }
//)
.clickable(
onClick = { navController.navigate("User/${author?.pubkeyHex}") }
)
) {
Row(verticalAlignment = Alignment.CenterVertically) {

Wyświetl plik

@ -15,9 +15,9 @@ import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
@Composable
fun HomeScreen(accountViewModel: AccountViewModel, navController: NavController) {
val account by accountViewModel.accountLiveData.observeAsState()
val accountState by accountViewModel.accountLiveData.observeAsState()
if (account != null) {
if (accountState != null) {
val feedViewModel: FeedViewModel = viewModel { FeedViewModel( NostrHomeDataSource ) }
Column(Modifier.fillMaxHeight()) {

Wyświetl plik

@ -5,7 +5,11 @@ import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.statusBarsPadding
import androidx.compose.material.*
import androidx.compose.material.DrawerValue
import androidx.compose.material.MaterialTheme
import androidx.compose.material.Scaffold
import androidx.compose.material.rememberDrawerState
import androidx.compose.material.rememberScaffoldState
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.ui.Modifier

Wyświetl plik

@ -1,29 +1,346 @@
package com.vitorpamplona.amethyst.ui.screen
import androidx.compose.foundation.Image
import androidx.compose.foundation.background
import androidx.compose.foundation.border
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxHeight
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.shape.CircleShape
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.Icon
import androidx.compose.material.MaterialTheme
import androidx.compose.material.Surface
import androidx.compose.material.Tab
import androidx.compose.material.TabRow
import androidx.compose.material.TabRowDefaults
import androidx.compose.material.Text
import androidx.compose.material.rememberScaffoldState
import androidx.compose.runtime.Composable
import androidx.compose.runtime.DisposableEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.livedata.observeAsState
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.platform.ClipboardManager
import androidx.compose.ui.platform.LocalClipboardManager
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.text.AnnotatedString
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.lifecycle.viewmodel.compose.viewModel
import androidx.navigation.NavController
import coil.compose.AsyncImage
import com.google.accompanist.pager.ExperimentalPagerApi
import com.google.accompanist.pager.HorizontalPager
import com.google.accompanist.pager.pagerTabIndicatorOffset
import com.google.accompanist.pager.rememberPagerState
import com.vitorpamplona.amethyst.R
import com.vitorpamplona.amethyst.model.User
import com.vitorpamplona.amethyst.service.NostrUserProfileDataSource
import com.vitorpamplona.amethyst.service.NostrUserProfileFollowersDataSource
import com.vitorpamplona.amethyst.service.NostrUserProfileFollowsDataSource
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
import kotlinx.coroutines.launch
import nostr.postr.toNpub
data class TabRowItem(
val title: String,
val screen: @Composable () -> Unit,
)
@OptIn(ExperimentalPagerApi::class)
@Composable
fun ProfileScreen(userId: String?, accountViewModel: AccountViewModel, navController: NavController) {
val accountState by accountViewModel.accountLiveData.observeAsState()
val account = accountState?.account
val accountUserState by accountViewModel.userLiveData.observeAsState()
val accountUser = accountUserState?.user
val clipboardManager = LocalClipboardManager.current
if (userId != null && account != null && accountUser != null) {
DisposableEffect(account) {
NostrUserProfileDataSource.loadUserProfile(userId)
NostrUserProfileFollowersDataSource.loadUserProfile(userId)
NostrUserProfileFollowsDataSource.loadUserProfile(userId)
onDispose {
NostrUserProfileDataSource.stop()
NostrUserProfileFollowsDataSource.stop()
NostrUserProfileFollowersDataSource.stop()
}
}
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() {
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)
)
}
Column(modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 10.dp)
.padding(top = 75.dp)) {
Row(horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.Bottom) {
AsyncImage(
model = user.profilePicture(),
contentDescription = "Profile Image",
modifier = Modifier
.width(100.dp)
.height(100.dp)
.clip(shape = CircleShape)
.border(3.dp, MaterialTheme.colors.background, CircleShape)
.background(MaterialTheme.colors.background)
)
Spacer(Modifier.weight(1f))
MessageButton(user, navController)
NPubCopyButton(clipboardManager, user)
if (accountUser == user) {
EditButton()
} else {
if (accountUser.follows?.contains(user) == true) {
UnfollowButton { account.unfollow(user) }
} else {
FollowButton { account.follow(user) }
}
}
}
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 = Color.White,
modifier = Modifier.padding(top = 5.dp, bottom = 5.dp)
)
Divider(modifier = Modifier.padding(top = 6.dp))
}
}
val pagerState = rememberPagerState()
val coroutineScope = rememberCoroutineScope()
Column(modifier = Modifier.padding(start = 10.dp, end = 10.dp)) {
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")
}
)
Tab(
selected = pagerState.currentPage == 1,
onClick = { coroutineScope.launch { pagerState.animateScrollToPage(1) } },
text = {
Text(text = "${user.follows?.size ?: "--"} Following")
}
)
Tab(
selected = pagerState.currentPage == 2,
onClick = { coroutineScope.launch { pagerState.animateScrollToPage(2) } },
text = {
Text(text = "${user.followers?.size ?: "--"} Followers")
}
)
}
HorizontalPager(count = 3, state = pagerState) {
when (pagerState.currentPage) {
0 -> TabNotes(user, accountViewModel, navController)
1 -> TabFollows(user, accountViewModel, navController)
2 -> TabFollowers(user, accountViewModel, navController)
}
}
}
}
}
}
}
@Composable
fun ProfileScreen(accountViewModel: AccountViewModel) {
val state = rememberScaffoldState()
val scope = rememberCoroutineScope()
fun TabNotes(user: User, accountViewModel: AccountViewModel, navController: NavController) {
val accountState by accountViewModel.accountLiveData.observeAsState()
if (accountState != null) {
val feedViewModel: FeedViewModel = viewModel { FeedViewModel( NostrUserProfileDataSource ) }
Column(
Modifier
.fillMaxHeight()
.fillMaxWidth(),
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.Center,
) {
Text("Profile Screen")
Column(Modifier.fillMaxHeight()) {
Column(
modifier = Modifier.padding(vertical = 0.dp)
) {
FeedView(feedViewModel, accountViewModel, navController)
}
}
}
}
}
@Composable
fun TabFollows(user: User, accountViewModel: AccountViewModel, navController: NavController) {
val feedViewModel: NostrUserProfileFollowsUserFeedViewModel = viewModel()
Column(Modifier.fillMaxHeight()) {
Column(
modifier = Modifier.padding(vertical = 0.dp)
) {
UserFeedView(feedViewModel, accountViewModel, navController)
}
}
}
@Composable
fun TabFollowers(user: User, accountViewModel: AccountViewModel, navController: NavController) {
val feedViewModel: NostrUserProfileFollowersUserFeedViewModel = viewModel()
Column(Modifier.fillMaxHeight()) {
Column(
modifier = Modifier.padding(vertical = 0.dp)
) {
UserFeedView(feedViewModel, accountViewModel, navController)
}
}
}
@Composable
private fun NPubCopyButton(
clipboardManager: ClipboardManager,
user: User
) {
Button(
modifier = Modifier.padding(horizontal = 3.dp),
onClick = { clipboardManager.setText(AnnotatedString(user.pubkey.toNpub())) },
shape = RoundedCornerShape(20.dp),
colors = ButtonDefaults
.buttonColors(
backgroundColor = MaterialTheme.colors.onSurface.copy(alpha = 0.32f)
),
) {
Text(text = "npub", color = Color.White)
}
}
@Composable
private fun MessageButton(user: User, navController: NavController) {
Button(
modifier = Modifier.padding(horizontal = 3.dp),
onClick = { navController.navigate("Room/${user.pubkeyHex}") },
shape = RoundedCornerShape(20.dp),
colors = ButtonDefaults
.buttonColors(
backgroundColor = MaterialTheme.colors.onSurface.copy(alpha = 0.32f)
),
) {
Icon(
painter = painterResource(R.drawable.ic_dm),
"Send a Direct Message",
modifier = Modifier.size(20.dp),
tint = Color.Unspecified
)
}
}
@Composable
private fun EditButton() {
Button(
modifier = Modifier.padding(horizontal = 3.dp),
onClick = {},
shape = RoundedCornerShape(20.dp),
colors = ButtonDefaults
.buttonColors(
backgroundColor = MaterialTheme.colors.primary
)
) {
Text(text = "Edit", color = Color.White)
}
}
@Composable
fun UnfollowButton(onClick: () -> Unit) {
Button(
modifier = Modifier.padding(horizontal = 3.dp),
onClick = onClick,
shape = RoundedCornerShape(20.dp),
colors = ButtonDefaults
.buttonColors(
backgroundColor = MaterialTheme.colors.primary
)
) {
Text(text = "Unfollow", color = Color.White)
}
}
@Composable
fun FollowButton(onClick: () -> Unit) {
Button(
modifier = Modifier.padding(start = 3.dp),
onClick = onClick,
shape = RoundedCornerShape(20.dp),
colors = ButtonDefaults
.buttonColors(
backgroundColor = MaterialTheme.colors.primary
)
) {
Text(text = "Follow", color = Color.White)
}
}

Wyświetl plik

@ -4,6 +4,7 @@ import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxHeight
import androidx.compose.foundation.layout.padding
import androidx.compose.runtime.Composable
import androidx.compose.runtime.DisposableEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.livedata.observeAsState
import androidx.compose.ui.Modifier
@ -17,6 +18,12 @@ import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
fun ThreadScreen(noteId: String?, accountViewModel: AccountViewModel, navController: NavController) {
val account by accountViewModel.accountLiveData.observeAsState()
DisposableEffect(account) {
onDispose {
NostrThreadDataSource.stop()
}
}
if (account != null && noteId != null) {
NostrThreadDataSource.loadThread(noteId)

Wyświetl plik

@ -43,7 +43,6 @@ import androidx.compose.ui.text.style.TextDecoration
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import com.vitorpamplona.amethyst.R
import com.vitorpamplona.amethyst.ui.theme.Purple700
@Composable
fun LoginPage(accountViewModel: AccountStateViewModel) {

Wyświetl plik

@ -3,6 +3,7 @@ buildscript {
compose_ui_version = '1.3.3'
nav_version = "2.5.3"
room_version = "2.4.3"
accompanist_version = "0.28.0"
}
}// Top-level build file where you can add configuration options common to all sub-projects/modules.
plugins {