Support for Blocking and Reporting Users/Posts
This commit is contained in:
+2
-2
@@ -4,9 +4,9 @@ import android.content.Context
|
|||||||
import androidx.security.crypto.EncryptedSharedPreferences
|
import androidx.security.crypto.EncryptedSharedPreferences
|
||||||
import androidx.security.crypto.MasterKeys
|
import androidx.security.crypto.MasterKeys
|
||||||
|
|
||||||
class KeyStorage {
|
class EncryptedStorage {
|
||||||
|
|
||||||
fun encryptedPreferences(context: Context): EncryptedSharedPreferences {
|
fun preferences(context: Context): EncryptedSharedPreferences {
|
||||||
val secretKey: String = MasterKeys.getOrCreate(MasterKeys.AES256_GCM_SPEC);
|
val secretKey: String = MasterKeys.getOrCreate(MasterKeys.AES256_GCM_SPEC);
|
||||||
val preferencesName = "secret_keeper"
|
val preferencesName = "secret_keeper"
|
||||||
|
|
||||||
@@ -0,0 +1,50 @@
|
|||||||
|
package com.vitorpamplona.amethyst
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import com.vitorpamplona.amethyst.model.Account
|
||||||
|
import com.vitorpamplona.amethyst.model.DefaultChannels
|
||||||
|
import com.vitorpamplona.amethyst.model.toByteArray
|
||||||
|
import nostr.postr.Persona
|
||||||
|
import nostr.postr.toHex
|
||||||
|
|
||||||
|
class LocalPreferences(context: Context) {
|
||||||
|
val encryptedPreferences = EncryptedStorage().preferences(context)
|
||||||
|
|
||||||
|
fun clearEncryptedStorage() {
|
||||||
|
encryptedPreferences.edit().apply {
|
||||||
|
remove("nostr_privkey")
|
||||||
|
remove("nostr_pubkey")
|
||||||
|
remove("following_channels")
|
||||||
|
remove("hidden_users")
|
||||||
|
}.apply()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun saveToEncryptedStorage(account: Account) {
|
||||||
|
encryptedPreferences.edit().apply {
|
||||||
|
account.loggedIn.privKey?.let { putString("nostr_privkey", it.toHex()) }
|
||||||
|
account.loggedIn.pubKey.let { putString("nostr_pubkey", it.toHex()) }
|
||||||
|
account.followingChannels.let { putStringSet("following_channels", it) }
|
||||||
|
account.hiddenUsers.let { putStringSet("hidden_users", it) }
|
||||||
|
}.apply()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun loadFromEncryptedStorage(): Account? {
|
||||||
|
encryptedPreferences.apply {
|
||||||
|
val privKey = getString("nostr_privkey", null)
|
||||||
|
val pubKey = getString("nostr_pubkey", null)
|
||||||
|
val followingChannels = getStringSet("following_channels", DefaultChannels)?.toMutableSet() ?: DefaultChannels.toMutableSet()
|
||||||
|
val hiddenUsers = getStringSet("hidden_users", emptySet())?.toMutableSet() ?: mutableSetOf()
|
||||||
|
|
||||||
|
if (pubKey != null) {
|
||||||
|
return Account(
|
||||||
|
Persona(privKey = privKey?.toByteArray(), pubKey = pubKey.toByteArray()),
|
||||||
|
followingChannels,
|
||||||
|
hiddenUsers
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -36,6 +36,13 @@ object ServiceManager {
|
|||||||
NostrNotificationDataSource.account = myAccount
|
NostrNotificationDataSource.account = myAccount
|
||||||
NostrChatroomListDataSource.account = myAccount
|
NostrChatroomListDataSource.account = myAccount
|
||||||
|
|
||||||
|
NostrGlobalDataSource.account = myAccount
|
||||||
|
NostrChannelDataSource.account = myAccount
|
||||||
|
|
||||||
|
NostrUserProfileDataSource.account = myAccount
|
||||||
|
NostrUserProfileFollowsDataSource.account = myAccount
|
||||||
|
NostrUserProfileFollowersDataSource.account = myAccount
|
||||||
|
|
||||||
NostrAccountDataSource.start()
|
NostrAccountDataSource.start()
|
||||||
NostrGlobalDataSource.start()
|
NostrGlobalDataSource.start()
|
||||||
NostrHomeDataSource.start()
|
NostrHomeDataSource.start()
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
package com.vitorpamplona.amethyst.lnurl
|
package com.vitorpamplona.amethyst.lnurl
|
||||||
|
|
||||||
|
import androidx.compose.ui.text.toLowerCase
|
||||||
import java.math.BigDecimal
|
import java.math.BigDecimal
|
||||||
import java.util.Locale
|
import java.util.Locale
|
||||||
import java.util.regex.Pattern
|
import java.util.regex.Pattern
|
||||||
@@ -128,7 +129,7 @@ object LnInvoiceUtil {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun multiplier(multiplier: String): BigDecimal {
|
private fun multiplier(multiplier: String): BigDecimal {
|
||||||
return when (multiplier) {
|
return when (multiplier.toLowerCase()) {
|
||||||
"m" -> BigDecimal("0.001")
|
"m" -> BigDecimal("0.001")
|
||||||
"u" -> BigDecimal("0.000001")
|
"u" -> BigDecimal("0.000001")
|
||||||
"n" -> BigDecimal("0.000000001")
|
"n" -> BigDecimal("0.000000001")
|
||||||
|
|||||||
@@ -10,8 +10,12 @@ import com.vitorpamplona.amethyst.service.model.RepostEvent
|
|||||||
import com.vitorpamplona.amethyst.service.relays.Client
|
import com.vitorpamplona.amethyst.service.relays.Client
|
||||||
import com.vitorpamplona.amethyst.service.relays.Relay
|
import com.vitorpamplona.amethyst.service.relays.Relay
|
||||||
import com.vitorpamplona.amethyst.service.relays.RelayPool
|
import com.vitorpamplona.amethyst.service.relays.RelayPool
|
||||||
import com.vitorpamplona.amethyst.ui.screen.AccountStateViewModel
|
|
||||||
import java.util.Date
|
import java.util.Date
|
||||||
|
import kotlinx.coroutines.CoroutineScope
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.Job
|
||||||
|
import kotlinx.coroutines.delay
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
import nostr.postr.Contact
|
import nostr.postr.Contact
|
||||||
import nostr.postr.Persona
|
import nostr.postr.Persona
|
||||||
import nostr.postr.Utils
|
import nostr.postr.Utils
|
||||||
@@ -27,8 +31,11 @@ val DefaultChannels = setOf(
|
|||||||
"42224859763652914db53052103f0b744df79dfc4efef7e950fc0802fc3df3c5" // -> Amethyst's Group
|
"42224859763652914db53052103f0b744df79dfc4efef7e950fc0802fc3df3c5" // -> Amethyst's Group
|
||||||
)
|
)
|
||||||
|
|
||||||
class Account(val loggedIn: Persona, val followingChannels: MutableSet<String> = DefaultChannels.toMutableSet()) {
|
class Account(
|
||||||
var seeReplies: Boolean = true
|
val loggedIn: Persona,
|
||||||
|
val followingChannels: MutableSet<String> = DefaultChannels.toMutableSet(),
|
||||||
|
val hiddenUsers: MutableSet<String> = DefaultChannels.toMutableSet()
|
||||||
|
) {
|
||||||
|
|
||||||
fun userProfile(): User {
|
fun userProfile(): User {
|
||||||
return LocalCache.getOrCreateUser(loggedIn.pubKey)
|
return LocalCache.getOrCreateUser(loggedIn.pubKey)
|
||||||
@@ -38,6 +45,10 @@ class Account(val loggedIn: Persona, val followingChannels: MutableSet<String> =
|
|||||||
return followingChannels.map { LocalCache.getOrCreateChannel(it) }
|
return followingChannels.map { LocalCache.getOrCreateChannel(it) }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun hiddenUsers(): List<User> {
|
||||||
|
return hiddenUsers.map { LocalCache.getOrCreateUser(it.toByteArray()) }
|
||||||
|
}
|
||||||
|
|
||||||
fun isWriteable(): Boolean {
|
fun isWriteable(): Boolean {
|
||||||
return loggedIn.privKey != null
|
return loggedIn.privKey != null
|
||||||
}
|
}
|
||||||
@@ -79,7 +90,7 @@ class Account(val loggedIn: Persona, val followingChannels: MutableSet<String> =
|
|||||||
fun reactTo(note: Note) {
|
fun reactTo(note: Note) {
|
||||||
if (!isWriteable()) return
|
if (!isWriteable()) return
|
||||||
|
|
||||||
if (note.reactions.firstOrNull { it.author == userProfile() } != null) {
|
if (note.reactions.firstOrNull { it.author == userProfile() && it.event?.content == "+️" } != null) {
|
||||||
// has already liked this note
|
// has already liked this note
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -91,6 +102,23 @@ class Account(val loggedIn: Persona, val followingChannels: MutableSet<String> =
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun report(note: Note) {
|
||||||
|
if (!isWriteable()) return
|
||||||
|
|
||||||
|
if (
|
||||||
|
note.reactions.firstOrNull { it.author == userProfile() && it.event?.content == "⚠️"} != null
|
||||||
|
) {
|
||||||
|
// has already liked this note
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
note.event?.let {
|
||||||
|
val event = ReactionEvent.createWarning(it, loggedIn.privKey!!)
|
||||||
|
Client.send(event)
|
||||||
|
LocalCache.consume(event)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fun boost(note: Note) {
|
fun boost(note: Note) {
|
||||||
if (!isWriteable()) return
|
if (!isWriteable()) return
|
||||||
|
|
||||||
@@ -186,7 +214,7 @@ class Account(val loggedIn: Persona, val followingChannels: MutableSet<String> =
|
|||||||
LocalCache.consume(signedEvent)
|
LocalCache.consume(signedEvent)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun sendCreateNewChannel(name: String, about: String, picture: String, accountStateViewModel: AccountStateViewModel) {
|
fun sendCreateNewChannel(name: String, about: String, picture: String) {
|
||||||
if (!isWriteable()) return
|
if (!isWriteable()) return
|
||||||
|
|
||||||
val metadata = ChannelCreateEvent.ChannelData(
|
val metadata = ChannelCreateEvent.ChannelData(
|
||||||
@@ -201,17 +229,25 @@ class Account(val loggedIn: Persona, val followingChannels: MutableSet<String> =
|
|||||||
Client.send(event)
|
Client.send(event)
|
||||||
LocalCache.consume(event)
|
LocalCache.consume(event)
|
||||||
|
|
||||||
joinChannel(event.id.toHex(), accountStateViewModel)
|
joinChannel(event.id.toHex())
|
||||||
}
|
}
|
||||||
|
|
||||||
fun joinChannel(idHex: String, accountStateViewModel: AccountStateViewModel) {
|
fun joinChannel(idHex: String) {
|
||||||
followingChannels.add(idHex)
|
followingChannels.add(idHex)
|
||||||
accountStateViewModel.saveToEncryptedStorage(this)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fun leaveChannel(idHex: String, accountStateViewModel: AccountStateViewModel) {
|
fun leaveChannel(idHex: String) {
|
||||||
followingChannels.remove(idHex)
|
followingChannels.remove(idHex)
|
||||||
accountStateViewModel.saveToEncryptedStorage(this)
|
}
|
||||||
|
|
||||||
|
fun hideUser(pubkeyHex: String) {
|
||||||
|
hiddenUsers.add(pubkeyHex)
|
||||||
|
invalidateData()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun showUser(pubkeyHex: String) {
|
||||||
|
hiddenUsers.remove(pubkeyHex)
|
||||||
|
invalidateData()
|
||||||
}
|
}
|
||||||
|
|
||||||
fun sendChangeChannel(name: String, about: String, picture: String, channel: Channel) {
|
fun sendChangeChannel(name: String, about: String, picture: String, channel: Channel) {
|
||||||
@@ -280,25 +316,44 @@ class Account(val loggedIn: Persona, val followingChannels: MutableSet<String> =
|
|||||||
// Observers line up here.
|
// Observers line up here.
|
||||||
val live: AccountLiveData = AccountLiveData(this)
|
val live: AccountLiveData = AccountLiveData(this)
|
||||||
|
|
||||||
private fun refreshObservers() {
|
// Refreshes observers in batches.
|
||||||
|
var handlerWaiting = false
|
||||||
|
@Synchronized
|
||||||
|
fun invalidateData() {
|
||||||
|
if (handlerWaiting) return
|
||||||
|
|
||||||
|
handlerWaiting = true
|
||||||
|
val scope = CoroutineScope(Job() + Dispatchers.Default)
|
||||||
|
scope.launch {
|
||||||
|
delay(100)
|
||||||
live.refresh()
|
live.refresh()
|
||||||
|
handlerWaiting = false
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
fun isAcceptable(user: User): Boolean {
|
||||||
|
return user !in hiddenUsers() // if user hasn't hided this author
|
||||||
|
}
|
||||||
|
|
||||||
|
fun isAcceptableDirect(note: Note): Boolean {
|
||||||
|
return note.reports.firstOrNull { it.author == userProfile() } == null // if user has not reported this post
|
||||||
|
&& note.reports.filter { it.author in userProfile().follows }.size < 5 // if it has 5 reports by reliable users
|
||||||
|
}
|
||||||
|
|
||||||
|
fun isAcceptable(note: Note): Boolean {
|
||||||
|
return note.author?.let { isAcceptable(it) } ?: true // if user hasn't hided this author
|
||||||
|
&& isAcceptableDirect(note)
|
||||||
|
&& (note.event !is ReactionEvent
|
||||||
|
|| (note.event is ReactionEvent && note.replyTo?.firstOrNull { isAcceptableDirect(note) } == null)
|
||||||
|
) // is not a reaction about a blocked post
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class AccountLiveData(private val account: Account): LiveData<AccountState>(AccountState(account)) {
|
class AccountLiveData(private val account: Account): LiveData<AccountState>(AccountState(account)) {
|
||||||
fun refresh() {
|
fun refresh() {
|
||||||
postValue(AccountState(account))
|
postValue(AccountState(account))
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onActive() {
|
|
||||||
super.onActive()
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onInactive() {
|
|
||||||
super.onInactive()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
class AccountState(val account: Account)
|
class AccountState(val account: Account)
|
||||||
@@ -256,6 +256,13 @@ object LocalCache {
|
|||||||
it.addReaction(note)
|
it.addReaction(note)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (event.content == "\u26A0\uFE0F") {
|
||||||
|
// Counts the replies
|
||||||
|
repliesTo.forEach {
|
||||||
|
it.addReport(note)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun consume(event: ChannelCreateEvent) {
|
fun consume(event: ChannelCreateEvent) {
|
||||||
|
|||||||
@@ -35,6 +35,8 @@ class Note(val idHex: String) {
|
|||||||
val reactions = Collections.synchronizedSet(mutableSetOf<Note>())
|
val reactions = Collections.synchronizedSet(mutableSetOf<Note>())
|
||||||
val boosts = Collections.synchronizedSet(mutableSetOf<Note>())
|
val boosts = Collections.synchronizedSet(mutableSetOf<Note>())
|
||||||
|
|
||||||
|
val reports = Collections.synchronizedSet(mutableSetOf<Note>())
|
||||||
|
|
||||||
var channel: Channel? = null
|
var channel: Channel? = null
|
||||||
|
|
||||||
fun loadEvent(event: Event, author: User, mentions: List<User>, replyTo: MutableList<Note>) {
|
fun loadEvent(event: Event, author: User, mentions: List<User>, replyTo: MutableList<Note>) {
|
||||||
@@ -88,6 +90,11 @@ class Note(val idHex: String) {
|
|||||||
invalidateData()
|
invalidateData()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun addReport(note: Note) {
|
||||||
|
if (reports.add(note))
|
||||||
|
invalidateData()
|
||||||
|
}
|
||||||
|
|
||||||
fun isReactedBy(user: User): Boolean {
|
fun isReactedBy(user: User): Boolean {
|
||||||
return synchronized(reactions) {
|
return synchronized(reactions) {
|
||||||
reactions.any { it.author == user }
|
reactions.any { it.author == user }
|
||||||
|
|||||||
@@ -1,11 +1,13 @@
|
|||||||
package com.vitorpamplona.amethyst.service
|
package com.vitorpamplona.amethyst.service
|
||||||
|
|
||||||
|
import com.vitorpamplona.amethyst.model.Account
|
||||||
import com.vitorpamplona.amethyst.model.LocalCache
|
import com.vitorpamplona.amethyst.model.LocalCache
|
||||||
import com.vitorpamplona.amethyst.model.Note
|
import com.vitorpamplona.amethyst.model.Note
|
||||||
import com.vitorpamplona.amethyst.service.model.ChannelMessageEvent
|
import com.vitorpamplona.amethyst.service.model.ChannelMessageEvent
|
||||||
import nostr.postr.JsonFilter
|
import nostr.postr.JsonFilter
|
||||||
|
|
||||||
object NostrChannelDataSource: NostrDataSource<Note>("ChatroomFeed") {
|
object NostrChannelDataSource: NostrDataSource<Note>("ChatroomFeed") {
|
||||||
|
lateinit var account: Account
|
||||||
var channel: com.vitorpamplona.amethyst.model.Channel? = null
|
var channel: com.vitorpamplona.amethyst.model.Channel? = null
|
||||||
|
|
||||||
fun loadMessagesBetween(channelId: String) {
|
fun loadMessagesBetween(channelId: String) {
|
||||||
@@ -28,7 +30,7 @@ object NostrChannelDataSource: NostrDataSource<Note>("ChatroomFeed") {
|
|||||||
|
|
||||||
// returns the last Note of each user.
|
// returns the last Note of each user.
|
||||||
override fun feed(): List<Note> {
|
override fun feed(): List<Note> {
|
||||||
return channel?.notes?.values?.sortedBy { it.event?.createdAt } ?: emptyList()
|
return channel?.notes?.values?.filter { account.isAcceptable(it) }?.sortedBy { it.event?.createdAt } ?: emptyList()
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun updateChannelFilters() {
|
override fun updateChannelFilters() {
|
||||||
|
|||||||
@@ -33,7 +33,7 @@ object NostrChatRoomDataSource: NostrDataSource<Note>("ChatroomFeed") {
|
|||||||
|
|
||||||
// returns the last Note of each user.
|
// returns the last Note of each user.
|
||||||
override fun feed(): List<Note> {
|
override fun feed(): List<Note> {
|
||||||
val messages = account.userProfile().messages[withUser]
|
val messages = account.userProfile().messages[withUser]?.filter { account.isAcceptable(it) }
|
||||||
|
|
||||||
return messages?.sortedBy { it.event?.createdAt } ?: emptyList()
|
return messages?.sortedBy { it.event?.createdAt } ?: emptyList()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -64,14 +64,14 @@ object NostrChatroomListDataSource: NostrDataSource<Note>("MailBoxFeed") {
|
|||||||
// returns the last Note of each user.
|
// returns the last Note of each user.
|
||||||
override fun feed(): List<Note> {
|
override fun feed(): List<Note> {
|
||||||
val messages = account.userProfile().messages
|
val messages = account.userProfile().messages
|
||||||
val messagingWith = messages.keys().toList()
|
val messagingWith = messages.keys().toList().filter { account.isAcceptable(it) }
|
||||||
|
|
||||||
val privateMessages = messagingWith.mapNotNull {
|
val privateMessages = messagingWith.mapNotNull {
|
||||||
messages[it]?.sortedBy { it.event?.createdAt }?.lastOrNull { it.event != null }
|
messages[it]?.sortedBy { it.event?.createdAt }?.lastOrNull { it.event != null }
|
||||||
}
|
}
|
||||||
|
|
||||||
val publicChannels = account.followingChannels().map {
|
val publicChannels = account.followingChannels().map {
|
||||||
it.notes.values.sortedBy { it.event?.createdAt }.lastOrNull { it.event != null }
|
it.notes.values.filter { account.isAcceptable(it) }.sortedBy { it.event?.createdAt }.lastOrNull { it.event != null }
|
||||||
}
|
}
|
||||||
|
|
||||||
val channelsCreatedByMe = LocalCache.channels.values.filter {
|
val channelsCreatedByMe = LocalCache.channels.values.filter {
|
||||||
|
|||||||
@@ -1,11 +1,13 @@
|
|||||||
package com.vitorpamplona.amethyst.service
|
package com.vitorpamplona.amethyst.service
|
||||||
|
|
||||||
|
import com.vitorpamplona.amethyst.model.Account
|
||||||
import com.vitorpamplona.amethyst.model.LocalCache
|
import com.vitorpamplona.amethyst.model.LocalCache
|
||||||
import com.vitorpamplona.amethyst.model.Note
|
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<Note>("GlobalFeed") {
|
object NostrGlobalDataSource: NostrDataSource<Note>("GlobalFeed") {
|
||||||
|
lateinit var account: Account
|
||||||
fun createGlobalFilter() = JsonFilter(
|
fun createGlobalFilter() = JsonFilter(
|
||||||
kinds = listOf(TextNoteEvent.kind),
|
kinds = listOf(TextNoteEvent.kind),
|
||||||
limit = 50
|
limit = 50
|
||||||
@@ -14,6 +16,7 @@ object NostrGlobalDataSource: NostrDataSource<Note>("GlobalFeed") {
|
|||||||
val globalFeedChannel = requestNewChannel()
|
val globalFeedChannel = requestNewChannel()
|
||||||
|
|
||||||
override fun feed() = LocalCache.notes.values
|
override fun feed() = LocalCache.notes.values
|
||||||
|
.filter { account.isAcceptable(it) }
|
||||||
.filter {
|
.filter {
|
||||||
it.event is TextNoteEvent && (it.event as TextNoteEvent).replyTos.isEmpty()
|
it.event is TextNoteEvent && (it.event as TextNoteEvent).replyTos.isEmpty()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,16 @@
|
|||||||
|
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.TextNoteEvent
|
||||||
|
|
||||||
|
object NostrHiddenAccountsDataSource: NostrDataSource<User>("HiddenAccounts") {
|
||||||
|
lateinit var account: Account
|
||||||
|
|
||||||
|
override fun feed() = account.hiddenUsers()
|
||||||
|
|
||||||
|
override fun updateChannelFilters() {}
|
||||||
|
}
|
||||||
@@ -82,6 +82,7 @@ object NostrHomeDataSource: NostrDataSource<Note>("HomeFeed") {
|
|||||||
|
|
||||||
return LocalCache.notes.values
|
return LocalCache.notes.values
|
||||||
.filter { (it.event is TextNoteEvent || it.event is RepostEvent) && it.author?.pubkeyHex in allowSet }
|
.filter { (it.event is TextNoteEvent || it.event is RepostEvent) && it.author?.pubkeyHex in allowSet }
|
||||||
|
.filter { account.isAcceptable(it) }
|
||||||
.sortedBy { it.event?.createdAt }
|
.sortedBy { it.event?.createdAt }
|
||||||
.reversed()
|
.reversed()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ object NostrNotificationDataSource: NostrDataSource<Note>("NotificationFeed") {
|
|||||||
override fun feed(): List<Note> {
|
override fun feed(): List<Note> {
|
||||||
val set = account.userProfile().taggedPosts
|
val set = account.userProfile().taggedPosts
|
||||||
val filtered = synchronized(set) {
|
val filtered = synchronized(set) {
|
||||||
set.filter { it.event != null }
|
set.filter { it.event != null }.filter { account.isAcceptable(it) }
|
||||||
}
|
}
|
||||||
|
|
||||||
return filtered.sortedBy { it.event?.createdAt }.reversed()
|
return filtered.sortedBy { it.event?.createdAt }.reversed()
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
package com.vitorpamplona.amethyst.service
|
package com.vitorpamplona.amethyst.service
|
||||||
|
|
||||||
|
import com.vitorpamplona.amethyst.model.Account
|
||||||
import com.vitorpamplona.amethyst.model.LocalCache
|
import com.vitorpamplona.amethyst.model.LocalCache
|
||||||
import com.vitorpamplona.amethyst.model.Note
|
import com.vitorpamplona.amethyst.model.Note
|
||||||
import com.vitorpamplona.amethyst.model.User
|
import com.vitorpamplona.amethyst.model.User
|
||||||
@@ -8,6 +9,7 @@ import nostr.postr.events.MetadataEvent
|
|||||||
import nostr.postr.events.TextNoteEvent
|
import nostr.postr.events.TextNoteEvent
|
||||||
|
|
||||||
object NostrUserProfileDataSource: NostrDataSource<Note>("UserProfileFeed") {
|
object NostrUserProfileDataSource: NostrDataSource<Note>("UserProfileFeed") {
|
||||||
|
lateinit var account: Account
|
||||||
var user: User? = null
|
var user: User? = null
|
||||||
|
|
||||||
fun loadUserProfile(userId: String) {
|
fun loadUserProfile(userId: String) {
|
||||||
@@ -37,7 +39,7 @@ object NostrUserProfileDataSource: NostrDataSource<Note>("UserProfileFeed") {
|
|||||||
override fun feed(): List<Note> {
|
override fun feed(): List<Note> {
|
||||||
val notes = user?.notes ?: return emptyList()
|
val notes = user?.notes ?: return emptyList()
|
||||||
val sortedNotes = synchronized(notes) {
|
val sortedNotes = synchronized(notes) {
|
||||||
notes.sortedBy { it.event?.createdAt }
|
notes.filter { account.isAcceptable(it) }.sortedBy { it.event?.createdAt }
|
||||||
}
|
}
|
||||||
return sortedNotes.reversed()
|
return sortedNotes.reversed()
|
||||||
}
|
}
|
||||||
|
|||||||
+3
-1
@@ -1,11 +1,13 @@
|
|||||||
package com.vitorpamplona.amethyst.service
|
package com.vitorpamplona.amethyst.service
|
||||||
|
|
||||||
|
import com.vitorpamplona.amethyst.model.Account
|
||||||
import com.vitorpamplona.amethyst.model.LocalCache
|
import com.vitorpamplona.amethyst.model.LocalCache
|
||||||
import com.vitorpamplona.amethyst.model.User
|
import com.vitorpamplona.amethyst.model.User
|
||||||
import nostr.postr.JsonFilter
|
import nostr.postr.JsonFilter
|
||||||
import nostr.postr.events.ContactListEvent
|
import nostr.postr.events.ContactListEvent
|
||||||
|
|
||||||
object NostrUserProfileFollowersDataSource: NostrDataSource<User>("UserProfileFollowerFeed") {
|
object NostrUserProfileFollowersDataSource: NostrDataSource<User>("UserProfileFollowerFeed") {
|
||||||
|
lateinit var account: Account
|
||||||
var user: User? = null
|
var user: User? = null
|
||||||
|
|
||||||
fun loadUserProfile(userId: String) {
|
fun loadUserProfile(userId: String) {
|
||||||
@@ -24,7 +26,7 @@ object NostrUserProfileFollowersDataSource: NostrDataSource<User>("UserProfileFo
|
|||||||
val followers = user?.followers ?: emptyList()
|
val followers = user?.followers ?: emptyList()
|
||||||
|
|
||||||
return synchronized(followers) {
|
return synchronized(followers) {
|
||||||
followers.toList()
|
followers.filter { account.isAcceptable(it) }.toList()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+7
-1
@@ -1,11 +1,13 @@
|
|||||||
package com.vitorpamplona.amethyst.service
|
package com.vitorpamplona.amethyst.service
|
||||||
|
|
||||||
|
import com.vitorpamplona.amethyst.model.Account
|
||||||
import com.vitorpamplona.amethyst.model.LocalCache
|
import com.vitorpamplona.amethyst.model.LocalCache
|
||||||
import com.vitorpamplona.amethyst.model.User
|
import com.vitorpamplona.amethyst.model.User
|
||||||
import nostr.postr.JsonFilter
|
import nostr.postr.JsonFilter
|
||||||
import nostr.postr.events.ContactListEvent
|
import nostr.postr.events.ContactListEvent
|
||||||
|
|
||||||
object NostrUserProfileFollowsDataSource: NostrDataSource<User>("UserProfileFollowsFeed") {
|
object NostrUserProfileFollowsDataSource: NostrDataSource<User>("UserProfileFollowsFeed") {
|
||||||
|
lateinit var account: Account
|
||||||
var user: User? = null
|
var user: User? = null
|
||||||
|
|
||||||
fun loadUserProfile(userId: String) {
|
fun loadUserProfile(userId: String) {
|
||||||
@@ -24,7 +26,11 @@ object NostrUserProfileFollowsDataSource: NostrDataSource<User>("UserProfileFoll
|
|||||||
val followChannel = requestNewChannel()
|
val followChannel = requestNewChannel()
|
||||||
|
|
||||||
override fun feed(): List<User> {
|
override fun feed(): List<User> {
|
||||||
return user?.follows?.toList() ?: emptyList()
|
val follows = user?.follows ?: emptyList()
|
||||||
|
|
||||||
|
return synchronized(follows) {
|
||||||
|
follows.filter { account.isAcceptable(it) }.toList()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun updateChannelFilters() {
|
override fun updateChannelFilters() {
|
||||||
|
|||||||
@@ -25,6 +25,10 @@ class ReactionEvent (
|
|||||||
companion object {
|
companion object {
|
||||||
const val kind = 7
|
const val kind = 7
|
||||||
|
|
||||||
|
fun createWarning(originalNote: Event, privateKey: ByteArray, createdAt: Long = Date().time / 1000): ReactionEvent {
|
||||||
|
return create("\u26A0\uFE0F", originalNote, privateKey, createdAt)
|
||||||
|
}
|
||||||
|
|
||||||
fun createLike(originalNote: Event, privateKey: ByteArray, createdAt: Long = Date().time / 1000): ReactionEvent {
|
fun createLike(originalNote: Event, privateKey: ByteArray, createdAt: Long = Date().time / 1000): ReactionEvent {
|
||||||
return create("+", originalNote, privateKey, createdAt)
|
return create("+", originalNote, privateKey, createdAt)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,21 +14,9 @@ import coil.ImageLoader
|
|||||||
import coil.decode.GifDecoder
|
import coil.decode.GifDecoder
|
||||||
import coil.decode.ImageDecoderDecoder
|
import coil.decode.ImageDecoderDecoder
|
||||||
import coil.decode.SvgDecoder
|
import coil.decode.SvgDecoder
|
||||||
import com.vitorpamplona.amethyst.KeyStorage
|
import com.vitorpamplona.amethyst.EncryptedStorage
|
||||||
|
import com.vitorpamplona.amethyst.LocalPreferences
|
||||||
import com.vitorpamplona.amethyst.ServiceManager
|
import com.vitorpamplona.amethyst.ServiceManager
|
||||||
import com.vitorpamplona.amethyst.model.Account
|
|
||||||
import com.vitorpamplona.amethyst.service.NostrAccountDataSource
|
|
||||||
import com.vitorpamplona.amethyst.service.NostrChannelDataSource
|
|
||||||
import com.vitorpamplona.amethyst.service.NostrChatroomListDataSource
|
|
||||||
import com.vitorpamplona.amethyst.service.NostrGlobalDataSource
|
|
||||||
import com.vitorpamplona.amethyst.service.NostrHomeDataSource
|
|
||||||
import com.vitorpamplona.amethyst.service.NostrNotificationDataSource
|
|
||||||
import com.vitorpamplona.amethyst.service.NostrSingleEventDataSource
|
|
||||||
import com.vitorpamplona.amethyst.service.NostrSingleUserDataSource
|
|
||||||
import com.vitorpamplona.amethyst.service.NostrThreadDataSource
|
|
||||||
import com.vitorpamplona.amethyst.service.NostrUserProfileDataSource
|
|
||||||
import com.vitorpamplona.amethyst.service.NostrUserProfileFollowersDataSource
|
|
||||||
import com.vitorpamplona.amethyst.service.NostrUserProfileFollowsDataSource
|
|
||||||
import com.vitorpamplona.amethyst.service.relays.Client
|
import com.vitorpamplona.amethyst.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
|
||||||
@@ -57,7 +45,7 @@ class MainActivity : ComponentActivity() {
|
|||||||
Surface(modifier = Modifier.fillMaxSize(), color = MaterialTheme.colors.background) {
|
Surface(modifier = Modifier.fillMaxSize(), color = MaterialTheme.colors.background) {
|
||||||
|
|
||||||
val accountViewModel: AccountStateViewModel = viewModel {
|
val accountViewModel: AccountStateViewModel = viewModel {
|
||||||
AccountStateViewModel(KeyStorage().encryptedPreferences(applicationContext))
|
AccountStateViewModel(LocalPreferences(applicationContext))
|
||||||
}
|
}
|
||||||
|
|
||||||
AccountScreen(accountViewModel)
|
AccountScreen(accountViewModel)
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ import androidx.compose.material.Text
|
|||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.platform.LocalContext
|
||||||
import androidx.compose.ui.text.input.KeyboardCapitalization
|
import androidx.compose.ui.text.input.KeyboardCapitalization
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.compose.ui.window.Dialog
|
import androidx.compose.ui.window.Dialog
|
||||||
@@ -26,10 +27,11 @@ import com.vitorpamplona.amethyst.ui.screen.AccountStateViewModel
|
|||||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun NewChannelView(onClose: () -> Unit, account: Account, accountStateViewModel: AccountStateViewModel, channel: Channel? = null) {
|
fun NewChannelView(onClose: () -> Unit, account: Account, channel: Channel? = null) {
|
||||||
val postViewModel: NewChannelViewModel = viewModel()
|
val postViewModel: NewChannelViewModel = viewModel()
|
||||||
|
val context = LocalContext.current.applicationContext
|
||||||
|
|
||||||
postViewModel.load(account, channel, accountStateViewModel)
|
postViewModel.load(account, channel)
|
||||||
|
|
||||||
Dialog(
|
Dialog(
|
||||||
onDismissRequest = { onClose() },
|
onDismissRequest = { onClose() },
|
||||||
@@ -55,7 +57,7 @@ fun NewChannelView(onClose: () -> Unit, account: Account, accountStateViewModel:
|
|||||||
|
|
||||||
PostButton(
|
PostButton(
|
||||||
onPost = {
|
onPost = {
|
||||||
postViewModel.create()
|
postViewModel.create(context)
|
||||||
onClose()
|
onClose()
|
||||||
},
|
},
|
||||||
postViewModel.channelName.value.text.isNotBlank()
|
postViewModel.channelName.value.text.isNotBlank()
|
||||||
|
|||||||
@@ -1,25 +1,26 @@
|
|||||||
package com.vitorpamplona.amethyst.ui.actions
|
package com.vitorpamplona.amethyst.ui.actions
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
import androidx.compose.runtime.mutableStateOf
|
import androidx.compose.runtime.mutableStateOf
|
||||||
import androidx.compose.ui.text.input.TextFieldValue
|
import androidx.compose.ui.text.input.TextFieldValue
|
||||||
import androidx.lifecycle.ViewModel
|
import androidx.lifecycle.ViewModel
|
||||||
|
import com.vitorpamplona.amethyst.LocalPreferences
|
||||||
import com.vitorpamplona.amethyst.model.Account
|
import com.vitorpamplona.amethyst.model.Account
|
||||||
import com.vitorpamplona.amethyst.model.Channel
|
import com.vitorpamplona.amethyst.model.Channel
|
||||||
import com.vitorpamplona.amethyst.ui.screen.AccountStateViewModel
|
import com.vitorpamplona.amethyst.ui.screen.AccountStateViewModel
|
||||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
||||||
|
import nostr.postr.toHex
|
||||||
|
|
||||||
class NewChannelViewModel: ViewModel() {
|
class NewChannelViewModel: ViewModel() {
|
||||||
private var account: Account? = null
|
private var account: Account? = null
|
||||||
private var originalChannel: Channel? = null
|
private var originalChannel: Channel? = null
|
||||||
private var accountStateViewModel: AccountStateViewModel? = null
|
|
||||||
|
|
||||||
val channelName = mutableStateOf(TextFieldValue())
|
val channelName = mutableStateOf(TextFieldValue())
|
||||||
val channelPicture = mutableStateOf(TextFieldValue())
|
val channelPicture = mutableStateOf(TextFieldValue())
|
||||||
val channelDescription = mutableStateOf(TextFieldValue())
|
val channelDescription = mutableStateOf(TextFieldValue())
|
||||||
|
|
||||||
|
|
||||||
fun load(account: Account, channel: Channel?, accountStateViewModel: AccountStateViewModel) {
|
fun load(account: Account, channel: Channel?) {
|
||||||
this.accountStateViewModel = accountStateViewModel
|
|
||||||
this.account = account
|
this.account = account
|
||||||
if (channel != null) {
|
if (channel != null) {
|
||||||
originalChannel = channel
|
originalChannel = channel
|
||||||
@@ -29,21 +30,24 @@ class NewChannelViewModel: ViewModel() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun create() {
|
fun create(context: Context) {
|
||||||
if (originalChannel == null)
|
this.account?.let { account ->
|
||||||
this.account?.sendCreateNewChannel(
|
if (originalChannel == null) {
|
||||||
|
account.sendCreateNewChannel(
|
||||||
channelName.value.text,
|
channelName.value.text,
|
||||||
channelDescription.value.text,
|
channelDescription.value.text,
|
||||||
channelPicture.value.text,
|
channelPicture.value.text
|
||||||
accountStateViewModel!!
|
|
||||||
)
|
)
|
||||||
else
|
|
||||||
this.account?.sendChangeChannel(
|
LocalPreferences(context).saveToEncryptedStorage(account)
|
||||||
|
} else
|
||||||
|
account.sendChangeChannel(
|
||||||
channelName.value.text,
|
channelName.value.text,
|
||||||
channelDescription.value.text,
|
channelDescription.value.text,
|
||||||
channelPicture.value.text,
|
channelPicture.value.text,
|
||||||
originalChannel!!
|
originalChannel!!
|
||||||
)
|
)
|
||||||
|
}
|
||||||
|
|
||||||
clear()
|
clear()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -23,6 +23,7 @@ import androidx.compose.material.Text
|
|||||||
import androidx.compose.material.TextFieldDefaults
|
import androidx.compose.material.TextFieldDefaults
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.LaunchedEffect
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.ExperimentalComposeUiApi
|
import androidx.compose.ui.ExperimentalComposeUiApi
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
@@ -67,7 +68,7 @@ fun NewPostView(onClose: () -> Unit, baseReplyTo: Note? = null, account: Account
|
|||||||
val context = LocalContext.current
|
val context = LocalContext.current
|
||||||
|
|
||||||
// initialize focus reference to be able to request focus programmatically
|
// initialize focus reference to be able to request focus programmatically
|
||||||
val focusRequester = FocusRequester()
|
val focusRequester = remember { FocusRequester() }
|
||||||
val keyboardController = LocalSoftwareKeyboardController.current
|
val keyboardController = LocalSoftwareKeyboardController.current
|
||||||
|
|
||||||
LaunchedEffect(Unit) {
|
LaunchedEffect(Unit) {
|
||||||
|
|||||||
@@ -28,13 +28,13 @@ import com.vitorpamplona.amethyst.ui.screen.AccountStateViewModel
|
|||||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun NewChannelButton(account: Account, accountStateViewModel: AccountStateViewModel) {
|
fun NewChannelButton(account: Account) {
|
||||||
var wantsToPost by remember {
|
var wantsToPost by remember {
|
||||||
mutableStateOf(false)
|
mutableStateOf(false)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (wantsToPost)
|
if (wantsToPost)
|
||||||
NewChannelView({ wantsToPost = false }, account = account, accountStateViewModel)
|
NewChannelView({ wantsToPost = false }, account = account)
|
||||||
|
|
||||||
OutlinedButton(
|
OutlinedButton(
|
||||||
onClick = { wantsToPost = true },
|
onClick = { wantsToPost = true },
|
||||||
|
|||||||
@@ -162,6 +162,8 @@ fun ListContent(
|
|||||||
modifier: Modifier,
|
modifier: Modifier,
|
||||||
accountViewModel: AccountStateViewModel
|
accountViewModel: AccountStateViewModel
|
||||||
) {
|
) {
|
||||||
|
val coroutineScope = rememberCoroutineScope()
|
||||||
|
|
||||||
Column(modifier = modifier) {
|
Column(modifier = modifier) {
|
||||||
LazyColumn() {
|
LazyColumn() {
|
||||||
item {
|
item {
|
||||||
@@ -178,14 +180,19 @@ fun ListContent(
|
|||||||
thickness = 0.25.dp
|
thickness = 0.25.dp
|
||||||
)
|
)
|
||||||
Column(modifier = modifier.padding(horizontal = 25.dp)) {
|
Column(modifier = modifier.padding(horizontal = 25.dp)) {
|
||||||
|
Row(modifier = Modifier.clickable(onClick = {
|
||||||
|
navController.navigate(Route.Filters.route)
|
||||||
|
coroutineScope.launch {
|
||||||
|
scaffoldState.drawerState.close()
|
||||||
|
}
|
||||||
|
})) {
|
||||||
Text(
|
Text(
|
||||||
text = "Settings",
|
text = "Security Filters",
|
||||||
fontSize = 18.sp,
|
fontSize = 18.sp,
|
||||||
fontWeight = W500
|
fontWeight = W500
|
||||||
)
|
)
|
||||||
Row(
|
}
|
||||||
modifier = Modifier.clickable(onClick = { accountViewModel.logOff() }),
|
Row(modifier = Modifier.clickable(onClick = { accountViewModel.logOff() })) {
|
||||||
) {
|
|
||||||
Text(
|
Text(
|
||||||
text = "Log out",
|
text = "Log out",
|
||||||
modifier = Modifier.padding(vertical = 15.dp),
|
modifier = Modifier.padding(vertical = 15.dp),
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ import com.vitorpamplona.amethyst.ui.screen.AccountStateViewModel
|
|||||||
import com.vitorpamplona.amethyst.ui.screen.ChannelScreen
|
import com.vitorpamplona.amethyst.ui.screen.ChannelScreen
|
||||||
import com.vitorpamplona.amethyst.ui.screen.ChatroomListScreen
|
import com.vitorpamplona.amethyst.ui.screen.ChatroomListScreen
|
||||||
import com.vitorpamplona.amethyst.ui.screen.ChatroomScreen
|
import com.vitorpamplona.amethyst.ui.screen.ChatroomScreen
|
||||||
|
import com.vitorpamplona.amethyst.ui.screen.FiltersScreen
|
||||||
import com.vitorpamplona.amethyst.ui.screen.HomeScreen
|
import com.vitorpamplona.amethyst.ui.screen.HomeScreen
|
||||||
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
|
||||||
@@ -32,6 +33,7 @@ sealed class Route(
|
|||||||
object Search : Route("Search", R.drawable.ic_search, buildScreen = { acc, accSt, nav -> { _ -> SearchScreen(acc, nav) }})
|
object Search : Route("Search", R.drawable.ic_search, buildScreen = { acc, accSt, nav -> { _ -> SearchScreen(acc, nav) }})
|
||||||
object Notification : Route("Notification", R.drawable.ic_notifications,buildScreen = { acc, accSt, nav -> { _ -> NotificationScreen(acc, nav) }})
|
object Notification : Route("Notification", R.drawable.ic_notifications,buildScreen = { acc, accSt, nav -> { _ -> NotificationScreen(acc, nav) }})
|
||||||
object Message : Route("Message", R.drawable.ic_dm, buildScreen = { acc, accSt, nav -> { _ -> ChatroomListScreen(acc, nav) }})
|
object Message : Route("Message", R.drawable.ic_dm, buildScreen = { acc, accSt, nav -> { _ -> ChatroomListScreen(acc, nav) }})
|
||||||
|
object Filters : Route("Filters", R.drawable.ic_dm, buildScreen = { acc, accSt, nav -> { _ -> FiltersScreen(acc, nav) }})
|
||||||
|
|
||||||
object Profile : Route("User/{id}", R.drawable.ic_profile,
|
object Profile : Route("User/{id}", R.drawable.ic_profile,
|
||||||
arguments = listOf(navArgument("id") { type = NavType.StringType } ),
|
arguments = listOf(navArgument("id") { type = NavType.StringType } ),
|
||||||
@@ -65,7 +67,8 @@ val Routes = listOf(
|
|||||||
Route.Profile,
|
Route.Profile,
|
||||||
Route.Note,
|
Route.Note,
|
||||||
Route.Room,
|
Route.Room,
|
||||||
Route.Channel
|
Route.Channel,
|
||||||
|
Route.Filters
|
||||||
)
|
)
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
|
|||||||
@@ -26,7 +26,38 @@ fun BlankNote(modifier: Modifier = Modifier, isQuote: Boolean = false) {
|
|||||||
verticalAlignment = Alignment.CenterVertically
|
verticalAlignment = Alignment.CenterVertically
|
||||||
) {
|
) {
|
||||||
Text(
|
Text(
|
||||||
text = "Referenced event not found",
|
text = "Referenced post not found",
|
||||||
|
modifier = Modifier.padding(30.dp),
|
||||||
|
color = Color.Gray,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
Divider(
|
||||||
|
modifier = Modifier.padding(vertical = 10.dp),
|
||||||
|
thickness = 0.25.dp
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun HiddenNote(modifier: Modifier = Modifier, isQuote: Boolean = false) {
|
||||||
|
Column(modifier = modifier) {
|
||||||
|
Row(modifier = Modifier.padding(horizontal = if (!isQuote) 12.dp else 6.dp)) {
|
||||||
|
Column(modifier = Modifier.padding(start = if (!isQuote) 10.dp else 5.dp)) {
|
||||||
|
Row(
|
||||||
|
modifier = Modifier.padding(
|
||||||
|
start = 20.dp,
|
||||||
|
end = 20.dp,
|
||||||
|
bottom = 25.dp,
|
||||||
|
top = 15.dp
|
||||||
|
),
|
||||||
|
verticalAlignment = Alignment.CenterVertically
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = "Post was flagged as inappropriate",
|
||||||
modifier = Modifier.padding(30.dp),
|
modifier = Modifier.padding(30.dp),
|
||||||
color = Color.Gray,
|
color = Color.Gray,
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ import androidx.compose.foundation.shape.CircleShape
|
|||||||
import androidx.compose.material.Divider
|
import androidx.compose.material.Divider
|
||||||
import androidx.compose.material.DropdownMenu
|
import androidx.compose.material.DropdownMenu
|
||||||
import androidx.compose.material.DropdownMenuItem
|
import androidx.compose.material.DropdownMenuItem
|
||||||
|
import androidx.compose.material.LocalContentColor
|
||||||
import androidx.compose.material.MaterialTheme
|
import androidx.compose.material.MaterialTheme
|
||||||
import androidx.compose.material.Text
|
import androidx.compose.material.Text
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
@@ -27,6 +28,7 @@ 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.draw.clip
|
||||||
import androidx.compose.ui.platform.LocalClipboardManager
|
import androidx.compose.ui.platform.LocalClipboardManager
|
||||||
|
import androidx.compose.ui.platform.LocalContext
|
||||||
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.dp
|
import androidx.compose.ui.unit.dp
|
||||||
@@ -46,6 +48,9 @@ import nostr.postr.toNpub
|
|||||||
@OptIn(ExperimentalFoundationApi::class)
|
@OptIn(ExperimentalFoundationApi::class)
|
||||||
@Composable
|
@Composable
|
||||||
fun NoteCompose(baseNote: Note, modifier: Modifier = Modifier, isInnerNote: Boolean = false, accountViewModel: AccountViewModel, navController: NavController) {
|
fun NoteCompose(baseNote: Note, modifier: Modifier = Modifier, isInnerNote: Boolean = false, accountViewModel: AccountViewModel, navController: NavController) {
|
||||||
|
val accountState by accountViewModel.accountLiveData.observeAsState()
|
||||||
|
val account = accountState?.account
|
||||||
|
|
||||||
val noteState by baseNote.live.observeAsState()
|
val noteState by baseNote.live.observeAsState()
|
||||||
val note = noteState?.note
|
val note = noteState?.note
|
||||||
|
|
||||||
@@ -56,6 +61,11 @@ fun NoteCompose(baseNote: Note, modifier: Modifier = Modifier, isInnerNote: Bool
|
|||||||
onClick = { },
|
onClick = { },
|
||||||
onLongClick = { popupExpanded = true },
|
onLongClick = { popupExpanded = true },
|
||||||
), isInnerNote)
|
), isInnerNote)
|
||||||
|
} else if (account?.isAcceptable(note) == false) {
|
||||||
|
HiddenNote(modifier.combinedClickable(
|
||||||
|
onClick = { },
|
||||||
|
onLongClick = { popupExpanded = true },
|
||||||
|
), isInnerNote)
|
||||||
} else {
|
} else {
|
||||||
val authorState by note.author!!.live.observeAsState()
|
val authorState by note.author!!.live.observeAsState()
|
||||||
val author = authorState?.user
|
val author = authorState?.user
|
||||||
@@ -195,6 +205,7 @@ fun NoteCompose(baseNote: Note, modifier: Modifier = Modifier, isInnerNote: Bool
|
|||||||
@Composable
|
@Composable
|
||||||
fun NoteDropDownMenu(note: Note, popupExpanded: Boolean, onDismiss: () -> Unit, accountViewModel: AccountViewModel) {
|
fun NoteDropDownMenu(note: Note, popupExpanded: Boolean, onDismiss: () -> Unit, accountViewModel: AccountViewModel) {
|
||||||
val clipboardManager = LocalClipboardManager.current
|
val clipboardManager = LocalClipboardManager.current
|
||||||
|
val context = LocalContext.current.applicationContext
|
||||||
|
|
||||||
DropdownMenu(
|
DropdownMenu(
|
||||||
expanded = popupExpanded,
|
expanded = popupExpanded,
|
||||||
@@ -213,5 +224,12 @@ fun NoteDropDownMenu(note: Note, popupExpanded: Boolean, onDismiss: () -> Unit,
|
|||||||
DropdownMenuItem(onClick = { accountViewModel.broadcast(note); onDismiss() }) {
|
DropdownMenuItem(onClick = { accountViewModel.broadcast(note); onDismiss() }) {
|
||||||
Text("Broadcast")
|
Text("Broadcast")
|
||||||
}
|
}
|
||||||
|
Divider()
|
||||||
|
DropdownMenuItem(onClick = { accountViewModel.report(note); onDismiss() }) {
|
||||||
|
Text("Report Post")
|
||||||
|
}
|
||||||
|
DropdownMenuItem(onClick = { note.author?.let { accountViewModel.hide(it, context) }; onDismiss() }) {
|
||||||
|
Text("Hide User")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -14,6 +14,8 @@ import androidx.compose.material.IconButton
|
|||||||
import androidx.compose.material.MaterialTheme
|
import androidx.compose.material.MaterialTheme
|
||||||
import androidx.compose.material.Text
|
import androidx.compose.material.Text
|
||||||
import androidx.compose.material.icons.Icons
|
import androidx.compose.material.icons.Icons
|
||||||
|
import androidx.compose.material.icons.filled.Download
|
||||||
|
import androidx.compose.material.icons.filled.MoreVert
|
||||||
import androidx.compose.material.icons.outlined.BarChart
|
import androidx.compose.material.icons.outlined.BarChart
|
||||||
import androidx.compose.material.icons.outlined.Visibility
|
import androidx.compose.material.icons.outlined.Visibility
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
@@ -39,11 +41,14 @@ import com.vitorpamplona.amethyst.R
|
|||||||
import com.vitorpamplona.amethyst.model.Account
|
import com.vitorpamplona.amethyst.model.Account
|
||||||
import com.vitorpamplona.amethyst.model.Note
|
import com.vitorpamplona.amethyst.model.Note
|
||||||
import com.vitorpamplona.amethyst.ui.actions.NewPostView
|
import com.vitorpamplona.amethyst.ui.actions.NewPostView
|
||||||
|
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun ReactionsRow(note: Note, account: Account, boost: (Note) -> Unit, reactTo: (Note) -> Unit) {
|
fun ReactionsRow(note: Note, account: Account, accountViewModel: AccountViewModel) {
|
||||||
val grayTint = MaterialTheme.colors.onSurface.copy(alpha = 0.32f)
|
val grayTint = MaterialTheme.colors.onSurface.copy(alpha = 0.32f)
|
||||||
|
|
||||||
|
var popupExpanded by remember { mutableStateOf(false) }
|
||||||
|
|
||||||
var wantsToReplyTo by remember {
|
var wantsToReplyTo by remember {
|
||||||
mutableStateOf<Note?>(null)
|
mutableStateOf<Note?>(null)
|
||||||
}
|
}
|
||||||
@@ -79,7 +84,7 @@ fun ReactionsRow(note: Note, account: Account, boost: (Note) -> Unit, reactTo: (
|
|||||||
|
|
||||||
IconButton(
|
IconButton(
|
||||||
modifier = Modifier.then(Modifier.size(24.dp)),
|
modifier = Modifier.then(Modifier.size(24.dp)),
|
||||||
onClick = { if (account.isWriteable()) boost(note) }
|
onClick = { if (account.isWriteable()) accountViewModel.boost(note) }
|
||||||
) {
|
) {
|
||||||
if (note.isBoostedBy(account.userProfile())) {
|
if (note.isBoostedBy(account.userProfile())) {
|
||||||
Icon(
|
Icon(
|
||||||
@@ -107,7 +112,7 @@ fun ReactionsRow(note: Note, account: Account, boost: (Note) -> Unit, reactTo: (
|
|||||||
|
|
||||||
IconButton(
|
IconButton(
|
||||||
modifier = Modifier.then(Modifier.size(24.dp)),
|
modifier = Modifier.then(Modifier.size(24.dp)),
|
||||||
onClick = { if (account.isWriteable()) reactTo(note) }
|
onClick = { if (account.isWriteable()) accountViewModel.reactTo(note) }
|
||||||
) {
|
) {
|
||||||
if (note.isReactedBy(account.userProfile())) {
|
if (note.isReactedBy(account.userProfile())) {
|
||||||
Icon(
|
Icon(
|
||||||
@@ -161,10 +166,22 @@ fun ReactionsRow(note: Note, account: Account, boost: (Note) -> Unit, reactTo: (
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
IconButton(
|
||||||
|
modifier = Modifier.then(Modifier.size(24.dp)),
|
||||||
|
onClick = { popupExpanded = true }
|
||||||
|
) {
|
||||||
|
Icon(
|
||||||
|
imageVector = Icons.Default.MoreVert,
|
||||||
|
null,
|
||||||
|
modifier = Modifier.size(15.dp),
|
||||||
|
tint = grayTint,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
NoteDropDownMenu(note, popupExpanded, { popupExpanded = false }, accountViewModel)
|
||||||
|
}
|
||||||
|
|
||||||
fun showCount(size: Int?): String {
|
fun showCount(size: Int?): String {
|
||||||
if (size == null) return " "
|
if (size == null) return " "
|
||||||
return if (size == 0) return " " else "$size"
|
return if (size == 0) return " " else "$size"
|
||||||
|
|||||||
@@ -16,5 +16,5 @@ fun ReactionsRowState(baseNote: Note, accountViewModel: AccountViewModel) {
|
|||||||
|
|
||||||
if (account == null || note == null) return
|
if (account == null || note == null) return
|
||||||
|
|
||||||
ReactionsRow(note, account, accountViewModel::boost, accountViewModel::reactTo)
|
ReactionsRow(note, account, accountViewModel)
|
||||||
}
|
}
|
||||||
@@ -16,23 +16,29 @@ import androidx.compose.runtime.livedata.observeAsState
|
|||||||
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.draw.clip
|
||||||
|
import androidx.compose.ui.platform.LocalContext
|
||||||
import androidx.compose.ui.text.style.TextOverflow
|
import androidx.compose.ui.text.style.TextOverflow
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.navigation.NavController
|
import androidx.navigation.NavController
|
||||||
import coil.compose.AsyncImage
|
import coil.compose.AsyncImage
|
||||||
import coil.compose.rememberAsyncImagePainter
|
import coil.compose.rememberAsyncImagePainter
|
||||||
|
import com.vitorpamplona.amethyst.LocalPreferences
|
||||||
import com.vitorpamplona.amethyst.model.User
|
import com.vitorpamplona.amethyst.model.User
|
||||||
import com.vitorpamplona.amethyst.ui.screen.FollowButton
|
import com.vitorpamplona.amethyst.ui.screen.FollowButton
|
||||||
|
import com.vitorpamplona.amethyst.ui.screen.ShowUserButton
|
||||||
import com.vitorpamplona.amethyst.ui.screen.UnfollowButton
|
import com.vitorpamplona.amethyst.ui.screen.UnfollowButton
|
||||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun UserCompose(baseUser: User, accountViewModel: AccountViewModel, navController: NavController) {
|
fun UserCompose(baseUser: User, accountViewModel: AccountViewModel, navController: NavController) {
|
||||||
val accountState by accountViewModel.accountLiveData.observeAsState()
|
val accountState by accountViewModel.accountLiveData.observeAsState()
|
||||||
|
val account = accountState?.account
|
||||||
|
|
||||||
val userState by baseUser.live.observeAsState()
|
val userState by baseUser.live.observeAsState()
|
||||||
val user = userState?.user ?: return
|
val user = userState?.user ?: return
|
||||||
|
|
||||||
|
val ctx = LocalContext.current.applicationContext
|
||||||
|
|
||||||
Column(modifier =
|
Column(modifier =
|
||||||
Modifier.clickable(
|
Modifier.clickable(
|
||||||
onClick = { navController.navigate("User/${user.pubkeyHex}") }
|
onClick = { navController.navigate("User/${user.pubkeyHex}") }
|
||||||
@@ -69,10 +75,15 @@ fun UserCompose(baseUser: User, accountViewModel: AccountViewModel, navControlle
|
|||||||
}
|
}
|
||||||
|
|
||||||
Column(modifier = Modifier.padding(start = 10.dp)) {
|
Column(modifier = Modifier.padding(start = 10.dp)) {
|
||||||
if (accountState?.account?.userProfile()?.isFollowing(user) == true) {
|
if (account?.isAcceptable(user) == false) {
|
||||||
UnfollowButton { accountState?.account?.unfollow(user) }
|
ShowUserButton {
|
||||||
|
account.showUser(user.pubkeyHex)
|
||||||
|
LocalPreferences(ctx).saveToEncryptedStorage(account)
|
||||||
|
}
|
||||||
|
} else if (account?.userProfile()?.isFollowing(user) == true) {
|
||||||
|
UnfollowButton { account.unfollow(user) }
|
||||||
} else {
|
} else {
|
||||||
FollowButton { accountState?.account?.follow(user) }
|
FollowButton { account?.follow(user) }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ package com.vitorpamplona.amethyst.ui.screen
|
|||||||
import androidx.lifecycle.ViewModel
|
import androidx.lifecycle.ViewModel
|
||||||
import androidx.lifecycle.viewModelScope
|
import androidx.lifecycle.viewModelScope
|
||||||
import androidx.security.crypto.EncryptedSharedPreferences
|
import androidx.security.crypto.EncryptedSharedPreferences
|
||||||
|
import com.vitorpamplona.amethyst.LocalPreferences
|
||||||
import com.vitorpamplona.amethyst.ServiceManager
|
import com.vitorpamplona.amethyst.ServiceManager
|
||||||
import com.vitorpamplona.amethyst.model.Account
|
import com.vitorpamplona.amethyst.model.Account
|
||||||
import com.vitorpamplona.amethyst.model.DefaultChannels
|
import com.vitorpamplona.amethyst.model.DefaultChannels
|
||||||
@@ -30,15 +31,13 @@ import nostr.postr.Persona
|
|||||||
import nostr.postr.bechToBytes
|
import nostr.postr.bechToBytes
|
||||||
import nostr.postr.toHex
|
import nostr.postr.toHex
|
||||||
|
|
||||||
class AccountStateViewModel(
|
class AccountStateViewModel(private val localPreferences: LocalPreferences): ViewModel() {
|
||||||
private val encryptedPreferences: EncryptedSharedPreferences
|
|
||||||
): ViewModel() {
|
|
||||||
private val _accountContent = MutableStateFlow<AccountState>(AccountState.LoggedOff)
|
private val _accountContent = MutableStateFlow<AccountState>(AccountState.LoggedOff)
|
||||||
val accountContent = _accountContent.asStateFlow()
|
val accountContent = _accountContent.asStateFlow()
|
||||||
|
|
||||||
init {
|
init {
|
||||||
// pulls account from storage.
|
// pulls account from storage.
|
||||||
loadFromEncryptedStorage()?.let {
|
localPreferences.loadFromEncryptedStorage()?.let {
|
||||||
login(it)
|
login(it)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -58,14 +57,14 @@ class AccountStateViewModel(
|
|||||||
Account(Persona(Hex.decode(key)))
|
Account(Persona(Hex.decode(key)))
|
||||||
}
|
}
|
||||||
|
|
||||||
saveToEncryptedStorage(account)
|
localPreferences.saveToEncryptedStorage(account)
|
||||||
|
|
||||||
login(account)
|
login(account)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun newKey() {
|
fun newKey() {
|
||||||
val account = Account(Persona())
|
val account = Account(Persona())
|
||||||
saveToEncryptedStorage(account)
|
localPreferences.saveToEncryptedStorage(account)
|
||||||
login(account)
|
login(account)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -83,36 +82,6 @@ class AccountStateViewModel(
|
|||||||
fun logOff() {
|
fun logOff() {
|
||||||
_accountContent.update { AccountState.LoggedOff }
|
_accountContent.update { AccountState.LoggedOff }
|
||||||
|
|
||||||
clearEncryptedStorage()
|
localPreferences.clearEncryptedStorage()
|
||||||
}
|
|
||||||
|
|
||||||
fun clearEncryptedStorage() {
|
|
||||||
encryptedPreferences.edit().apply {
|
|
||||||
remove("nostr_privkey")
|
|
||||||
remove("nostr_pubkey")
|
|
||||||
remove("following_channels")
|
|
||||||
}.apply()
|
|
||||||
}
|
|
||||||
|
|
||||||
fun saveToEncryptedStorage(account: Account) {
|
|
||||||
encryptedPreferences.edit().apply {
|
|
||||||
account.loggedIn.privKey?.let { putString("nostr_privkey", it.toHex()) }
|
|
||||||
account.loggedIn.pubKey.let { putString("nostr_pubkey", it.toHex()) }
|
|
||||||
account.followingChannels.let { putStringSet("following_channels", account.followingChannels) }
|
|
||||||
}.apply()
|
|
||||||
}
|
|
||||||
|
|
||||||
fun loadFromEncryptedStorage(): Account? {
|
|
||||||
encryptedPreferences.apply {
|
|
||||||
val privKey = getString("nostr_privkey", null)
|
|
||||||
val pubKey = getString("nostr_pubkey", null)
|
|
||||||
val followingChannels = getStringSet("following_channels", DefaultChannels)?.toMutableSet() ?: DefaultChannels.toMutableSet()
|
|
||||||
|
|
||||||
if (pubKey != null) {
|
|
||||||
return Account(Persona(privKey = privKey?.toByteArray(), pubKey = pubKey.toByteArray()), followingChannels)
|
|
||||||
} else {
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -8,6 +8,7 @@ import com.vitorpamplona.amethyst.model.LocalCache
|
|||||||
import com.vitorpamplona.amethyst.model.LocalCacheState
|
import com.vitorpamplona.amethyst.model.LocalCacheState
|
||||||
import com.vitorpamplona.amethyst.model.User
|
import com.vitorpamplona.amethyst.model.User
|
||||||
import com.vitorpamplona.amethyst.service.NostrDataSource
|
import com.vitorpamplona.amethyst.service.NostrDataSource
|
||||||
|
import com.vitorpamplona.amethyst.service.NostrHiddenAccountsDataSource
|
||||||
import com.vitorpamplona.amethyst.service.NostrUserProfileFollowersDataSource
|
import com.vitorpamplona.amethyst.service.NostrUserProfileFollowersDataSource
|
||||||
import com.vitorpamplona.amethyst.service.NostrUserProfileFollowsDataSource
|
import com.vitorpamplona.amethyst.service.NostrUserProfileFollowsDataSource
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
@@ -28,6 +29,10 @@ class NostrUserProfileFollowersUserFeedViewModel(): UserFeedViewModel(
|
|||||||
NostrUserProfileFollowersDataSource
|
NostrUserProfileFollowersDataSource
|
||||||
)
|
)
|
||||||
|
|
||||||
|
class NostrHiddenAccountsFeedViewModel(): UserFeedViewModel(
|
||||||
|
NostrHiddenAccountsDataSource
|
||||||
|
)
|
||||||
|
|
||||||
open class UserFeedViewModel(val dataSource: NostrDataSource<User>): ViewModel() {
|
open class UserFeedViewModel(val dataSource: NostrDataSource<User>): ViewModel() {
|
||||||
private val _feedContent = MutableStateFlow<UserFeedState>(UserFeedState.Loading)
|
private val _feedContent = MutableStateFlow<UserFeedState>(UserFeedState.Loading)
|
||||||
val feedContent = _feedContent.asStateFlow()
|
val feedContent = _feedContent.asStateFlow()
|
||||||
|
|||||||
@@ -1,11 +1,14 @@
|
|||||||
package com.vitorpamplona.amethyst.ui.screen.loggedIn
|
package com.vitorpamplona.amethyst.ui.screen.loggedIn
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
import androidx.lifecycle.LiveData
|
import androidx.lifecycle.LiveData
|
||||||
import androidx.lifecycle.Transformations
|
import androidx.lifecycle.Transformations
|
||||||
import androidx.lifecycle.ViewModel
|
import androidx.lifecycle.ViewModel
|
||||||
|
import com.vitorpamplona.amethyst.LocalPreferences
|
||||||
import com.vitorpamplona.amethyst.model.Account
|
import com.vitorpamplona.amethyst.model.Account
|
||||||
import com.vitorpamplona.amethyst.model.AccountState
|
import com.vitorpamplona.amethyst.model.AccountState
|
||||||
import com.vitorpamplona.amethyst.model.Note
|
import com.vitorpamplona.amethyst.model.Note
|
||||||
|
import com.vitorpamplona.amethyst.model.User
|
||||||
import com.vitorpamplona.amethyst.model.UserState
|
import com.vitorpamplona.amethyst.model.UserState
|
||||||
|
|
||||||
class AccountViewModel(private val account: Account): ViewModel() {
|
class AccountViewModel(private val account: Account): ViewModel() {
|
||||||
@@ -15,6 +18,10 @@ class AccountViewModel(private val account: Account): ViewModel() {
|
|||||||
account.reactTo(note)
|
account.reactTo(note)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun report(note: Note) {
|
||||||
|
account.report(note)
|
||||||
|
}
|
||||||
|
|
||||||
fun boost(note: Note) {
|
fun boost(note: Note) {
|
||||||
account.boost(note)
|
account.boost(note)
|
||||||
}
|
}
|
||||||
@@ -26,4 +33,14 @@ class AccountViewModel(private val account: Account): ViewModel() {
|
|||||||
fun decrypt(note: Note): String? {
|
fun decrypt(note: Note): String? {
|
||||||
return account.decryptContent(note)
|
return account.decryptContent(note)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun hide(user: User, ctx: Context) {
|
||||||
|
account.hideUser(user.pubkeyHex)
|
||||||
|
LocalPreferences(ctx).saveToEncryptedStorage(account)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun show(user: User, ctx: Context) {
|
||||||
|
account.showUser(user.pubkeyHex)
|
||||||
|
LocalPreferences(ctx).saveToEncryptedStorage(account)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -30,6 +30,7 @@ import androidx.compose.ui.draw.clip
|
|||||||
import androidx.compose.ui.graphics.Color
|
import androidx.compose.ui.graphics.Color
|
||||||
import androidx.compose.ui.platform.ClipboardManager
|
import androidx.compose.ui.platform.ClipboardManager
|
||||||
import androidx.compose.ui.platform.LocalClipboardManager
|
import androidx.compose.ui.platform.LocalClipboardManager
|
||||||
|
import androidx.compose.ui.platform.LocalContext
|
||||||
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.text.input.KeyboardCapitalization
|
import androidx.compose.ui.text.input.KeyboardCapitalization
|
||||||
@@ -41,6 +42,7 @@ import androidx.lifecycle.viewmodel.compose.viewModel
|
|||||||
import androidx.navigation.NavController
|
import androidx.navigation.NavController
|
||||||
import coil.compose.AsyncImage
|
import coil.compose.AsyncImage
|
||||||
import coil.compose.rememberAsyncImagePainter
|
import coil.compose.rememberAsyncImagePainter
|
||||||
|
import com.vitorpamplona.amethyst.LocalPreferences
|
||||||
import com.vitorpamplona.amethyst.model.Account
|
import com.vitorpamplona.amethyst.model.Account
|
||||||
import com.vitorpamplona.amethyst.model.Channel
|
import com.vitorpamplona.amethyst.model.Channel
|
||||||
import com.vitorpamplona.amethyst.model.Note
|
import com.vitorpamplona.amethyst.model.Note
|
||||||
@@ -169,12 +171,12 @@ fun ChannelHeader(baseChannel: Channel, account: Account, accountStateViewModel:
|
|||||||
|
|
||||||
channel?.let {
|
channel?.let {
|
||||||
if (channel.creator == account.userProfile()) {
|
if (channel.creator == account.userProfile()) {
|
||||||
EditButton(account, it, accountStateViewModel)
|
EditButton(account, it)
|
||||||
} else {
|
} else {
|
||||||
if (account.followingChannels.contains(channel.idHex)) {
|
if (account.followingChannels.contains(channel.idHex)) {
|
||||||
LeaveButton(account,channel,accountStateViewModel, navController)
|
LeaveButton(account,channel, navController)
|
||||||
} else {
|
} else {
|
||||||
JoinButton(account,channel,accountStateViewModel, navController)
|
JoinButton(account,channel, navController)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -209,13 +211,13 @@ private fun NoteCopyButton(
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
private fun EditButton(account: Account, channel: Channel, accountStateViewModel: AccountStateViewModel) {
|
private fun EditButton(account: Account, channel: Channel) {
|
||||||
var wantsToPost by remember {
|
var wantsToPost by remember {
|
||||||
mutableStateOf(false)
|
mutableStateOf(false)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (wantsToPost)
|
if (wantsToPost)
|
||||||
NewChannelView({ wantsToPost = false }, account = account, accountStateViewModel, channel)
|
NewChannelView({ wantsToPost = false }, account = account, channel)
|
||||||
|
|
||||||
Button(
|
Button(
|
||||||
modifier = Modifier.padding(horizontal = 3.dp),
|
modifier = Modifier.padding(horizontal = 3.dp),
|
||||||
@@ -231,11 +233,14 @@ private fun EditButton(account: Account, channel: Channel, accountStateViewModel
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
private fun JoinButton(account: Account, channel: Channel, accountStateViewModel: AccountStateViewModel, navController: NavController) {
|
private fun JoinButton(account: Account, channel: Channel, navController: NavController) {
|
||||||
|
val context = LocalContext.current.applicationContext
|
||||||
|
|
||||||
Button(
|
Button(
|
||||||
modifier = Modifier.padding(horizontal = 3.dp),
|
modifier = Modifier.padding(horizontal = 3.dp),
|
||||||
onClick = {
|
onClick = {
|
||||||
account.joinChannel(channel.idHex, accountStateViewModel)
|
account.joinChannel(channel.idHex)
|
||||||
|
LocalPreferences(context).saveToEncryptedStorage(account)
|
||||||
navController.navigate(Route.Message.route)
|
navController.navigate(Route.Message.route)
|
||||||
},
|
},
|
||||||
shape = RoundedCornerShape(20.dp),
|
shape = RoundedCornerShape(20.dp),
|
||||||
@@ -249,11 +254,14 @@ private fun JoinButton(account: Account, channel: Channel, accountStateViewModel
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
private fun LeaveButton(account: Account, channel: Channel, accountStateViewModel: AccountStateViewModel, navController: NavController) {
|
private fun LeaveButton(account: Account, channel: Channel, navController: NavController) {
|
||||||
|
val context = LocalContext.current.applicationContext
|
||||||
|
|
||||||
Button(
|
Button(
|
||||||
modifier = Modifier.padding(horizontal = 3.dp),
|
modifier = Modifier.padding(horizontal = 3.dp),
|
||||||
onClick = {
|
onClick = {
|
||||||
account.leaveChannel(channel.idHex, accountStateViewModel)
|
account.leaveChannel(channel.idHex)
|
||||||
|
LocalPreferences(context).saveToEncryptedStorage(account)
|
||||||
navController.navigate(Route.Message.route)
|
navController.navigate(Route.Message.route)
|
||||||
},
|
},
|
||||||
shape = RoundedCornerShape(20.dp),
|
shape = RoundedCornerShape(20.dp),
|
||||||
|
|||||||
@@ -0,0 +1,70 @@
|
|||||||
|
package com.vitorpamplona.amethyst.ui.screen
|
||||||
|
|
||||||
|
import androidx.compose.foundation.layout.Column
|
||||||
|
import androidx.compose.foundation.layout.fillMaxHeight
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.material.MaterialTheme
|
||||||
|
import androidx.compose.material.Tab
|
||||||
|
import androidx.compose.material.TabRow
|
||||||
|
import androidx.compose.material.TabRowDefaults
|
||||||
|
import androidx.compose.material.Text
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.DisposableEffect
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.runtime.livedata.observeAsState
|
||||||
|
import androidx.compose.runtime.rememberCoroutineScope
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import androidx.lifecycle.viewmodel.compose.viewModel
|
||||||
|
import androidx.navigation.NavController
|
||||||
|
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.service.NostrHiddenAccountsDataSource
|
||||||
|
import com.vitorpamplona.amethyst.service.NostrThreadDataSource
|
||||||
|
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
|
||||||
|
@OptIn(ExperimentalPagerApi::class)
|
||||||
|
@Composable
|
||||||
|
fun FiltersScreen(accountViewModel: AccountViewModel, navController: NavController) {
|
||||||
|
val accountState by accountViewModel.accountLiveData.observeAsState()
|
||||||
|
val account = accountState?.account
|
||||||
|
|
||||||
|
if (account != null) {
|
||||||
|
NostrHiddenAccountsDataSource.account = account
|
||||||
|
|
||||||
|
val feedViewModel: NostrHiddenAccountsFeedViewModel = viewModel()
|
||||||
|
|
||||||
|
Column(Modifier.fillMaxHeight()) {
|
||||||
|
Column(modifier = Modifier.padding(start = 10.dp, end = 10.dp)) {
|
||||||
|
val pagerState = rememberPagerState()
|
||||||
|
val coroutineScope = rememberCoroutineScope()
|
||||||
|
|
||||||
|
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 = "Blocked Users")
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
HorizontalPager(count = 1, state = pagerState) {
|
||||||
|
when (pagerState.currentPage) {
|
||||||
|
0 -> UserFeedView(feedViewModel, accountViewModel, navController)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -85,7 +85,7 @@ fun FloatingButton(navController: NavHostController, accountViewModel: AccountSt
|
|||||||
// Does nothing.
|
// Does nothing.
|
||||||
}
|
}
|
||||||
is AccountState.LoggedIn -> {
|
is AccountState.LoggedIn -> {
|
||||||
NewChannelButton(state.account, accountViewModel)
|
NewChannelButton(state.account)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -41,6 +41,7 @@ import androidx.compose.ui.graphics.Color
|
|||||||
import androidx.compose.ui.layout.ContentScale
|
import androidx.compose.ui.layout.ContentScale
|
||||||
import androidx.compose.ui.platform.ClipboardManager
|
import androidx.compose.ui.platform.ClipboardManager
|
||||||
import androidx.compose.ui.platform.LocalClipboardManager
|
import androidx.compose.ui.platform.LocalClipboardManager
|
||||||
|
import androidx.compose.ui.platform.LocalContext
|
||||||
import androidx.compose.ui.res.painterResource
|
import androidx.compose.ui.res.painterResource
|
||||||
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
|
||||||
@@ -54,6 +55,7 @@ import com.google.accompanist.pager.ExperimentalPagerApi
|
|||||||
import com.google.accompanist.pager.HorizontalPager
|
import com.google.accompanist.pager.HorizontalPager
|
||||||
import com.google.accompanist.pager.pagerTabIndicatorOffset
|
import com.google.accompanist.pager.pagerTabIndicatorOffset
|
||||||
import com.google.accompanist.pager.rememberPagerState
|
import com.google.accompanist.pager.rememberPagerState
|
||||||
|
import com.vitorpamplona.amethyst.LocalPreferences
|
||||||
import com.vitorpamplona.amethyst.R
|
import com.vitorpamplona.amethyst.R
|
||||||
import com.vitorpamplona.amethyst.model.Account
|
import com.vitorpamplona.amethyst.model.Account
|
||||||
import com.vitorpamplona.amethyst.model.Note
|
import com.vitorpamplona.amethyst.model.Note
|
||||||
@@ -67,6 +69,7 @@ import com.vitorpamplona.amethyst.ui.actions.NewUserMetadataView
|
|||||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import nostr.postr.toNpub
|
import nostr.postr.toNpub
|
||||||
|
import nostr.postr.toNsec
|
||||||
|
|
||||||
@OptIn(ExperimentalPagerApi::class)
|
@OptIn(ExperimentalPagerApi::class)
|
||||||
@Composable
|
@Composable
|
||||||
@@ -77,6 +80,8 @@ fun ProfileScreen(userId: String?, accountViewModel: AccountViewModel, navContro
|
|||||||
val accountUserState by account.userProfile().live.observeAsState()
|
val accountUserState by account.userProfile().live.observeAsState()
|
||||||
val accountUser = accountUserState?.user
|
val accountUser = accountUserState?.user
|
||||||
|
|
||||||
|
val ctx = LocalContext.current.applicationContext
|
||||||
|
|
||||||
if (userId != null && accountUser != null) {
|
if (userId != null && accountUser != null) {
|
||||||
DisposableEffect(account) {
|
DisposableEffect(account) {
|
||||||
NostrUserProfileDataSource.loadUserProfile(userId)
|
NostrUserProfileDataSource.loadUserProfile(userId)
|
||||||
@@ -145,12 +150,21 @@ fun ProfileScreen(userId: String?, accountViewModel: AccountViewModel, navContro
|
|||||||
|
|
||||||
MessageButton(user, navController)
|
MessageButton(user, navController)
|
||||||
|
|
||||||
|
if (accountUser == user && account.isWriteable()) {
|
||||||
|
NSecCopyButton(account)
|
||||||
|
}
|
||||||
|
|
||||||
NPubCopyButton(user)
|
NPubCopyButton(user)
|
||||||
|
|
||||||
if (accountUser == user) {
|
if (accountUser == user) {
|
||||||
EditButton(account)
|
EditButton(account)
|
||||||
} else {
|
} else {
|
||||||
if (accountUser.isFollowing(user)) {
|
if (account?.isAcceptable(user) == false) {
|
||||||
|
ShowUserButton {
|
||||||
|
account.showUser(user.pubkeyHex)
|
||||||
|
LocalPreferences(ctx).saveToEncryptedStorage(account)
|
||||||
|
}
|
||||||
|
} else if (accountUser.isFollowing(user)) {
|
||||||
UnfollowButton { account.unfollow(user) }
|
UnfollowButton { account.unfollow(user) }
|
||||||
} else {
|
} else {
|
||||||
FollowButton { account.follow(user) }
|
FollowButton { account.follow(user) }
|
||||||
@@ -267,6 +281,25 @@ fun TabFollowers(user: User, accountViewModel: AccountViewModel, navController:
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun NSecCopyButton(
|
||||||
|
account: Account
|
||||||
|
) {
|
||||||
|
val clipboardManager = LocalClipboardManager.current
|
||||||
|
|
||||||
|
Button(
|
||||||
|
modifier = Modifier.padding(horizontal = 3.dp),
|
||||||
|
onClick = { account.loggedIn.privKey?.let { clipboardManager.setText(AnnotatedString(it.toNsec())) } },
|
||||||
|
shape = RoundedCornerShape(20.dp),
|
||||||
|
colors = ButtonDefaults
|
||||||
|
.buttonColors(
|
||||||
|
backgroundColor = MaterialTheme.colors.onSurface.copy(alpha = 0.32f)
|
||||||
|
),
|
||||||
|
) {
|
||||||
|
Text(text = "nsec", color = Color.White)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
private fun NPubCopyButton(
|
private fun NPubCopyButton(
|
||||||
user: User
|
user: User
|
||||||
@@ -358,4 +391,18 @@ fun FollowButton(onClick: () -> Unit) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun ShowUserButton(onClick: () -> Unit) {
|
||||||
|
Button(
|
||||||
|
modifier = Modifier.padding(start = 3.dp),
|
||||||
|
onClick = onClick,
|
||||||
|
shape = RoundedCornerShape(20.dp),
|
||||||
|
colors = ButtonDefaults
|
||||||
|
.buttonColors(
|
||||||
|
backgroundColor = MaterialTheme.colors.primary
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
Text(text = "Unblock", color = Color.White)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -40,6 +40,7 @@ import androidx.compose.ui.res.painterResource
|
|||||||
import androidx.compose.ui.text.font.FontWeight
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
import androidx.compose.ui.text.input.KeyboardCapitalization
|
import androidx.compose.ui.text.input.KeyboardCapitalization
|
||||||
import androidx.compose.ui.text.input.TextFieldValue
|
import androidx.compose.ui.text.input.TextFieldValue
|
||||||
|
import androidx.compose.ui.text.style.TextOverflow
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.lifecycle.viewmodel.compose.viewModel
|
import androidx.lifecycle.viewmodel.compose.viewModel
|
||||||
import androidx.navigation.NavController
|
import androidx.navigation.NavController
|
||||||
@@ -54,6 +55,7 @@ import com.vitorpamplona.amethyst.service.NostrSearchEventOrUserDataSource
|
|||||||
import com.vitorpamplona.amethyst.service.NostrThreadDataSource
|
import com.vitorpamplona.amethyst.service.NostrThreadDataSource
|
||||||
import com.vitorpamplona.amethyst.ui.note.ChannelName
|
import com.vitorpamplona.amethyst.ui.note.ChannelName
|
||||||
import com.vitorpamplona.amethyst.ui.note.NoteCompose
|
import com.vitorpamplona.amethyst.ui.note.NoteCompose
|
||||||
|
import com.vitorpamplona.amethyst.ui.note.UserCompose
|
||||||
import com.vitorpamplona.amethyst.ui.note.UsernameDisplay
|
import com.vitorpamplona.amethyst.ui.note.UsernameDisplay
|
||||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
||||||
|
|
||||||
@@ -167,9 +169,7 @@ private fun SearchBar(accountViewModel: AccountViewModel, navController: NavCont
|
|||||||
)
|
)
|
||||||
) {
|
) {
|
||||||
itemsIndexed(searchResults.value, key = { _, item -> "u"+item.pubkeyHex }) { index, item ->
|
itemsIndexed(searchResults.value, key = { _, item -> "u"+item.pubkeyHex }) { index, item ->
|
||||||
UserLine(item) {
|
UserCompose(item, accountViewModel = accountViewModel, navController = navController)
|
||||||
navController.navigate("User/${item.pubkeyHex}")
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
itemsIndexed(searchResultsChannels.value, key = { _, item -> "c"+item.idHex }) { index, item ->
|
itemsIndexed(searchResultsChannels.value, key = { _, item -> "c"+item.idHex }) { index, item ->
|
||||||
@@ -236,7 +236,9 @@ fun UserLine(
|
|||||||
|
|
||||||
Text(
|
Text(
|
||||||
user.info.about?.take(100) ?: "",
|
user.info.about?.take(100) ?: "",
|
||||||
color = MaterialTheme.colors.onSurface.copy(alpha = 0.32f)
|
color = MaterialTheme.colors.onSurface.copy(alpha = 0.32f),
|
||||||
|
maxLines = 1,
|
||||||
|
overflow = TextOverflow.Ellipsis
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user