mirror of
https://github.com/vitorpamplona/amethyst.git
synced 2025-09-27 15:46:18 +02:00
Private message support
This commit is contained in:
@@ -5,6 +5,8 @@ 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.Persona
|
import nostr.postr.Persona
|
||||||
|
import nostr.postr.Utils
|
||||||
|
import nostr.postr.events.PrivateDmEvent
|
||||||
import nostr.postr.events.TextNoteEvent
|
import nostr.postr.events.TextNoteEvent
|
||||||
import nostr.postr.toHex
|
import nostr.postr.toHex
|
||||||
|
|
||||||
@@ -78,6 +80,48 @@ class Account(val loggedIn: Persona) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun sendPrivateMeesage(message: String, toUser: String, replyingTo: Note? = null) {
|
||||||
|
if (!isWriteable()) return
|
||||||
|
val user = LocalCache.users[toUser] ?: return
|
||||||
|
|
||||||
|
val signedEvent = PrivateDmEvent.create(
|
||||||
|
recipientPubKey = user.pubkey,
|
||||||
|
publishedRecipientPubKey = user.pubkey,
|
||||||
|
msg = message,
|
||||||
|
privateKey = loggedIn.privKey!!,
|
||||||
|
advertiseNip18 = false
|
||||||
|
)
|
||||||
|
Client.send(signedEvent)
|
||||||
|
LocalCache.consume(signedEvent)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun decryptContent(note: Note): String? {
|
||||||
|
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)
|
||||||
|
|
||||||
|
return try {
|
||||||
|
val retVal = Utils.decrypt(event.content, sharedSecret)
|
||||||
|
|
||||||
|
if (retVal.startsWith(PrivateDmEvent.nip18Advertisement)) {
|
||||||
|
retVal.substring(16)
|
||||||
|
} else {
|
||||||
|
retVal
|
||||||
|
}
|
||||||
|
|
||||||
|
} catch (e: Exception) {
|
||||||
|
e.printStackTrace()
|
||||||
|
null
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
event?.content
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Observers line up here.
|
// Observers line up here.
|
||||||
val live: AccountLiveData = AccountLiveData(this)
|
val live: AccountLiveData = AccountLiveData(this)
|
||||||
|
|
||||||
|
@@ -115,9 +115,9 @@ object LocalCache {
|
|||||||
|
|
||||||
fun consume(event: ContactListEvent) {
|
fun consume(event: ContactListEvent) {
|
||||||
val user = getOrCreateUser(event.pubKey)
|
val user = getOrCreateUser(event.pubKey)
|
||||||
//Log.d("CL", "${user.toBestDisplayName()} ${event.follows}")
|
|
||||||
|
|
||||||
if (event.createdAt > user.updatedFollowsAt) {
|
if (event.createdAt > user.updatedFollowsAt) {
|
||||||
|
//Log.d("CL", "${user.toBestDisplayName()} ${event.follows.size}")
|
||||||
user.updateFollows(
|
user.updateFollows(
|
||||||
event.follows.map {
|
event.follows.map {
|
||||||
try {
|
try {
|
||||||
@@ -138,7 +138,25 @@ object LocalCache {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun consume(event: PrivateDmEvent) {
|
fun consume(event: PrivateDmEvent) {
|
||||||
//Log.d("PM", event.toJson())
|
val note = getOrCreateNote(event.id.toHex())
|
||||||
|
|
||||||
|
// Already processed this event.
|
||||||
|
if (note.event != null) return
|
||||||
|
|
||||||
|
val author = getOrCreateUser(event.pubKey)
|
||||||
|
val recipient = event.recipientPubKey?.let { getOrCreateUser(it) }
|
||||||
|
|
||||||
|
//Log.d("PM", "${author.toBestDisplayName()} to ${recipient?.toBestDisplayName()}")
|
||||||
|
|
||||||
|
val repliesTo = event.tags.filter { it.firstOrNull() == "e" }.mapNotNull { it.getOrNull(1) }.map { getOrCreateNote(it) }.toMutableList()
|
||||||
|
val mentions = event.tags.filter { it.firstOrNull() == "p" }.mapNotNull { it.getOrNull(1) }.map { getOrCreateUser(decodePublicKey(it)) }
|
||||||
|
|
||||||
|
note.loadEvent(event, author, mentions, repliesTo)
|
||||||
|
|
||||||
|
if (recipient != null) {
|
||||||
|
author.addMessage(recipient, note)
|
||||||
|
recipient.addMessage(author, note)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun consume(event: DeletionEvent) {
|
fun consume(event: DeletionEvent) {
|
||||||
|
@@ -4,6 +4,7 @@ import androidx.lifecycle.LiveData
|
|||||||
import com.vitorpamplona.amethyst.service.NostrSingleUserDataSource
|
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
|
||||||
|
|
||||||
class User(val pubkey: ByteArray) {
|
class User(val pubkey: ByteArray) {
|
||||||
val pubkeyHex = pubkey.toHexKey()
|
val pubkeyHex = pubkey.toHexKey()
|
||||||
@@ -20,6 +21,8 @@ class User(val pubkey: ByteArray) {
|
|||||||
|
|
||||||
val followers = Collections.synchronizedSet(mutableSetOf<User>())
|
val followers = Collections.synchronizedSet(mutableSetOf<User>())
|
||||||
|
|
||||||
|
val messages = ConcurrentHashMap<User, MutableSet<Note>>()
|
||||||
|
|
||||||
fun toBestDisplayName(): String {
|
fun toBestDisplayName(): String {
|
||||||
return bestDisplayName() ?: bestUsername() ?: pubkeyDisplayHex
|
return bestDisplayName() ?: bestUsername() ?: pubkeyDisplayHex
|
||||||
}
|
}
|
||||||
@@ -46,6 +49,20 @@ class User(val pubkey: ByteArray) {
|
|||||||
user.followers.remove(this)
|
user.followers.remove(this)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Synchronized
|
||||||
|
fun getOrCreateChannel(user: User): MutableSet<Note> {
|
||||||
|
return messages[user] ?: run {
|
||||||
|
val channel = mutableSetOf<Note>()
|
||||||
|
messages[user] = channel
|
||||||
|
channel
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun addMessage(user: User, msg: Note) {
|
||||||
|
getOrCreateChannel(user).add(msg)
|
||||||
|
live.refresh()
|
||||||
|
}
|
||||||
|
|
||||||
fun updateFollows(newFollows: List<User>, updateAt: Long) {
|
fun updateFollows(newFollows: List<User>, updateAt: Long) {
|
||||||
val toBeAdded = newFollows - follows
|
val toBeAdded = newFollows - follows
|
||||||
val toBeRemoved = follows - newFollows
|
val toBeRemoved = follows - newFollows
|
||||||
|
@@ -0,0 +1,45 @@
|
|||||||
|
package com.vitorpamplona.amethyst.service
|
||||||
|
|
||||||
|
import com.vitorpamplona.amethyst.model.Account
|
||||||
|
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.PrivateDmEvent
|
||||||
|
|
||||||
|
object NostrChatRoomDataSource: NostrDataSource("ChatroomFeed") {
|
||||||
|
lateinit var account: Account
|
||||||
|
var withUser: User? = null
|
||||||
|
|
||||||
|
fun loadMessagesBetween(accountIn: Account, userId: String) {
|
||||||
|
account = accountIn
|
||||||
|
withUser = LocalCache.users[userId]
|
||||||
|
}
|
||||||
|
|
||||||
|
fun createMessagesToMeFilter() = JsonFilter(
|
||||||
|
kinds = listOf(PrivateDmEvent.kind),
|
||||||
|
authors = withUser?.let { listOf(it.pubkeyHex) },
|
||||||
|
tags = mapOf("p" to listOf(account.userProfile().pubkeyHex))
|
||||||
|
)
|
||||||
|
|
||||||
|
fun createMessagesFromMeFilter() = JsonFilter(
|
||||||
|
kinds = listOf(PrivateDmEvent.kind),
|
||||||
|
authors = listOf(account.userProfile().pubkeyHex),
|
||||||
|
tags = withUser?.let { mapOf("p" to listOf(it.pubkeyHex)) }
|
||||||
|
)
|
||||||
|
|
||||||
|
val incomingChannel = requestNewChannel()
|
||||||
|
val outgoingChannel = requestNewChannel()
|
||||||
|
|
||||||
|
// returns the last Note of each user.
|
||||||
|
override fun feed(): List<Note> {
|
||||||
|
val messages = account.userProfile().messages[withUser]
|
||||||
|
|
||||||
|
return messages?.sortedBy { it.event!!.createdAt } ?: emptyList()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun updateChannelFilters() {
|
||||||
|
incomingChannel.filter = createMessagesToMeFilter()
|
||||||
|
outgoingChannel.filter = createMessagesFromMeFilter()
|
||||||
|
}
|
||||||
|
}
|
@@ -0,0 +1,38 @@
|
|||||||
|
package com.vitorpamplona.amethyst.service
|
||||||
|
|
||||||
|
import com.vitorpamplona.amethyst.model.Account
|
||||||
|
import com.vitorpamplona.amethyst.model.Note
|
||||||
|
import nostr.postr.JsonFilter
|
||||||
|
import nostr.postr.events.PrivateDmEvent
|
||||||
|
|
||||||
|
object NostrChatroomListDataSource: NostrDataSource("MailBoxFeed") {
|
||||||
|
lateinit var account: Account
|
||||||
|
|
||||||
|
fun createMessagesToMeFilter() = JsonFilter(
|
||||||
|
kinds = listOf(PrivateDmEvent.kind),
|
||||||
|
tags = mapOf("p" to listOf(account.userProfile().pubkeyHex))
|
||||||
|
)
|
||||||
|
|
||||||
|
fun createMessagesFromMeFilter() = JsonFilter(
|
||||||
|
kinds = listOf(PrivateDmEvent.kind),
|
||||||
|
authors = listOf(account.userProfile().pubkeyHex)
|
||||||
|
)
|
||||||
|
|
||||||
|
val incomingChannel = requestNewChannel()
|
||||||
|
val outgoingChannel = requestNewChannel()
|
||||||
|
|
||||||
|
// returns the last Note of each user.
|
||||||
|
override fun feed(): List<Note> {
|
||||||
|
val messages = account.userProfile().messages
|
||||||
|
val messagingWith = messages.keys().toList()
|
||||||
|
|
||||||
|
return messagingWith.mapNotNull {
|
||||||
|
messages[it]?.sortedBy { it.event?.createdAt }?.last { it.event != null }
|
||||||
|
}.sortedBy { it.event?.createdAt }.reversed()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun updateChannelFilters() {
|
||||||
|
incomingChannel.filter = createMessagesToMeFilter()
|
||||||
|
outgoingChannel.filter = createMessagesFromMeFilter()
|
||||||
|
}
|
||||||
|
}
|
@@ -8,7 +8,7 @@ object NostrNotificationDataSource: NostrDataSource("GlobalFeed") {
|
|||||||
lateinit var account: Account
|
lateinit var account: Account
|
||||||
|
|
||||||
fun createGlobalFilter() = JsonFilter(
|
fun createGlobalFilter() = JsonFilter(
|
||||||
since = System.currentTimeMillis() / 1000 - (60 * 60 * 24 * 7), // 2 days
|
since = System.currentTimeMillis() / 1000 - (60 * 60 * 24 * 7), // 7 days
|
||||||
tags = mapOf("p" to listOf(account.userProfile().pubkeyHex).filterNotNull())
|
tags = mapOf("p" to listOf(account.userProfile().pubkeyHex).filterNotNull())
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@@ -62,7 +62,6 @@ class Relay(
|
|||||||
it.onSendResponse(this@Relay, msg[1].asString, msg[2].asBoolean, msg[3].asString)
|
it.onSendResponse(this@Relay, msg[1].asString, msg[2].asBoolean, msg[3].asString)
|
||||||
}
|
}
|
||||||
else -> listeners.forEach {
|
else -> listeners.forEach {
|
||||||
println("else: " + text)
|
|
||||||
it.onError(
|
it.onError(
|
||||||
this@Relay,
|
this@Relay,
|
||||||
channel,
|
channel,
|
||||||
|
@@ -8,12 +8,12 @@ import androidx.compose.material.MaterialTheme
|
|||||||
import androidx.compose.material.Surface
|
import androidx.compose.material.Surface
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.lifecycle.viewmodel.compose.viewModel
|
import androidx.lifecycle.viewmodel.compose.viewModel
|
||||||
import com.google.android.exoplayer2.util.Util
|
|
||||||
import com.vitorpamplona.amethyst.KeyStorage
|
import com.vitorpamplona.amethyst.KeyStorage
|
||||||
import com.vitorpamplona.amethyst.service.NostrAccountDataSource
|
import com.vitorpamplona.amethyst.service.NostrAccountDataSource
|
||||||
import com.vitorpamplona.amethyst.service.NostrGlobalDataSource
|
import com.vitorpamplona.amethyst.service.NostrGlobalDataSource
|
||||||
import com.vitorpamplona.amethyst.service.NostrHomeDataSource
|
import com.vitorpamplona.amethyst.service.NostrHomeDataSource
|
||||||
import com.vitorpamplona.amethyst.service.NostrNotificationDataSource
|
import com.vitorpamplona.amethyst.service.NostrNotificationDataSource
|
||||||
|
import com.vitorpamplona.amethyst.service.NostrChatroomListDataSource
|
||||||
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
|
||||||
@@ -51,6 +51,7 @@ class MainActivity : ComponentActivity() {
|
|||||||
override fun onPause() {
|
override fun onPause() {
|
||||||
NostrAccountDataSource.stop()
|
NostrAccountDataSource.stop()
|
||||||
NostrHomeDataSource.stop()
|
NostrHomeDataSource.stop()
|
||||||
|
NostrChatroomListDataSource.stop()
|
||||||
|
|
||||||
NostrGlobalDataSource.stop()
|
NostrGlobalDataSource.stop()
|
||||||
NostrNotificationDataSource.stop()
|
NostrNotificationDataSource.stop()
|
||||||
|
@@ -107,7 +107,8 @@ fun NewPostView(onClose: () -> Unit, replyingTo: Note? = null, account: Account)
|
|||||||
onPost = {
|
onPost = {
|
||||||
postViewModel.sendPost()
|
postViewModel.sendPost()
|
||||||
onClose()
|
onClose()
|
||||||
}
|
},
|
||||||
|
postViewModel.message.isNotBlank()
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -207,15 +208,17 @@ fun CloseButton(onCancel: () -> Unit) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun PostButton(onPost: () -> Unit = {}) {
|
fun PostButton(onPost: () -> Unit = {}, isActive: Boolean) {
|
||||||
Button(
|
Button(
|
||||||
onClick = {
|
onClick = {
|
||||||
onPost()
|
if (isActive) {
|
||||||
|
onPost()
|
||||||
|
}
|
||||||
},
|
},
|
||||||
shape = RoundedCornerShape(20.dp),
|
shape = RoundedCornerShape(20.dp),
|
||||||
colors = ButtonDefaults
|
colors = ButtonDefaults
|
||||||
.buttonColors(
|
.buttonColors(
|
||||||
backgroundColor = MaterialTheme.colors.primary
|
backgroundColor = if (isActive) MaterialTheme.colors.primary else Color.Gray
|
||||||
)
|
)
|
||||||
) {
|
) {
|
||||||
Text(text = "Post", color = Color.White)
|
Text(text = "Post", color = Color.White)
|
||||||
|
@@ -10,8 +10,9 @@ import androidx.navigation.NavType
|
|||||||
import androidx.navigation.compose.currentBackStackEntryAsState
|
import androidx.navigation.compose.currentBackStackEntryAsState
|
||||||
import androidx.navigation.navArgument
|
import androidx.navigation.navArgument
|
||||||
import com.vitorpamplona.amethyst.R
|
import com.vitorpamplona.amethyst.R
|
||||||
|
import com.vitorpamplona.amethyst.ui.screen.ChatroomScreen
|
||||||
import com.vitorpamplona.amethyst.ui.screen.HomeScreen
|
import com.vitorpamplona.amethyst.ui.screen.HomeScreen
|
||||||
import com.vitorpamplona.amethyst.ui.screen.MessageScreen
|
import com.vitorpamplona.amethyst.ui.screen.ChatroomListScreen
|
||||||
import com.vitorpamplona.amethyst.ui.screen.ThreadScreen
|
import com.vitorpamplona.amethyst.ui.screen.ThreadScreen
|
||||||
import com.vitorpamplona.amethyst.ui.screen.NotificationScreen
|
import com.vitorpamplona.amethyst.ui.screen.NotificationScreen
|
||||||
import com.vitorpamplona.amethyst.ui.screen.ProfileScreen
|
import com.vitorpamplona.amethyst.ui.screen.ProfileScreen
|
||||||
@@ -28,7 +29,7 @@ sealed class Route(
|
|||||||
object Home : Route("Home", R.drawable.ic_home, buildScreen = { acc, nav -> { _ -> HomeScreen(acc, nav) } })
|
object Home : Route("Home", R.drawable.ic_home, buildScreen = { acc, nav -> { _ -> HomeScreen(acc, nav) } })
|
||||||
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 -> { _ -> MessageScreen(acc) }})
|
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 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 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 Topics : Route("Topics", R.drawable.ic_topics, buildScreen = { acc, nav -> { _ -> ProfileScreen(acc) }})
|
||||||
@@ -39,6 +40,11 @@ sealed class Route(
|
|||||||
arguments = listOf(navArgument("id") { type = NavType.StringType } ),
|
arguments = listOf(navArgument("id") { type = NavType.StringType } ),
|
||||||
buildScreen = { acc, nav -> { ThreadScreen(it.arguments?.getString("id"), acc, nav) }}
|
buildScreen = { acc, nav -> { ThreadScreen(it.arguments?.getString("id"), acc, nav) }}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
object Room : Route("Room/{id}", R.drawable.ic_moments,
|
||||||
|
arguments = listOf(navArgument("id") { type = NavType.StringType } ),
|
||||||
|
buildScreen = { acc, nav -> { ChatroomScreen(it.arguments?.getString("id"), acc, nav) }}
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
val Routes = listOf(
|
val Routes = listOf(
|
||||||
@@ -56,7 +62,8 @@ val Routes = listOf(
|
|||||||
Route.Moments,
|
Route.Moments,
|
||||||
|
|
||||||
//inner
|
//inner
|
||||||
Route.Note
|
Route.Note,
|
||||||
|
Route.Room
|
||||||
)
|
)
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
|
@@ -0,0 +1,98 @@
|
|||||||
|
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.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.Note
|
||||||
|
import com.vitorpamplona.amethyst.ui.components.RichTextViewer
|
||||||
|
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun ChatroomCompose(baseNote: Note, accountViewModel: AccountViewModel, navController: NavController) {
|
||||||
|
val noteState by baseNote.live.observeAsState()
|
||||||
|
val note = noteState?.note
|
||||||
|
|
||||||
|
val accountUserState by accountViewModel.userLiveData.observeAsState()
|
||||||
|
val accountUser = accountUserState?.user
|
||||||
|
|
||||||
|
if (note?.event == null) {
|
||||||
|
BlankNote(Modifier)
|
||||||
|
} else {
|
||||||
|
val authorState by note.author!!.live.observeAsState()
|
||||||
|
val author = authorState?.user
|
||||||
|
|
||||||
|
val replyAuthorBase = note.mentions?.first()
|
||||||
|
|
||||||
|
var userToComposeOn = author
|
||||||
|
|
||||||
|
if ( replyAuthorBase != null ) {
|
||||||
|
val replyAuthorState by replyAuthorBase.live.observeAsState()
|
||||||
|
val replyAuthor = replyAuthorState?.user
|
||||||
|
|
||||||
|
if (author == accountUser) {
|
||||||
|
userToComposeOn = replyAuthor
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Column(modifier =
|
||||||
|
Modifier.clickable(
|
||||||
|
onClick = { navController.navigate("Room/${userToComposeOn?.pubkeyHex}") }
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
Row(
|
||||||
|
modifier = Modifier
|
||||||
|
.padding(
|
||||||
|
start = 12.dp,
|
||||||
|
end = 12.dp,
|
||||||
|
top = 10.dp)
|
||||||
|
) {
|
||||||
|
|
||||||
|
AsyncImage(
|
||||||
|
model = userToComposeOn?.profilePicture(),
|
||||||
|
contentDescription = "Profile Image",
|
||||||
|
modifier = Modifier
|
||||||
|
.width(55.dp)
|
||||||
|
.clip(shape = CircleShape)
|
||||||
|
)
|
||||||
|
|
||||||
|
Column(modifier = Modifier.padding(start = 10.dp)) {
|
||||||
|
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||||
|
if (userToComposeOn != null)
|
||||||
|
UserDisplay(userToComposeOn)
|
||||||
|
|
||||||
|
Text(
|
||||||
|
timeAgo(note.event?.createdAt),
|
||||||
|
color = MaterialTheme.colors.onSurface.copy(alpha = 0.32f)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
val eventContent = accountViewModel.decrypt(note)
|
||||||
|
if (eventContent != null)
|
||||||
|
RichTextViewer(eventContent.take(100), note.event?.tags, note, accountViewModel, navController)
|
||||||
|
else
|
||||||
|
RichTextViewer("Referenced event not found", note.event?.tags, note, accountViewModel, navController)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Divider(
|
||||||
|
modifier = Modifier.padding(top = 10.dp),
|
||||||
|
thickness = 0.25.dp
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@@ -0,0 +1,122 @@
|
|||||||
|
package com.vitorpamplona.amethyst.ui.note
|
||||||
|
|
||||||
|
import androidx.compose.foundation.border
|
||||||
|
import androidx.compose.foundation.clickable
|
||||||
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
|
import androidx.compose.foundation.layout.Column
|
||||||
|
import androidx.compose.foundation.layout.Row
|
||||||
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.foundation.shape.CircleShape
|
||||||
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
|
import androidx.compose.material.MaterialTheme
|
||||||
|
import androidx.compose.material.Surface
|
||||||
|
import androidx.compose.material.Text
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.runtime.livedata.observeAsState
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.graphics.Color
|
||||||
|
import androidx.compose.ui.graphics.Shape
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import androidx.navigation.NavController
|
||||||
|
import com.vitorpamplona.amethyst.model.Note
|
||||||
|
import com.vitorpamplona.amethyst.ui.components.RichTextViewer
|
||||||
|
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
||||||
|
|
||||||
|
val ChatBubbleShapeMe = RoundedCornerShape(20.dp, 20.dp, 3.dp, 20.dp)
|
||||||
|
val ChatBubbleShapeThem = RoundedCornerShape(20.dp, 20.dp, 20.dp, 3.dp)
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun ChatroomMessageCompose(baseNote: Note, accountViewModel: AccountViewModel, navController: NavController) {
|
||||||
|
val noteState by baseNote.live.observeAsState()
|
||||||
|
val note = noteState?.note
|
||||||
|
|
||||||
|
val accountUserState by accountViewModel.userLiveData.observeAsState()
|
||||||
|
val accountUser = accountUserState?.user
|
||||||
|
|
||||||
|
if (note?.event == null) {
|
||||||
|
BlankNote(Modifier)
|
||||||
|
} else {
|
||||||
|
val authorState by note.author!!.live.observeAsState()
|
||||||
|
val author = authorState?.user
|
||||||
|
|
||||||
|
Column(modifier =
|
||||||
|
Modifier.clickable(
|
||||||
|
onClick = { navController.navigate("User/${note.idHex}") }
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
var backgroundBubbleColor: Color
|
||||||
|
var alignment: Arrangement.Horizontal
|
||||||
|
var shape: Shape
|
||||||
|
|
||||||
|
if (author == accountUser) {
|
||||||
|
backgroundBubbleColor = MaterialTheme.colors.primary.copy(alpha = 0.32f)
|
||||||
|
alignment = Arrangement.End
|
||||||
|
shape = ChatBubbleShapeMe
|
||||||
|
} else {
|
||||||
|
backgroundBubbleColor = MaterialTheme.colors.onSurface.copy(alpha = 0.12f)
|
||||||
|
alignment = Arrangement.Start
|
||||||
|
shape = ChatBubbleShapeThem
|
||||||
|
}
|
||||||
|
|
||||||
|
Row(
|
||||||
|
horizontalArrangement = alignment,
|
||||||
|
modifier = Modifier.fillMaxWidth()
|
||||||
|
.padding(
|
||||||
|
start = 12.dp,
|
||||||
|
end = 12.dp,
|
||||||
|
top = 10.dp)
|
||||||
|
) {
|
||||||
|
|
||||||
|
Row(
|
||||||
|
verticalAlignment = Alignment.CenterVertically
|
||||||
|
) {
|
||||||
|
Surface(
|
||||||
|
color = backgroundBubbleColor,
|
||||||
|
shape = shape
|
||||||
|
) {
|
||||||
|
Column(
|
||||||
|
modifier = Modifier.padding(10.dp),
|
||||||
|
) {
|
||||||
|
Row(
|
||||||
|
verticalAlignment = Alignment.CenterVertically
|
||||||
|
) {
|
||||||
|
val eventContent = accountViewModel.decrypt(note)
|
||||||
|
if (eventContent != null)
|
||||||
|
RichTextViewer(
|
||||||
|
eventContent,
|
||||||
|
note.event?.tags,
|
||||||
|
note,
|
||||||
|
accountViewModel,
|
||||||
|
navController
|
||||||
|
)
|
||||||
|
else
|
||||||
|
RichTextViewer(
|
||||||
|
"Could Not decrypt the message",
|
||||||
|
note.event?.tags,
|
||||||
|
note,
|
||||||
|
accountViewModel,
|
||||||
|
navController
|
||||||
|
)
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
Row(
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
horizontalArrangement = alignment
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
timeAgoLong(note.event?.createdAt),
|
||||||
|
color = MaterialTheme.colors.onSurface.copy(alpha = 0.32f)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@@ -1,10 +1,8 @@
|
|||||||
package com.vitorpamplona.amethyst.ui.note
|
package com.vitorpamplona.amethyst.ui.note
|
||||||
|
|
||||||
import android.text.format.DateUtils.getRelativeTimeSpanString
|
|
||||||
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
|
||||||
@@ -29,10 +27,8 @@ import androidx.compose.ui.draw.clip
|
|||||||
import androidx.compose.ui.platform.LocalClipboardManager
|
import androidx.compose.ui.platform.LocalClipboardManager
|
||||||
import androidx.compose.ui.text.AnnotatedString
|
import androidx.compose.ui.text.AnnotatedString
|
||||||
import androidx.compose.ui.text.font.FontWeight
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
import androidx.compose.ui.unit.DpOffset
|
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.navigation.NavController
|
import androidx.navigation.NavController
|
||||||
import androidx.navigation.compose.rememberNavController
|
|
||||||
import coil.compose.AsyncImage
|
import coil.compose.AsyncImage
|
||||||
import com.vitorpamplona.amethyst.model.Note
|
import com.vitorpamplona.amethyst.model.Note
|
||||||
import com.vitorpamplona.amethyst.model.toNote
|
import com.vitorpamplona.amethyst.model.toNote
|
@@ -18,6 +18,7 @@ fun timeAgo(mills: Long?): String {
|
|||||||
return " • " + humanReadable
|
return " • " + humanReadable
|
||||||
.replace(" hr. ago", "h")
|
.replace(" hr. ago", "h")
|
||||||
.replace(" min. ago", "m")
|
.replace(" min. ago", "m")
|
||||||
|
.replace(" days. ago", "d")
|
||||||
}
|
}
|
||||||
|
|
||||||
fun timeAgoLong(mills: Long?): String {
|
fun timeAgoLong(mills: Long?): String {
|
||||||
|
@@ -9,14 +9,21 @@ import com.vitorpamplona.amethyst.model.User
|
|||||||
@Composable
|
@Composable
|
||||||
fun UserDisplay(user: User) {
|
fun UserDisplay(user: User) {
|
||||||
if (user.bestUsername() != null || user.bestDisplayName() != null) {
|
if (user.bestUsername() != null || user.bestDisplayName() != null) {
|
||||||
Text(
|
if (user.bestDisplayName().isNullOrBlank()) {
|
||||||
user.bestDisplayName() ?: "",
|
Text(
|
||||||
fontWeight = FontWeight.Bold,
|
"@${(user.bestUsername() ?: "")}",
|
||||||
)
|
fontWeight = FontWeight.Bold,
|
||||||
Text(
|
)
|
||||||
"@${(user.bestUsername() ?: "")}",
|
} else {
|
||||||
color = MaterialTheme.colors.onSurface.copy(alpha = 0.32f),
|
Text(
|
||||||
)
|
user.bestDisplayName() ?: "",
|
||||||
|
fontWeight = FontWeight.Bold,
|
||||||
|
)
|
||||||
|
Text(
|
||||||
|
"@${(user.bestUsername() ?: "")}",
|
||||||
|
color = MaterialTheme.colors.onSurface.copy(alpha = 0.32f),
|
||||||
|
)
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
Text(
|
Text(
|
||||||
user.pubkeyDisplayHex,
|
user.pubkeyDisplayHex,
|
||||||
|
@@ -8,6 +8,7 @@ import com.vitorpamplona.amethyst.service.NostrAccountDataSource
|
|||||||
import com.vitorpamplona.amethyst.service.NostrGlobalDataSource
|
import com.vitorpamplona.amethyst.service.NostrGlobalDataSource
|
||||||
import com.vitorpamplona.amethyst.service.NostrHomeDataSource
|
import com.vitorpamplona.amethyst.service.NostrHomeDataSource
|
||||||
import com.vitorpamplona.amethyst.service.NostrNotificationDataSource
|
import com.vitorpamplona.amethyst.service.NostrNotificationDataSource
|
||||||
|
import com.vitorpamplona.amethyst.service.NostrChatroomListDataSource
|
||||||
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
|
||||||
@@ -59,6 +60,7 @@ class AccountStateViewModel(private val encryptedPreferences: EncryptedSharedPre
|
|||||||
NostrAccountDataSource.account = loggedIn
|
NostrAccountDataSource.account = loggedIn
|
||||||
NostrHomeDataSource.account = loggedIn
|
NostrHomeDataSource.account = loggedIn
|
||||||
NostrNotificationDataSource.account = loggedIn
|
NostrNotificationDataSource.account = loggedIn
|
||||||
|
NostrChatroomListDataSource.account = loggedIn
|
||||||
|
|
||||||
NostrAccountDataSource.start()
|
NostrAccountDataSource.start()
|
||||||
NostrGlobalDataSource.start()
|
NostrGlobalDataSource.start()
|
||||||
@@ -67,6 +69,7 @@ class AccountStateViewModel(private val encryptedPreferences: EncryptedSharedPre
|
|||||||
NostrSingleEventDataSource.start()
|
NostrSingleEventDataSource.start()
|
||||||
NostrSingleUserDataSource.start()
|
NostrSingleUserDataSource.start()
|
||||||
NostrThreadDataSource.start()
|
NostrThreadDataSource.start()
|
||||||
|
NostrChatroomListDataSource.start()
|
||||||
}
|
}
|
||||||
|
|
||||||
fun newKey() {
|
fun newKey() {
|
||||||
|
@@ -0,0 +1,98 @@
|
|||||||
|
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.layout.Row
|
||||||
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
|
import androidx.compose.foundation.layout.height
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.foundation.lazy.LazyColumn
|
||||||
|
import androidx.compose.foundation.lazy.itemsIndexed
|
||||||
|
import androidx.compose.foundation.lazy.rememberLazyListState
|
||||||
|
import androidx.compose.material.Divider
|
||||||
|
import androidx.compose.material.MaterialTheme
|
||||||
|
import androidx.compose.material.Surface
|
||||||
|
import androidx.compose.material.Text
|
||||||
|
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.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import androidx.lifecycle.compose.ExperimentalLifecycleComposeApi
|
||||||
|
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.model.Note
|
||||||
|
import com.vitorpamplona.amethyst.ui.components.RichTextViewer
|
||||||
|
import com.vitorpamplona.amethyst.ui.note.ChatroomMessageCompose
|
||||||
|
import com.vitorpamplona.amethyst.ui.note.timeAgoLong
|
||||||
|
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
||||||
|
|
||||||
|
@OptIn(ExperimentalLifecycleComposeApi::class)
|
||||||
|
@Composable
|
||||||
|
fun ChatroomFeedView(userId: String, viewModel: FeedViewModel, 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 FeedState.Empty -> {
|
||||||
|
FeedEmpty {
|
||||||
|
isRefreshing = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
is FeedState.FeedError -> {
|
||||||
|
FeedError(state.errorMessage) {
|
||||||
|
isRefreshing = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
is FeedState.Loaded -> {
|
||||||
|
LazyColumn(
|
||||||
|
contentPadding = PaddingValues(
|
||||||
|
top = 10.dp,
|
||||||
|
bottom = 10.dp
|
||||||
|
),
|
||||||
|
state = listState
|
||||||
|
) {
|
||||||
|
var previousDate: String = ""
|
||||||
|
itemsIndexed(state.feed, key = { _, item -> item.idHex }) { index, item ->
|
||||||
|
ChatroomMessageCompose(item, accountViewModel = accountViewModel, navController = navController)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
LaunchedEffect(Unit) {
|
||||||
|
listState.animateScrollToItem(state.feed.size-1, 0)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
FeedState.Loading -> {
|
||||||
|
LoadingFeed()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@@ -0,0 +1,80 @@
|
|||||||
|
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.ExperimentalLifecycleComposeApi
|
||||||
|
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.ChatroomCompose
|
||||||
|
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
||||||
|
|
||||||
|
@OptIn(ExperimentalLifecycleComposeApi::class)
|
||||||
|
@Composable
|
||||||
|
fun ChatroomListFeedView(viewModel: FeedViewModel, 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 FeedState.Empty -> {
|
||||||
|
FeedEmpty {
|
||||||
|
isRefreshing = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
is FeedState.FeedError -> {
|
||||||
|
FeedError(state.errorMessage) {
|
||||||
|
isRefreshing = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
is FeedState.Loaded -> {
|
||||||
|
LazyColumn(
|
||||||
|
contentPadding = PaddingValues(
|
||||||
|
top = 10.dp,
|
||||||
|
bottom = 10.dp
|
||||||
|
),
|
||||||
|
state = listState
|
||||||
|
) {
|
||||||
|
itemsIndexed(state.feed, key = { _, item -> item.idHex }) { index, item ->
|
||||||
|
ChatroomCompose(item, accountViewModel = accountViewModel, navController = navController)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
FeedState.Loading -> {
|
||||||
|
LoadingFeed()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@@ -133,57 +133,4 @@ fun FeedEmpty(onRefresh: () -> Unit) {
|
|||||||
Text(text = "Refresh")
|
Text(text = "Refresh")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
// Bosted code to be deleted:
|
|
||||||
|
|
||||||
/*
|
|
||||||
|
|
||||||
Boosted By: removed because it was ugly
|
|
||||||
|
|
||||||
if (item.event is RepostEvent) {
|
|
||||||
Row(
|
|
||||||
modifier = Modifier.padding(
|
|
||||||
start = 12.dp,
|
|
||||||
end = 12.dp,
|
|
||||||
bottom = 8.dp
|
|
||||||
),
|
|
||||||
verticalAlignment = Alignment.CenterVertically
|
|
||||||
) {
|
|
||||||
Icon(
|
|
||||||
painter = painterResource(R.drawable.ic_retweet),
|
|
||||||
null,
|
|
||||||
modifier = Modifier.size(20.dp),
|
|
||||||
tint = Color.Gray
|
|
||||||
)
|
|
||||||
Text(
|
|
||||||
text = "Boosted by ${item.author.toBestDisplayName()}",
|
|
||||||
modifier = Modifier.padding(start = 10.dp),
|
|
||||||
fontWeight = FontWeight.Bold,
|
|
||||||
color = Color.Gray,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
val refNote = item.replyTo.firstOrNull()
|
|
||||||
if (refNote != null) {
|
|
||||||
NoteCompose(index, refNote)
|
|
||||||
} else {
|
|
||||||
Row(
|
|
||||||
modifier = Modifier.padding(
|
|
||||||
start = 40.dp,
|
|
||||||
end = 40.dp,
|
|
||||||
bottom = 25.dp,
|
|
||||||
top = 15.dp
|
|
||||||
),
|
|
||||||
verticalAlignment = Alignment.CenterVertically
|
|
||||||
) {
|
|
||||||
Text(
|
|
||||||
text = "Could not find referenced event",
|
|
||||||
modifier = Modifier.padding(30.dp),
|
|
||||||
color = Color.Gray,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
NoteCompose(index, item)
|
|
||||||
}*/
|
|
@@ -8,6 +8,7 @@ import com.vitorpamplona.amethyst.model.AccountState
|
|||||||
import com.vitorpamplona.amethyst.model.Note
|
import com.vitorpamplona.amethyst.model.Note
|
||||||
import com.vitorpamplona.amethyst.model.UserState
|
import com.vitorpamplona.amethyst.model.UserState
|
||||||
import com.vitorpamplona.amethyst.service.relays.Client
|
import com.vitorpamplona.amethyst.service.relays.Client
|
||||||
|
import nostr.postr.events.PrivateDmEvent
|
||||||
|
|
||||||
class AccountViewModel(private val account: Account): ViewModel() {
|
class AccountViewModel(private val account: Account): ViewModel() {
|
||||||
val accountLiveData: LiveData<AccountState> = Transformations.map(account.live) { it }
|
val accountLiveData: LiveData<AccountState> = Transformations.map(account.live) { it }
|
||||||
@@ -24,4 +25,8 @@ class AccountViewModel(private val account: Account): ViewModel() {
|
|||||||
fun broadcast(note: Note) {
|
fun broadcast(note: Note) {
|
||||||
account.broadcast(note)
|
account.broadcast(note)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun decrypt(note: Note): String? {
|
||||||
|
return account.decryptContent(note)
|
||||||
|
}
|
||||||
}
|
}
|
@@ -0,0 +1,47 @@
|
|||||||
|
package com.vitorpamplona.amethyst.ui.screen
|
||||||
|
|
||||||
|
import androidx.compose.foundation.clickable
|
||||||
|
import androidx.compose.foundation.layout.Column
|
||||||
|
import androidx.compose.foundation.layout.Row
|
||||||
|
import androidx.compose.foundation.layout.fillMaxHeight
|
||||||
|
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.lifecycle.viewmodel.compose.viewModel
|
||||||
|
import androidx.navigation.NavController
|
||||||
|
import coil.compose.AsyncImage
|
||||||
|
import com.vitorpamplona.amethyst.model.Note
|
||||||
|
import com.vitorpamplona.amethyst.service.NostrChatroomListDataSource
|
||||||
|
import com.vitorpamplona.amethyst.ui.components.RichTextViewer
|
||||||
|
import com.vitorpamplona.amethyst.ui.note.BlankNote
|
||||||
|
import com.vitorpamplona.amethyst.ui.note.UserDisplay
|
||||||
|
import com.vitorpamplona.amethyst.ui.note.timeAgo
|
||||||
|
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun ChatroomListScreen(accountViewModel: AccountViewModel, navController: NavController) {
|
||||||
|
val account by accountViewModel.accountLiveData.observeAsState()
|
||||||
|
|
||||||
|
if (account != null) {
|
||||||
|
val feedViewModel: FeedViewModel = viewModel { FeedViewModel( NostrChatroomListDataSource ) }
|
||||||
|
|
||||||
|
Column(Modifier.fillMaxHeight()) {
|
||||||
|
Column(
|
||||||
|
modifier = Modifier.padding(vertical = 0.dp)
|
||||||
|
) {
|
||||||
|
ChatroomListFeedView(feedViewModel, accountViewModel, navController)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@@ -0,0 +1,145 @@
|
|||||||
|
package com.vitorpamplona.amethyst.ui.screen
|
||||||
|
|
||||||
|
import androidx.compose.foundation.border
|
||||||
|
import androidx.compose.foundation.clickable
|
||||||
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
|
import androidx.compose.foundation.layout.Column
|
||||||
|
import androidx.compose.foundation.layout.Row
|
||||||
|
import androidx.compose.foundation.layout.fillMaxHeight
|
||||||
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.foundation.layout.width
|
||||||
|
import androidx.compose.foundation.shape.CircleShape
|
||||||
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
|
import androidx.compose.foundation.text.KeyboardOptions
|
||||||
|
import androidx.compose.material.Divider
|
||||||
|
import androidx.compose.material.MaterialTheme
|
||||||
|
import androidx.compose.material.OutlinedTextField
|
||||||
|
import androidx.compose.material.Text
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.runtime.livedata.observeAsState
|
||||||
|
import androidx.compose.runtime.mutableStateOf
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.draw.clip
|
||||||
|
import androidx.compose.ui.focus.focusRequester
|
||||||
|
import androidx.compose.ui.focus.onFocusChanged
|
||||||
|
import androidx.compose.ui.text.input.ImeAction
|
||||||
|
import androidx.compose.ui.text.input.KeyboardCapitalization
|
||||||
|
import androidx.compose.ui.text.input.KeyboardType
|
||||||
|
import androidx.compose.ui.text.input.TextFieldValue
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import androidx.lifecycle.viewmodel.compose.viewModel
|
||||||
|
import androidx.navigation.NavController
|
||||||
|
import coil.compose.AsyncImage
|
||||||
|
import com.vitorpamplona.amethyst.model.Note
|
||||||
|
import com.vitorpamplona.amethyst.model.User
|
||||||
|
import com.vitorpamplona.amethyst.service.NostrChatRoomDataSource
|
||||||
|
import com.vitorpamplona.amethyst.service.NostrThreadDataSource
|
||||||
|
import com.vitorpamplona.amethyst.ui.actions.NewPostViewModel
|
||||||
|
import com.vitorpamplona.amethyst.ui.actions.PostButton
|
||||||
|
import com.vitorpamplona.amethyst.ui.components.RichTextViewer
|
||||||
|
import com.vitorpamplona.amethyst.ui.note.BlankNote
|
||||||
|
import com.vitorpamplona.amethyst.ui.note.ChatroomCompose
|
||||||
|
import com.vitorpamplona.amethyst.ui.note.UserDisplay
|
||||||
|
import com.vitorpamplona.amethyst.ui.note.timeAgo
|
||||||
|
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun ChatroomScreen(userId: String?, accountViewModel: AccountViewModel, navController: NavController) {
|
||||||
|
val accountState by accountViewModel.accountLiveData.observeAsState()
|
||||||
|
val account = accountState?.account
|
||||||
|
|
||||||
|
if (account != null && userId != null) {
|
||||||
|
val newPost = remember { mutableStateOf(TextFieldValue("")) }
|
||||||
|
|
||||||
|
NostrChatRoomDataSource.loadMessagesBetween(account, userId)
|
||||||
|
|
||||||
|
val feedViewModel: FeedViewModel = viewModel { FeedViewModel( NostrChatRoomDataSource ) }
|
||||||
|
|
||||||
|
Column(Modifier.fillMaxHeight()) {
|
||||||
|
NostrChatRoomDataSource.withUser?.let {
|
||||||
|
ChatroomHeader(
|
||||||
|
it,
|
||||||
|
accountViewModel = accountViewModel,
|
||||||
|
navController = navController
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
Column(
|
||||||
|
modifier = Modifier.fillMaxHeight().padding(vertical = 0.dp).weight(1f, true)
|
||||||
|
) {
|
||||||
|
ChatroomFeedView(userId, feedViewModel, accountViewModel, navController)
|
||||||
|
}
|
||||||
|
|
||||||
|
//LAST ROW
|
||||||
|
Row(modifier = Modifier.padding(10.dp).fillMaxWidth(),
|
||||||
|
horizontalArrangement = Arrangement.SpaceBetween,
|
||||||
|
verticalAlignment = Alignment.CenterVertically
|
||||||
|
) {
|
||||||
|
OutlinedTextField(
|
||||||
|
value = newPost.value,
|
||||||
|
onValueChange = { newPost.value = it },
|
||||||
|
keyboardOptions = KeyboardOptions.Default.copy(
|
||||||
|
capitalization = KeyboardCapitalization.Sentences
|
||||||
|
),
|
||||||
|
modifier = Modifier.weight(1f, true).padding(end = 10.dp),
|
||||||
|
placeholder = {
|
||||||
|
Text(
|
||||||
|
text = "reply here.. ",
|
||||||
|
color = MaterialTheme.colors.onSurface.copy(alpha = 0.32f)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
PostButton(
|
||||||
|
onPost = {
|
||||||
|
account.sendPrivateMeesage(newPost.value.text, userId)
|
||||||
|
newPost.value = TextFieldValue("")
|
||||||
|
},
|
||||||
|
newPost.value.text.isNotBlank()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun ChatroomHeader(baseUser: User, accountViewModel: AccountViewModel, navController: NavController) {
|
||||||
|
val authorState by baseUser.live.observeAsState()
|
||||||
|
val author = authorState?.user
|
||||||
|
|
||||||
|
Column(modifier =
|
||||||
|
Modifier
|
||||||
|
.padding(12.dp)
|
||||||
|
//.clickable(
|
||||||
|
//onClick = { navController.navigate("User/${author?.pubkeyHex}") }
|
||||||
|
//)
|
||||||
|
) {
|
||||||
|
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||||
|
|
||||||
|
AsyncImage(
|
||||||
|
model = author?.profilePicture(),
|
||||||
|
contentDescription = "Profile Image",
|
||||||
|
modifier = Modifier
|
||||||
|
.width(35.dp)
|
||||||
|
.clip(shape = CircleShape)
|
||||||
|
)
|
||||||
|
|
||||||
|
Column(modifier = Modifier.padding(start = 10.dp)) {
|
||||||
|
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||||
|
if (author != null)
|
||||||
|
UserDisplay(author)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Divider(
|
||||||
|
modifier = Modifier.padding(top = 12.dp, start = 12.dp, end = 12.dp),
|
||||||
|
thickness = 0.25.dp
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
@@ -1,29 +0,0 @@
|
|||||||
package com.vitorpamplona.amethyst.ui.screen
|
|
||||||
|
|
||||||
import androidx.compose.foundation.layout.Arrangement
|
|
||||||
import androidx.compose.foundation.layout.Column
|
|
||||||
import androidx.compose.foundation.layout.fillMaxHeight
|
|
||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
|
||||||
import androidx.compose.material.Text
|
|
||||||
import androidx.compose.material.rememberScaffoldState
|
|
||||||
import androidx.compose.runtime.Composable
|
|
||||||
import androidx.compose.runtime.rememberCoroutineScope
|
|
||||||
import androidx.compose.ui.Alignment
|
|
||||||
import androidx.compose.ui.Modifier
|
|
||||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
|
||||||
|
|
||||||
@Composable
|
|
||||||
fun MessageScreen(accountViewModel: AccountViewModel) {
|
|
||||||
val state = rememberScaffoldState()
|
|
||||||
val scope = rememberCoroutineScope()
|
|
||||||
|
|
||||||
Column(
|
|
||||||
Modifier
|
|
||||||
.fillMaxHeight()
|
|
||||||
.fillMaxWidth(),
|
|
||||||
horizontalAlignment = Alignment.CenterHorizontally,
|
|
||||||
verticalArrangement = Arrangement.Center,
|
|
||||||
) {
|
|
||||||
Text("Message Screen")
|
|
||||||
}
|
|
||||||
}
|
|
Reference in New Issue
Block a user