mirror of
https://github.com/vitorpamplona/amethyst.git
synced 2025-11-10 22:46:49 +01:00
Support for User Profiles
This commit is contained in:
@@ -99,6 +99,10 @@ dependencies {
|
|||||||
// view videos
|
// view videos
|
||||||
implementation 'com.google.android.exoplayer:exoplayer:2.18.2'
|
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'
|
testImplementation 'junit:junit:4.13.2'
|
||||||
androidTestImplementation 'androidx.test.ext:junit:1.1.5'
|
androidTestImplementation 'androidx.test.ext:junit:1.1.5'
|
||||||
androidTestImplementation 'androidx.test.espresso:espresso-core:3.5.1'
|
androidTestImplementation 'androidx.test.espresso:espresso-core:3.5.1'
|
||||||
|
|||||||
@@ -1,11 +1,14 @@
|
|||||||
package com.vitorpamplona.amethyst.model
|
package com.vitorpamplona.amethyst.model
|
||||||
|
|
||||||
import androidx.lifecycle.LiveData
|
import androidx.lifecycle.LiveData
|
||||||
|
import com.vitorpamplona.amethyst.service.Constants
|
||||||
import com.vitorpamplona.amethyst.service.model.ReactionEvent
|
import com.vitorpamplona.amethyst.service.model.ReactionEvent
|
||||||
import com.vitorpamplona.amethyst.service.model.RepostEvent
|
import com.vitorpamplona.amethyst.service.model.RepostEvent
|
||||||
import com.vitorpamplona.amethyst.service.relays.Client
|
import com.vitorpamplona.amethyst.service.relays.Client
|
||||||
|
import nostr.postr.Contact
|
||||||
import nostr.postr.Persona
|
import nostr.postr.Persona
|
||||||
import nostr.postr.Utils
|
import nostr.postr.Utils
|
||||||
|
import nostr.postr.events.ContactListEvent
|
||||||
import nostr.postr.events.PrivateDmEvent
|
import nostr.postr.events.PrivateDmEvent
|
||||||
import nostr.postr.events.TextNoteEvent
|
import nostr.postr.events.TextNoteEvent
|
||||||
import nostr.postr.toHex
|
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?) {
|
fun sendPost(message: String, replyingTo: Note?) {
|
||||||
if (!isWriteable()) return
|
if (!isWriteable()) return
|
||||||
|
|
||||||
@@ -99,12 +128,15 @@ class Account(val loggedIn: Persona) {
|
|||||||
val event = note.event
|
val event = note.event
|
||||||
return if (event is PrivateDmEvent && loggedIn.privKey != null) {
|
return if (event is PrivateDmEvent && loggedIn.privKey != null) {
|
||||||
var pubkeyToUse = event.pubKey
|
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 {
|
return try {
|
||||||
|
val sharedSecret = Utils.getSharedSecret(loggedIn.privKey!!, pubkeyToUse)
|
||||||
|
|
||||||
val retVal = Utils.decrypt(event.content, sharedSecret)
|
val retVal = Utils.decrypt(event.content, sharedSecret)
|
||||||
|
|
||||||
if (retVal.startsWith(PrivateDmEvent.nip18Advertisement)) {
|
if (retVal.startsWith(PrivateDmEvent.nip18Advertisement)) {
|
||||||
|
|||||||
@@ -116,7 +116,7 @@ object LocalCache {
|
|||||||
val user = getOrCreateUser(event.pubKey)
|
val user = getOrCreateUser(event.pubKey)
|
||||||
|
|
||||||
if (event.createdAt > user.updatedFollowsAt) {
|
if (event.createdAt > user.updatedFollowsAt) {
|
||||||
//Log.d("CL", "${user.toBestDisplayName()} ${event.follows.size}")
|
Log.d("CL", "AAA ${user.toBestDisplayName()} ${event.follows.size}")
|
||||||
user.updateFollows(
|
user.updateFollows(
|
||||||
event.follows.map {
|
event.follows.map {
|
||||||
try {
|
try {
|
||||||
@@ -131,6 +131,8 @@ object LocalCache {
|
|||||||
}.filterNotNull(),
|
}.filterNotNull(),
|
||||||
event.createdAt
|
event.createdAt
|
||||||
)
|
)
|
||||||
|
|
||||||
|
user.lastestContactList = event
|
||||||
}
|
}
|
||||||
|
|
||||||
refreshObservers()
|
refreshObservers()
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import com.vitorpamplona.amethyst.service.NostrSingleUserDataSource
|
|||||||
import com.vitorpamplona.amethyst.ui.note.toDisplayHex
|
import com.vitorpamplona.amethyst.ui.note.toDisplayHex
|
||||||
import java.util.Collections
|
import java.util.Collections
|
||||||
import java.util.concurrent.ConcurrentHashMap
|
import java.util.concurrent.ConcurrentHashMap
|
||||||
|
import nostr.postr.events.ContactListEvent
|
||||||
|
|
||||||
class User(val pubkey: ByteArray) {
|
class User(val pubkey: ByteArray) {
|
||||||
val pubkeyHex = pubkey.toHexKey()
|
val pubkeyHex = pubkey.toHexKey()
|
||||||
@@ -15,6 +16,8 @@ class User(val pubkey: ByteArray) {
|
|||||||
var updatedMetadataAt: Long = 0;
|
var updatedMetadataAt: Long = 0;
|
||||||
var updatedFollowsAt: Long = 0;
|
var updatedFollowsAt: Long = 0;
|
||||||
|
|
||||||
|
var lastestContactList: ContactListEvent? = null
|
||||||
|
|
||||||
val notes = Collections.synchronizedSet(mutableSetOf<Note>())
|
val notes = Collections.synchronizedSet(mutableSetOf<Note>())
|
||||||
val follows = Collections.synchronizedSet(mutableSetOf<User>())
|
val follows = Collections.synchronizedSet(mutableSetOf<User>())
|
||||||
val taggedPosts = Collections.synchronizedSet(mutableSetOf<Note>())
|
val taggedPosts = Collections.synchronizedSet(mutableSetOf<Note>())
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ import nostr.postr.events.ContactListEvent
|
|||||||
import nostr.postr.events.MetadataEvent
|
import nostr.postr.events.MetadataEvent
|
||||||
import nostr.postr.events.TextNoteEvent
|
import nostr.postr.events.TextNoteEvent
|
||||||
|
|
||||||
object NostrAccountDataSource: NostrDataSource("AccountData") {
|
object NostrAccountDataSource: NostrDataSource<Note>("AccountData") {
|
||||||
lateinit var account: Account
|
lateinit var account: Account
|
||||||
|
|
||||||
private val cacheListener: (UserState) -> Unit = {
|
private val cacheListener: (UserState) -> Unit = {
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ import com.vitorpamplona.amethyst.model.User
|
|||||||
import nostr.postr.JsonFilter
|
import nostr.postr.JsonFilter
|
||||||
import nostr.postr.events.PrivateDmEvent
|
import nostr.postr.events.PrivateDmEvent
|
||||||
|
|
||||||
object NostrChatRoomDataSource: NostrDataSource("ChatroomFeed") {
|
object NostrChatRoomDataSource: NostrDataSource<Note>("ChatroomFeed") {
|
||||||
lateinit var account: Account
|
lateinit var account: Account
|
||||||
var withUser: User? = null
|
var withUser: User? = null
|
||||||
|
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import com.vitorpamplona.amethyst.model.Note
|
|||||||
import nostr.postr.JsonFilter
|
import nostr.postr.JsonFilter
|
||||||
import nostr.postr.events.PrivateDmEvent
|
import nostr.postr.events.PrivateDmEvent
|
||||||
|
|
||||||
object NostrChatroomListDataSource: NostrDataSource("MailBoxFeed") {
|
object NostrChatroomListDataSource: NostrDataSource<Note>("MailBoxFeed") {
|
||||||
lateinit var account: Account
|
lateinit var account: Account
|
||||||
|
|
||||||
fun createMessagesToMeFilter() = JsonFilter(
|
fun createMessagesToMeFilter() = JsonFilter(
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
package com.vitorpamplona.amethyst.service
|
package com.vitorpamplona.amethyst.service
|
||||||
|
|
||||||
import com.vitorpamplona.amethyst.model.LocalCache
|
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.ReactionEvent
|
||||||
import com.vitorpamplona.amethyst.service.model.RepostEvent
|
import com.vitorpamplona.amethyst.service.model.RepostEvent
|
||||||
import com.vitorpamplona.amethyst.service.relays.Client
|
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.RecommendRelayEvent
|
||||||
import nostr.postr.events.TextNoteEvent
|
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 channels = Collections.synchronizedSet(mutableSetOf<Channel>())
|
||||||
private val channelIds = Collections.synchronizedSet(mutableSetOf<String>())
|
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)
|
return feed().take(100)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -135,5 +134,5 @@ abstract class NostrDataSource(val debugName: String) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
abstract fun updateChannelFilters()
|
abstract fun updateChannelFilters()
|
||||||
abstract fun feed(): List<Note>
|
abstract fun feed(): List<T>
|
||||||
}
|
}
|
||||||
@@ -1,10 +1,11 @@
|
|||||||
package com.vitorpamplona.amethyst.service
|
package com.vitorpamplona.amethyst.service
|
||||||
|
|
||||||
import com.vitorpamplona.amethyst.model.LocalCache
|
import com.vitorpamplona.amethyst.model.LocalCache
|
||||||
|
import com.vitorpamplona.amethyst.model.Note
|
||||||
import nostr.postr.JsonFilter
|
import nostr.postr.JsonFilter
|
||||||
import nostr.postr.events.TextNoteEvent
|
import nostr.postr.events.TextNoteEvent
|
||||||
|
|
||||||
object NostrGlobalDataSource: NostrDataSource("GlobalFeed") {
|
object NostrGlobalDataSource: NostrDataSource<Note>("GlobalFeed") {
|
||||||
val fifteenMinutes = (60*15) // 15 mins
|
val fifteenMinutes = (60*15) // 15 mins
|
||||||
|
|
||||||
fun createGlobalFilter() = JsonFilter(
|
fun createGlobalFilter() = JsonFilter(
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ import nostr.postr.JsonFilter
|
|||||||
import nostr.postr.events.TextNoteEvent
|
import nostr.postr.events.TextNoteEvent
|
||||||
import nostr.postr.toHex
|
import nostr.postr.toHex
|
||||||
|
|
||||||
object NostrHomeDataSource: NostrDataSource("HomeFeed") {
|
object NostrHomeDataSource: NostrDataSource<Note>("HomeFeed") {
|
||||||
lateinit var account: Account
|
lateinit var account: Account
|
||||||
|
|
||||||
private val cacheListener: (UserState) -> Unit = {
|
private val cacheListener: (UserState) -> Unit = {
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import com.vitorpamplona.amethyst.model.Account
|
|||||||
import com.vitorpamplona.amethyst.model.Note
|
import com.vitorpamplona.amethyst.model.Note
|
||||||
import nostr.postr.JsonFilter
|
import nostr.postr.JsonFilter
|
||||||
|
|
||||||
object NostrNotificationDataSource: NostrDataSource("GlobalFeed") {
|
object NostrNotificationDataSource: NostrDataSource<Note>("GlobalFeed") {
|
||||||
lateinit var account: Account
|
lateinit var account: Account
|
||||||
|
|
||||||
fun createGlobalFilter() = JsonFilter(
|
fun createGlobalFilter() = JsonFilter(
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ import java.util.Collections
|
|||||||
import nostr.postr.JsonFilter
|
import nostr.postr.JsonFilter
|
||||||
import nostr.postr.events.TextNoteEvent
|
import nostr.postr.events.TextNoteEvent
|
||||||
|
|
||||||
object NostrSingleEventDataSource: NostrDataSource("SingleEventFeed") {
|
object NostrSingleEventDataSource: NostrDataSource<Note>("SingleEventFeed") {
|
||||||
val eventsToWatch = Collections.synchronizedList(mutableListOf<String>())
|
val eventsToWatch = Collections.synchronizedList(mutableListOf<String>())
|
||||||
|
|
||||||
fun createRepliesAndReactionsFilter(): JsonFilter? {
|
fun createRepliesAndReactionsFilter(): JsonFilter? {
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import java.util.Collections
|
|||||||
import nostr.postr.JsonFilter
|
import nostr.postr.JsonFilter
|
||||||
import nostr.postr.events.MetadataEvent
|
import nostr.postr.events.MetadataEvent
|
||||||
|
|
||||||
object NostrSingleUserDataSource: NostrDataSource("SingleUserFeed") {
|
object NostrSingleUserDataSource: NostrDataSource<Note>("SingleUserFeed") {
|
||||||
val usersToWatch = Collections.synchronizedList(mutableListOf<String>())
|
val usersToWatch = Collections.synchronizedList(mutableListOf<String>())
|
||||||
|
|
||||||
fun createUserFilter(): JsonFilter? {
|
fun createUserFilter(): JsonFilter? {
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import com.vitorpamplona.amethyst.model.Note
|
|||||||
import java.util.Collections
|
import java.util.Collections
|
||||||
import nostr.postr.JsonFilter
|
import nostr.postr.JsonFilter
|
||||||
|
|
||||||
object NostrThreadDataSource: NostrDataSource("SingleThreadFeed") {
|
object NostrThreadDataSource: NostrDataSource<Note>("SingleThreadFeed") {
|
||||||
val eventsToWatch = Collections.synchronizedList(mutableListOf<String>())
|
val eventsToWatch = Collections.synchronizedList(mutableListOf<String>())
|
||||||
|
|
||||||
fun createRepliesAndReactionsFilter(): JsonFilter? {
|
fun createRepliesAndReactionsFilter(): JsonFilter? {
|
||||||
|
|||||||
@@ -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()
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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()
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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()
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -17,6 +17,9 @@ import com.vitorpamplona.amethyst.service.NostrNotificationDataSource
|
|||||||
import com.vitorpamplona.amethyst.service.NostrSingleEventDataSource
|
import com.vitorpamplona.amethyst.service.NostrSingleEventDataSource
|
||||||
import com.vitorpamplona.amethyst.service.NostrSingleUserDataSource
|
import com.vitorpamplona.amethyst.service.NostrSingleUserDataSource
|
||||||
import com.vitorpamplona.amethyst.service.NostrThreadDataSource
|
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.service.relays.Client
|
||||||
import com.vitorpamplona.amethyst.ui.screen.AccountScreen
|
import com.vitorpamplona.amethyst.ui.screen.AccountScreen
|
||||||
import com.vitorpamplona.amethyst.ui.screen.AccountStateViewModel
|
import com.vitorpamplona.amethyst.ui.screen.AccountStateViewModel
|
||||||
@@ -52,6 +55,9 @@ class MainActivity : ComponentActivity() {
|
|||||||
NostrAccountDataSource.stop()
|
NostrAccountDataSource.stop()
|
||||||
NostrHomeDataSource.stop()
|
NostrHomeDataSource.stop()
|
||||||
NostrChatroomListDataSource.stop()
|
NostrChatroomListDataSource.stop()
|
||||||
|
NostrUserProfileDataSource.stop()
|
||||||
|
NostrUserProfileFollowersDataSource.stop()
|
||||||
|
NostrUserProfileFollowsDataSource.stop()
|
||||||
|
|
||||||
NostrGlobalDataSource.stop()
|
NostrGlobalDataSource.stop()
|
||||||
NostrNotificationDataSource.stop()
|
NostrNotificationDataSource.stop()
|
||||||
|
|||||||
@@ -10,7 +10,6 @@ import androidx.compose.material.Text
|
|||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.runtime.livedata.observeAsState
|
import androidx.compose.runtime.livedata.observeAsState
|
||||||
import androidx.compose.ui.ExperimentalComposeUiApi
|
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.text.AnnotatedString
|
import androidx.compose.ui.text.AnnotatedString
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
|
|||||||
@@ -38,14 +38,13 @@ import kotlinx.coroutines.launch
|
|||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun AppTopBar(navController: NavHostController, scaffoldState: ScaffoldState, accountViewModel: AccountViewModel) {
|
fun AppTopBar(navController: NavHostController, scaffoldState: ScaffoldState, accountViewModel: AccountViewModel) {
|
||||||
|
|
||||||
|
|
||||||
when (currentRoute(navController)) {
|
when (currentRoute(navController)) {
|
||||||
Route.Profile.route,
|
//Route.Profile.route -> TopBarWithBackButton(navController)
|
||||||
Route.Lists.route,
|
|
||||||
Route.Topics.route,
|
|
||||||
Route.Bookmarks.route,
|
|
||||||
Route.Moments.route -> TopBarWithBackButton(navController)
|
|
||||||
else -> MainTopBar(scaffoldState, accountViewModel)
|
else -> MainTopBar(scaffoldState, accountViewModel)
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
|
|||||||
@@ -13,7 +13,6 @@ import androidx.compose.foundation.layout.padding
|
|||||||
import androidx.compose.foundation.layout.size
|
import androidx.compose.foundation.layout.size
|
||||||
import androidx.compose.foundation.layout.width
|
import androidx.compose.foundation.layout.width
|
||||||
import androidx.compose.foundation.lazy.LazyColumn
|
import androidx.compose.foundation.lazy.LazyColumn
|
||||||
import androidx.compose.foundation.lazy.items
|
|
||||||
import androidx.compose.foundation.shape.CircleShape
|
import androidx.compose.foundation.shape.CircleShape
|
||||||
import androidx.compose.material.Divider
|
import androidx.compose.material.Divider
|
||||||
import androidx.compose.material.Icon
|
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.text.font.FontWeight.Companion.W500
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.compose.ui.unit.sp
|
import androidx.compose.ui.unit.sp
|
||||||
|
import androidx.navigation.NavController
|
||||||
import androidx.navigation.NavHostController
|
import androidx.navigation.NavHostController
|
||||||
import coil.compose.AsyncImage
|
import coil.compose.AsyncImage
|
||||||
import com.vitorpamplona.amethyst.R
|
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 com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
|
|
||||||
val bottomNavigations = listOf(
|
|
||||||
Route.Profile,
|
|
||||||
Route.Lists,
|
|
||||||
//Route.Topics,
|
|
||||||
Route.Bookmarks,
|
|
||||||
//Route.Moments
|
|
||||||
)
|
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun DrawerContent(navController: NavHostController,
|
fun DrawerContent(navController: NavHostController,
|
||||||
scaffoldState: ScaffoldState,
|
scaffoldState: ScaffoldState,
|
||||||
@@ -66,19 +58,31 @@ fun DrawerContent(navController: NavHostController,
|
|||||||
) {
|
) {
|
||||||
Column() {
|
Column() {
|
||||||
Box {
|
Box {
|
||||||
Image(
|
val banner = accountUser?.info?.banner
|
||||||
painter = painterResource(R.drawable.profile_banner),
|
if (banner != null && banner.isNotBlank()) {
|
||||||
contentDescription = "Profile Banner",
|
AsyncImage(
|
||||||
contentScale = ContentScale.FillWidth,
|
model = banner,
|
||||||
modifier = Modifier.fillMaxWidth()
|
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(
|
ProfileContent(
|
||||||
accountUser,
|
accountUser,
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.padding(horizontal = 25.dp)
|
.padding(horizontal = 25.dp)
|
||||||
.padding(top = 125.dp)
|
.padding(top = 100.dp),
|
||||||
|
scaffoldState,
|
||||||
|
navController
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
Divider(
|
Divider(
|
||||||
@@ -86,6 +90,7 @@ fun DrawerContent(navController: NavHostController,
|
|||||||
modifier = Modifier.padding(top = 20.dp)
|
modifier = Modifier.padding(top = 20.dp)
|
||||||
)
|
)
|
||||||
ListContent(
|
ListContent(
|
||||||
|
accountUser,
|
||||||
navController,
|
navController,
|
||||||
scaffoldState,
|
scaffoldState,
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
@@ -98,7 +103,9 @@ fun DrawerContent(navController: NavHostController,
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@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) {
|
Column(modifier = modifier) {
|
||||||
AsyncImage(
|
AsyncImage(
|
||||||
model = accountUser?.profilePicture() ?: "https://robohash.org/ohno.png",
|
model = accountUser?.profilePicture() ?: "https://robohash.org/ohno.png",
|
||||||
@@ -108,6 +115,14 @@ fun ProfileContent(accountUser: User?, modifier: Modifier = Modifier) {
|
|||||||
.clip(shape = CircleShape)
|
.clip(shape = CircleShape)
|
||||||
.border(3.dp, MaterialTheme.colors.background, CircleShape)
|
.border(3.dp, MaterialTheme.colors.background, CircleShape)
|
||||||
.background(MaterialTheme.colors.background)
|
.background(MaterialTheme.colors.background)
|
||||||
|
.clickable(onClick = {
|
||||||
|
accountUser?.let {
|
||||||
|
navController.navigate("User/${it.pubkeyHex}")
|
||||||
|
}
|
||||||
|
coroutineScope.launch {
|
||||||
|
scaffoldState.drawerState.close()
|
||||||
|
}
|
||||||
|
})
|
||||||
)
|
)
|
||||||
Text(
|
Text(
|
||||||
accountUser?.bestDisplayName() ?: "",
|
accountUser?.bestDisplayName() ?: "",
|
||||||
@@ -131,21 +146,25 @@ fun ProfileContent(accountUser: User?, modifier: Modifier = Modifier) {
|
|||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun ListContent(
|
fun ListContent(
|
||||||
|
accountUser: User?,
|
||||||
navController: NavHostController,
|
navController: NavHostController,
|
||||||
scaffoldState: ScaffoldState,
|
scaffoldState: ScaffoldState,
|
||||||
modifier: Modifier,
|
modifier: Modifier,
|
||||||
accountViewModel: AccountStateViewModel
|
accountViewModel: AccountStateViewModel
|
||||||
) {
|
) {
|
||||||
Column(
|
Column(modifier = modifier) {
|
||||||
modifier = modifier
|
|
||||||
) {
|
|
||||||
LazyColumn() {
|
LazyColumn() {
|
||||||
items(items = bottomNavigations) {
|
|
||||||
NavigationRow(navController, scaffoldState, it)
|
|
||||||
}
|
|
||||||
item {
|
item {
|
||||||
|
if (accountUser != null)
|
||||||
|
NavigationRow(navController,
|
||||||
|
scaffoldState,
|
||||||
|
"User/${accountUser.pubkeyHex}",
|
||||||
|
Route.Profile.icon,
|
||||||
|
"Profile"
|
||||||
|
)
|
||||||
|
|
||||||
Divider(
|
Divider(
|
||||||
modifier = Modifier.padding(vertical = 15.dp),
|
modifier = Modifier.padding(bottom = 15.dp),
|
||||||
thickness = 0.25.dp
|
thickness = 0.25.dp
|
||||||
)
|
)
|
||||||
Column(modifier = modifier.padding(horizontal = 25.dp)) {
|
Column(modifier = modifier.padding(horizontal = 25.dp)) {
|
||||||
@@ -171,31 +190,36 @@ fun ListContent(
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@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 coroutineScope = rememberCoroutineScope()
|
||||||
val currentRoute = currentRoute(navController)
|
val currentRoute = currentRoute(navController)
|
||||||
Row(
|
Row(modifier = Modifier
|
||||||
modifier = Modifier
|
.fillMaxWidth()
|
||||||
.padding(vertical = 15.dp, horizontal = 25.dp)
|
.clickable(onClick = {
|
||||||
.clickable(onClick = {
|
if (currentRoute != route) {
|
||||||
if (currentRoute != route.route) {
|
navController.navigate(route)
|
||||||
navController.navigate(route.route)
|
}
|
||||||
}
|
coroutineScope.launch {
|
||||||
coroutineScope.launch {
|
scaffoldState.drawerState.close()
|
||||||
scaffoldState.drawerState.close()
|
}
|
||||||
}
|
})
|
||||||
}),
|
|
||||||
verticalAlignment = Alignment.CenterVertically
|
|
||||||
) {
|
) {
|
||||||
Icon(
|
Row(
|
||||||
painter = painterResource(route.icon), null,
|
modifier = Modifier.fillMaxWidth()
|
||||||
modifier = Modifier.size(22.dp),
|
.padding(vertical = 15.dp, horizontal = 25.dp),
|
||||||
tint = MaterialTheme.colors.primary
|
verticalAlignment = Alignment.CenterVertically
|
||||||
)
|
) {
|
||||||
Text(
|
Icon(
|
||||||
modifier = Modifier.padding(start = 16.dp),
|
painter = painterResource(icon), null,
|
||||||
text = route.route,
|
modifier = Modifier.size(22.dp),
|
||||||
fontSize = 18.sp,
|
tint = MaterialTheme.colors.primary
|
||||||
)
|
)
|
||||||
|
Text(
|
||||||
|
modifier = Modifier.padding(start = 16.dp),
|
||||||
|
text = title,
|
||||||
|
fontSize = 18.sp,
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -30,11 +30,11 @@ sealed class Route(
|
|||||||
object Search : Route("Search", R.drawable.ic_search, buildScreen = { acc, nav -> { _ -> SearchScreen(acc, nav) }})
|
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 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 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 Profile : Route("User/{id}", R.drawable.ic_profile,
|
||||||
object Topics : Route("Topics", R.drawable.ic_topics, buildScreen = { acc, nav -> { _ -> ProfileScreen(acc) }})
|
arguments = listOf(navArgument("id") { type = NavType.StringType } ),
|
||||||
object Bookmarks : Route("Bookmarks", R.drawable.ic_bookmarks, buildScreen = { acc, nav -> { _ -> ProfileScreen(acc) }})
|
buildScreen = { acc, nav -> { ProfileScreen(it.arguments?.getString("id"), acc, nav) }}
|
||||||
object Moments : Route("Moments", R.drawable.ic_moments, buildScreen = { acc, nav -> { _ -> ProfileScreen(acc) }})
|
)
|
||||||
|
|
||||||
object Note : Route("Note/{id}", R.drawable.ic_moments,
|
object Note : Route("Note/{id}", R.drawable.ic_moments,
|
||||||
arguments = listOf(navArgument("id") { type = NavType.StringType } ),
|
arguments = listOf(navArgument("id") { type = NavType.StringType } ),
|
||||||
@@ -56,12 +56,6 @@ val Routes = listOf(
|
|||||||
|
|
||||||
//drawer
|
//drawer
|
||||||
Route.Profile,
|
Route.Profile,
|
||||||
Route.Lists,
|
|
||||||
Route.Topics,
|
|
||||||
Route.Bookmarks,
|
|
||||||
Route.Moments,
|
|
||||||
|
|
||||||
//inner
|
|
||||||
Route.Note,
|
Route.Note,
|
||||||
Route.Room
|
Route.Room
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
package com.vitorpamplona.amethyst.ui.note
|
package com.vitorpamplona.amethyst.ui.note
|
||||||
|
|
||||||
|
import androidx.compose.foundation.clickable
|
||||||
import androidx.compose.foundation.layout.Box
|
import androidx.compose.foundation.layout.Box
|
||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.layout.Column
|
||||||
import androidx.compose.foundation.layout.Row
|
import androidx.compose.foundation.layout.Row
|
||||||
@@ -71,6 +72,11 @@ fun BoostSetCompose(likeSetCard: BoostSetCard, isInnerNote: Boolean = false, acc
|
|||||||
.width(35.dp)
|
.width(35.dp)
|
||||||
.height(35.dp)
|
.height(35.dp)
|
||||||
.clip(shape = CircleShape)
|
.clip(shape = CircleShape)
|
||||||
|
.clickable(onClick = {
|
||||||
|
userState?.let {
|
||||||
|
navController.navigate("User/${it.user.pubkeyHex}")
|
||||||
|
}
|
||||||
|
})
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -52,7 +52,7 @@ fun ChatroomCompose(baseNote: Note, accountViewModel: AccountViewModel, navContr
|
|||||||
|
|
||||||
Column(modifier =
|
Column(modifier =
|
||||||
Modifier.clickable(
|
Modifier.clickable(
|
||||||
onClick = { navController.navigate("Room/${userToComposeOn?.pubkeyHex}") }
|
onClick = { navController.navigate("Room/${userToComposeOn?.pubkeyHex}") }
|
||||||
)
|
)
|
||||||
) {
|
) {
|
||||||
Row(
|
Row(
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
package com.vitorpamplona.amethyst.ui.note
|
package com.vitorpamplona.amethyst.ui.note
|
||||||
|
|
||||||
|
import androidx.compose.foundation.clickable
|
||||||
import androidx.compose.foundation.layout.Box
|
import androidx.compose.foundation.layout.Box
|
||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.layout.Column
|
||||||
import androidx.compose.foundation.layout.Row
|
import androidx.compose.foundation.layout.Row
|
||||||
@@ -71,6 +72,11 @@ fun LikeSetCompose(likeSetCard: LikeSetCard, modifier: Modifier = Modifier, isIn
|
|||||||
.width(35.dp).height(35.dp)
|
.width(35.dp).height(35.dp)
|
||||||
.height(35.dp)
|
.height(35.dp)
|
||||||
.clip(shape = CircleShape)
|
.clip(shape = CircleShape)
|
||||||
|
.clickable(onClick = {
|
||||||
|
userState?.let {
|
||||||
|
navController.navigate("User/${it.user.pubkeyHex}")
|
||||||
|
}
|
||||||
|
})
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ package com.vitorpamplona.amethyst.ui.note
|
|||||||
import androidx.compose.foundation.ExperimentalFoundationApi
|
import androidx.compose.foundation.ExperimentalFoundationApi
|
||||||
import androidx.compose.foundation.background
|
import androidx.compose.foundation.background
|
||||||
import androidx.compose.foundation.border
|
import androidx.compose.foundation.border
|
||||||
|
import androidx.compose.foundation.clickable
|
||||||
import androidx.compose.foundation.combinedClickable
|
import androidx.compose.foundation.combinedClickable
|
||||||
import androidx.compose.foundation.layout.Box
|
import androidx.compose.foundation.layout.Box
|
||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.layout.Column
|
||||||
@@ -77,6 +78,11 @@ fun NoteCompose(baseNote: Note, modifier: Modifier = Modifier, isInnerNote: Bool
|
|||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.width(55.dp).height(55.dp)
|
.width(55.dp).height(55.dp)
|
||||||
.clip(shape = CircleShape)
|
.clip(shape = CircleShape)
|
||||||
|
.clickable(onClick = {
|
||||||
|
author?.let {
|
||||||
|
navController.navigate("User/${it.pubkeyHex}")
|
||||||
|
}
|
||||||
|
})
|
||||||
)
|
)
|
||||||
|
|
||||||
// boosted picture
|
// boosted picture
|
||||||
@@ -91,12 +97,23 @@ fun NoteCompose(baseNote: Note, modifier: Modifier = Modifier, isInnerNote: Bool
|
|||||||
.align(Alignment.BottomEnd)
|
.align(Alignment.BottomEnd)
|
||||||
.background(MaterialTheme.colors.background)
|
.background(MaterialTheme.colors.background)
|
||||||
.border(2.dp, MaterialTheme.colors.primary, CircleShape)
|
.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) {
|
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||||
if (author != null)
|
if (author != null)
|
||||||
UserDisplay(author)
|
UserDisplay(author)
|
||||||
|
|||||||
@@ -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
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -14,7 +14,7 @@ import kotlinx.coroutines.flow.asStateFlow
|
|||||||
import kotlinx.coroutines.flow.update
|
import kotlinx.coroutines.flow.update
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
|
|
||||||
class CardFeedViewModel(val dataSource: NostrDataSource): ViewModel() {
|
class CardFeedViewModel(val dataSource: NostrDataSource<Note>): ViewModel() {
|
||||||
private val _feedContent = MutableStateFlow<CardFeedState>(CardFeedState.Loading)
|
private val _feedContent = MutableStateFlow<CardFeedState>(CardFeedState.Loading)
|
||||||
val feedContent = _feedContent.asStateFlow()
|
val feedContent = _feedContent.asStateFlow()
|
||||||
|
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ import kotlinx.coroutines.flow.update
|
|||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
|
|
||||||
|
|
||||||
class FeedViewModel(val dataSource: NostrDataSource): ViewModel() {
|
class FeedViewModel(val dataSource: NostrDataSource<Note>): ViewModel() {
|
||||||
private val _feedContent = MutableStateFlow<FeedState>(FeedState.Loading)
|
private val _feedContent = MutableStateFlow<FeedState>(FeedState.Loading)
|
||||||
val feedContent = _feedContent.asStateFlow()
|
val feedContent = _feedContent.asStateFlow()
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
package com.vitorpamplona.amethyst.ui.screen
|
package com.vitorpamplona.amethyst.ui.screen
|
||||||
|
|
||||||
import androidx.compose.animation.Crossfade
|
import androidx.compose.animation.Crossfade
|
||||||
|
import androidx.compose.foundation.clickable
|
||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.layout.Column
|
||||||
import androidx.compose.foundation.layout.PaddingValues
|
import androidx.compose.foundation.layout.PaddingValues
|
||||||
import androidx.compose.foundation.layout.Row
|
import androidx.compose.foundation.layout.Row
|
||||||
@@ -164,7 +165,14 @@ fun NoteMaster(baseNote: Note, accountViewModel: AccountViewModel, navController
|
|||||||
Modifier
|
Modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.padding(top = 10.dp)) {
|
.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.
|
// Draws the boosted picture outside the boosted card.
|
||||||
AsyncImage(
|
AsyncImage(
|
||||||
model = author?.profilePicture(),
|
model = author?.profilePicture(),
|
||||||
|
|||||||
@@ -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()
|
||||||
|
}
|
||||||
@@ -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()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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()
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
package com.vitorpamplona.amethyst.ui.screen
|
package com.vitorpamplona.amethyst.ui.screen
|
||||||
|
|
||||||
|
import androidx.compose.foundation.clickable
|
||||||
import androidx.compose.foundation.layout.Arrangement
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.layout.Column
|
||||||
import androidx.compose.foundation.layout.Row
|
import androidx.compose.foundation.layout.Row
|
||||||
@@ -102,9 +103,9 @@ fun ChatroomHeader(baseUser: User, accountViewModel: AccountViewModel, navContro
|
|||||||
Column(modifier =
|
Column(modifier =
|
||||||
Modifier
|
Modifier
|
||||||
.padding(12.dp)
|
.padding(12.dp)
|
||||||
//.clickable(
|
.clickable(
|
||||||
//onClick = { navController.navigate("User/${author?.pubkeyHex}") }
|
onClick = { navController.navigate("User/${author?.pubkeyHex}") }
|
||||||
//)
|
)
|
||||||
) {
|
) {
|
||||||
Row(verticalAlignment = Alignment.CenterVertically) {
|
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||||
|
|
||||||
|
|||||||
@@ -15,9 +15,9 @@ import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
|||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun HomeScreen(accountViewModel: AccountViewModel, navController: NavController) {
|
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 ) }
|
val feedViewModel: FeedViewModel = viewModel { FeedViewModel( NostrHomeDataSource ) }
|
||||||
|
|
||||||
Column(Modifier.fillMaxHeight()) {
|
Column(Modifier.fillMaxHeight()) {
|
||||||
|
|||||||
@@ -5,7 +5,11 @@ import androidx.compose.foundation.background
|
|||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.layout.Column
|
||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
import androidx.compose.foundation.layout.statusBarsPadding
|
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.Composable
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
|
|||||||
@@ -1,29 +1,346 @@
|
|||||||
package com.vitorpamplona.amethyst.ui.screen
|
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.Arrangement
|
||||||
|
import androidx.compose.foundation.layout.Box
|
||||||
import androidx.compose.foundation.layout.Column
|
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.fillMaxHeight
|
||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
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.Text
|
||||||
import androidx.compose.material.rememberScaffoldState
|
|
||||||
import androidx.compose.runtime.Composable
|
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.runtime.rememberCoroutineScope
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
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 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
|
@Composable
|
||||||
fun ProfileScreen(accountViewModel: AccountViewModel) {
|
fun ProfileScreen(userId: String?, accountViewModel: AccountViewModel, navController: NavController) {
|
||||||
val state = rememberScaffoldState()
|
val accountState by accountViewModel.accountLiveData.observeAsState()
|
||||||
val scope = rememberCoroutineScope()
|
val account = accountState?.account
|
||||||
|
|
||||||
Column(
|
val accountUserState by accountViewModel.userLiveData.observeAsState()
|
||||||
Modifier
|
val accountUser = accountUserState?.user
|
||||||
.fillMaxHeight()
|
|
||||||
.fillMaxWidth(),
|
val clipboardManager = LocalClipboardManager.current
|
||||||
horizontalAlignment = Alignment.CenterHorizontally,
|
|
||||||
verticalArrangement = Arrangement.Center,
|
if (userId != null && account != null && accountUser != null) {
|
||||||
) {
|
DisposableEffect(account) {
|
||||||
Text("Profile Screen")
|
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 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()) {
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import androidx.compose.foundation.layout.Column
|
|||||||
import androidx.compose.foundation.layout.fillMaxHeight
|
import androidx.compose.foundation.layout.fillMaxHeight
|
||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.DisposableEffect
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.runtime.livedata.observeAsState
|
import androidx.compose.runtime.livedata.observeAsState
|
||||||
import androidx.compose.ui.Modifier
|
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) {
|
fun ThreadScreen(noteId: String?, accountViewModel: AccountViewModel, navController: NavController) {
|
||||||
val account by accountViewModel.accountLiveData.observeAsState()
|
val account by accountViewModel.accountLiveData.observeAsState()
|
||||||
|
|
||||||
|
DisposableEffect(account) {
|
||||||
|
onDispose {
|
||||||
|
NostrThreadDataSource.stop()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (account != null && noteId != null) {
|
if (account != null && noteId != null) {
|
||||||
NostrThreadDataSource.loadThread(noteId)
|
NostrThreadDataSource.loadThread(noteId)
|
||||||
|
|
||||||
|
|||||||
@@ -43,7 +43,6 @@ import androidx.compose.ui.text.style.TextDecoration
|
|||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.compose.ui.unit.sp
|
import androidx.compose.ui.unit.sp
|
||||||
import com.vitorpamplona.amethyst.R
|
import com.vitorpamplona.amethyst.R
|
||||||
import com.vitorpamplona.amethyst.ui.theme.Purple700
|
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun LoginPage(accountViewModel: AccountStateViewModel) {
|
fun LoginPage(accountViewModel: AccountStateViewModel) {
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ buildscript {
|
|||||||
compose_ui_version = '1.3.3'
|
compose_ui_version = '1.3.3'
|
||||||
nav_version = "2.5.3"
|
nav_version = "2.5.3"
|
||||||
room_version = "2.4.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.
|
}// Top-level build file where you can add configuration options common to all sub-projects/modules.
|
||||||
plugins {
|
plugins {
|
||||||
|
|||||||
Reference in New Issue
Block a user