mirror of
https://github.com/vitorpamplona/amethyst.git
synced 2026-08-10 00:16:59 +00:00
- Refactoring feeds use of the pullrefresh
- Adding more places to check if in the main thread.
This commit is contained in:
@@ -567,16 +567,15 @@ class Account(
|
||||
LocalCache.consume(signedEvent, null)
|
||||
}
|
||||
|
||||
fun sendPrivateMessage(message: String, toUser: String, replyingTo: Note? = null, mentions: List<User>?, zapReceiver: String? = null, wantsToMarkAsSensitive: Boolean) {
|
||||
fun sendPrivateMessage(message: String, toUser: User, replyingTo: Note? = null, mentions: List<User>?, zapReceiver: String? = null, wantsToMarkAsSensitive: Boolean) {
|
||||
if (!isWriteable()) return
|
||||
val user = LocalCache.users[toUser] ?: return
|
||||
|
||||
val repliesToHex = listOfNotNull(replyingTo?.idHex).ifEmpty { null }
|
||||
val mentionsHex = mentions?.map { it.pubkeyHex }
|
||||
|
||||
val signedEvent = PrivateDmEvent.create(
|
||||
recipientPubKey = user.pubkey(),
|
||||
publishedRecipientPubKey = user.pubkey(),
|
||||
recipientPubKey = toUser.pubkey(),
|
||||
publishedRecipientPubKey = toUser.pubkey(),
|
||||
msg = message,
|
||||
replyTos = repliesToHex,
|
||||
mentions = mentionsHex,
|
||||
@@ -1135,7 +1134,9 @@ class Account(
|
||||
|
||||
// saves contact list for the next time.
|
||||
userProfile().live().follows.observeForever {
|
||||
updateContactListTo(userProfile().latestContactList)
|
||||
GlobalScope.launch(Dispatchers.IO) {
|
||||
updateContactListTo(userProfile().latestContactList)
|
||||
}
|
||||
}
|
||||
|
||||
// imports transient blocks due to spam.
|
||||
|
||||
@@ -3,7 +3,9 @@ package com.vitorpamplona.amethyst.model
|
||||
import android.util.Log
|
||||
import android.util.LruCache
|
||||
import androidx.lifecycle.LiveData
|
||||
import com.vitorpamplona.amethyst.service.checkNotInMainThread
|
||||
import com.vitorpamplona.amethyst.service.model.Event
|
||||
import com.vitorpamplona.amethyst.service.nip19.Nip19
|
||||
import com.vitorpamplona.amethyst.service.relays.Relay
|
||||
import com.vitorpamplona.amethyst.ui.components.BundledUpdate
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
@@ -16,6 +18,8 @@ class AntiSpamFilter {
|
||||
|
||||
@Synchronized
|
||||
fun isSpam(event: Event, relay: Relay?): Boolean {
|
||||
checkNotInMainThread()
|
||||
|
||||
val idHex = event.id
|
||||
|
||||
// if short message, ok
|
||||
@@ -28,6 +32,7 @@ class AntiSpamFilter {
|
||||
val hash = (event.content + event.tags.flatten().joinToString(",")).hashCode()
|
||||
|
||||
if ((recentMessages[hash] != null && recentMessages[hash] != idHex) || spamMessages[hash] != null) {
|
||||
Log.w("Potential SPAM Message for sharing", "${Nip19.createNEvent(event.id, event.pubKey, event.kind, null)}")
|
||||
Log.w("Potential SPAM Message", "${event.id} ${recentMessages[hash]} ${spamMessages[hash] != null} ${relay?.url} ${event.content.replace("\n", " | ")}")
|
||||
|
||||
// Log down offenders
|
||||
|
||||
@@ -32,6 +32,8 @@ object LocalCache {
|
||||
ConcurrentHashMap<HexKey, Pair<Note?, (LnZapPaymentResponseEvent) -> Unit>>(10)
|
||||
|
||||
fun checkGetOrCreateUser(key: String): User? {
|
||||
checkNotInMainThread()
|
||||
|
||||
if (isValidHexNpub(key)) {
|
||||
return getOrCreateUser(key)
|
||||
}
|
||||
@@ -40,6 +42,8 @@ object LocalCache {
|
||||
|
||||
@Synchronized
|
||||
fun getOrCreateUser(key: HexKey): User {
|
||||
// checkNotInMainThread()
|
||||
|
||||
return users[key] ?: run {
|
||||
val answer = User(key)
|
||||
users.put(key, answer)
|
||||
@@ -48,6 +52,8 @@ object LocalCache {
|
||||
}
|
||||
|
||||
fun checkGetOrCreateNote(key: String): Note? {
|
||||
checkNotInMainThread()
|
||||
|
||||
if (ATag.isATag(key)) {
|
||||
return checkGetOrCreateAddressableNote(key)
|
||||
}
|
||||
@@ -59,6 +65,8 @@ object LocalCache {
|
||||
|
||||
@Synchronized
|
||||
fun getOrCreateNote(idHex: String): Note {
|
||||
checkNotInMainThread()
|
||||
|
||||
return notes[idHex] ?: run {
|
||||
val answer = Note(idHex)
|
||||
notes.put(idHex, answer)
|
||||
@@ -67,6 +75,8 @@ object LocalCache {
|
||||
}
|
||||
|
||||
fun checkGetOrCreateChannel(key: String): Channel? {
|
||||
checkNotInMainThread()
|
||||
|
||||
if (isValidHexNpub(key)) {
|
||||
return getOrCreateChannel(key)
|
||||
}
|
||||
@@ -85,6 +95,8 @@ object LocalCache {
|
||||
|
||||
@Synchronized
|
||||
fun getOrCreateChannel(key: String): Channel {
|
||||
checkNotInMainThread()
|
||||
|
||||
return channels[key] ?: run {
|
||||
val answer = Channel(key)
|
||||
channels.put(key, answer)
|
||||
@@ -108,6 +120,8 @@ object LocalCache {
|
||||
|
||||
@Synchronized
|
||||
fun getOrCreateAddressableNote(key: ATag): AddressableNote {
|
||||
checkNotInMainThread()
|
||||
|
||||
// we can't use naddr here because naddr might include relay info and
|
||||
// the preferred relay should not be part of the index.
|
||||
return addressables[key.toTag()] ?: run {
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
package com.vitorpamplona.amethyst.model
|
||||
|
||||
import com.vitorpamplona.amethyst.service.checkNotInMainThread
|
||||
import com.vitorpamplona.amethyst.service.previews.BahaUrlPreview
|
||||
import com.vitorpamplona.amethyst.service.previews.IUrlPreviewCallback
|
||||
import com.vitorpamplona.amethyst.service.previews.UrlInfoItem
|
||||
@@ -11,6 +12,8 @@ object UrlCachedPreviewer {
|
||||
private set
|
||||
|
||||
fun previewInfo(url: String, callback: IUrlPreviewCallback? = null) {
|
||||
checkNotInMainThread()
|
||||
|
||||
cache[url]?.let {
|
||||
callback?.onComplete(it)
|
||||
return
|
||||
|
||||
@@ -4,6 +4,7 @@ import androidx.compose.runtime.Immutable
|
||||
import androidx.compose.runtime.Stable
|
||||
import androidx.lifecycle.LiveData
|
||||
import com.vitorpamplona.amethyst.service.NostrSingleUserDataSource
|
||||
import com.vitorpamplona.amethyst.service.checkNotInMainThread
|
||||
import com.vitorpamplona.amethyst.service.model.BookmarkListEvent
|
||||
import com.vitorpamplona.amethyst.service.model.ContactListEvent
|
||||
import com.vitorpamplona.amethyst.service.model.LnZapEvent
|
||||
@@ -185,6 +186,8 @@ class User(val pubkeyHex: String) {
|
||||
|
||||
@Synchronized
|
||||
private fun getOrCreatePrivateChatroom(user: User): Chatroom {
|
||||
checkNotInMainThread()
|
||||
|
||||
return privateChatrooms[user] ?: run {
|
||||
val privateChatroom = Chatroom(setOf<Note>())
|
||||
privateChatrooms = privateChatrooms + Pair(user, privateChatroom)
|
||||
@@ -194,6 +197,8 @@ class User(val pubkeyHex: String) {
|
||||
|
||||
@Synchronized
|
||||
fun addMessage(user: User, msg: Note) {
|
||||
checkNotInMainThread()
|
||||
|
||||
val privateChatroom = getOrCreatePrivateChatroom(user)
|
||||
if (msg !in privateChatroom.roomMessages) {
|
||||
privateChatroom.roomMessages = privateChatroom.roomMessages + msg
|
||||
@@ -203,6 +208,8 @@ class User(val pubkeyHex: String) {
|
||||
|
||||
@Synchronized
|
||||
fun removeMessage(user: User, msg: Note) {
|
||||
checkNotInMainThread()
|
||||
|
||||
val privateChatroom = getOrCreatePrivateChatroom(user)
|
||||
if (msg in privateChatroom.roomMessages) {
|
||||
privateChatroom.roomMessages = privateChatroom.roomMessages - msg
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
package com.vitorpamplona.amethyst.service
|
||||
|
||||
import android.os.Looper
|
||||
import com.vitorpamplona.amethyst.BuildConfig
|
||||
|
||||
fun checkNotInMainThread() {
|
||||
if (isMainThread()) throw OnMainThreadException("It should not be in the MainThread")
|
||||
if (BuildConfig.DEBUG && isMainThread()) throw OnMainThreadException("It should not be in the MainThread")
|
||||
}
|
||||
|
||||
fun isMainThread() = Looper.myLooper() == Looper.getMainLooper()
|
||||
|
||||
@@ -2,7 +2,6 @@ package com.vitorpamplona.amethyst.service
|
||||
|
||||
import com.vitorpamplona.amethyst.model.Account
|
||||
import com.vitorpamplona.amethyst.model.Channel
|
||||
import com.vitorpamplona.amethyst.model.LocalCache
|
||||
import com.vitorpamplona.amethyst.service.model.ChannelMessageEvent
|
||||
import com.vitorpamplona.amethyst.service.relays.FeedType
|
||||
import com.vitorpamplona.amethyst.service.relays.JsonFilter
|
||||
@@ -12,9 +11,9 @@ object NostrChannelDataSource : NostrDataSource("ChatroomFeed") {
|
||||
var account: Account? = null
|
||||
var channel: Channel? = null
|
||||
|
||||
fun loadMessagesBetween(account: Account, channelId: String) {
|
||||
fun loadMessagesBetween(account: Account, channel: Channel) {
|
||||
this.account = account
|
||||
channel = LocalCache.getOrCreateChannel(channelId)
|
||||
this.channel = channel
|
||||
resetFilters()
|
||||
}
|
||||
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
package com.vitorpamplona.amethyst.service
|
||||
|
||||
import com.vitorpamplona.amethyst.model.Account
|
||||
import com.vitorpamplona.amethyst.model.LocalCache
|
||||
import com.vitorpamplona.amethyst.model.User
|
||||
import com.vitorpamplona.amethyst.service.model.PrivateDmEvent
|
||||
import com.vitorpamplona.amethyst.service.relays.FeedType
|
||||
@@ -12,9 +11,9 @@ object NostrChatroomDataSource : NostrDataSource("ChatroomFeed") {
|
||||
lateinit var account: Account
|
||||
var withUser: User? = null
|
||||
|
||||
fun loadMessagesBetween(accountIn: Account, userId: String) {
|
||||
fun loadMessagesBetween(accountIn: Account, user: User) {
|
||||
account = accountIn
|
||||
withUser = LocalCache.users[userId]
|
||||
withUser = user
|
||||
resetFilters()
|
||||
}
|
||||
|
||||
|
||||
@@ -123,6 +123,8 @@ abstract class NostrDataSource(val debugName: String) {
|
||||
}
|
||||
|
||||
fun resetFiltersSuspend() {
|
||||
checkNotInMainThread()
|
||||
|
||||
// saves the channels that are currently active
|
||||
val activeSubscriptions = subscriptions.values.filter { it.typedFilters != null }
|
||||
// saves the current content to only update if it changes
|
||||
|
||||
+13
-5
@@ -13,15 +13,22 @@ import com.vitorpamplona.amethyst.service.model.PrivateDmEvent
|
||||
import com.vitorpamplona.amethyst.service.notifications.NotificationUtils.sendDMNotification
|
||||
import com.vitorpamplona.amethyst.service.notifications.NotificationUtils.sendZapNotification
|
||||
import com.vitorpamplona.amethyst.ui.note.showAmount
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
class EventNotificationConsumer(private val applicationContext: Context) {
|
||||
fun consume(event: Event) {
|
||||
// adds to database
|
||||
LocalCache.consume(event, null)
|
||||
val scope = CoroutineScope(Job() + Dispatchers.IO)
|
||||
scope.launch {
|
||||
// adds to database
|
||||
LocalCache.consume(event, null)
|
||||
|
||||
when (event) {
|
||||
is PrivateDmEvent -> notify(event)
|
||||
is LnZapEvent -> notify(event)
|
||||
when (event) {
|
||||
is PrivateDmEvent -> notify(event)
|
||||
is LnZapEvent -> notify(event)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -54,6 +61,7 @@ class EventNotificationConsumer(private val applicationContext: Context) {
|
||||
|
||||
private fun notify(event: LnZapEvent) {
|
||||
val noteZapEvent = LocalCache.notes[event.id] ?: return
|
||||
|
||||
val noteZapRequest = event.zapRequest?.id?.let { LocalCache.checkGetOrCreateNote(it) }
|
||||
val noteZapped = event.zappedPost().firstOrNull()?.let { LocalCache.checkGetOrCreateNote(it) }
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package com.vitorpamplona.amethyst.service.relays
|
||||
|
||||
import com.vitorpamplona.amethyst.service.HttpClient
|
||||
import com.vitorpamplona.amethyst.service.checkNotInMainThread
|
||||
import com.vitorpamplona.amethyst.service.model.Event
|
||||
import com.vitorpamplona.amethyst.service.model.EventInterface
|
||||
import kotlinx.coroutines.DelicateCoroutinesApi
|
||||
@@ -32,6 +33,8 @@ object Client : RelayPool.Listener {
|
||||
|
||||
@Synchronized
|
||||
fun connect(relays: Array<Relay>) {
|
||||
checkNotInMainThread()
|
||||
|
||||
RelayPool.register(this)
|
||||
RelayPool.unloadRelays()
|
||||
RelayPool.loadRelays(relays.toList())
|
||||
@@ -54,6 +57,8 @@ object Client : RelayPool.Listener {
|
||||
subscriptionId: String = UUID.randomUUID().toString().substring(0..10),
|
||||
filters: List<TypedFilter> = listOf()
|
||||
) {
|
||||
checkNotInMainThread()
|
||||
|
||||
subscriptions = subscriptions + Pair(subscriptionId, filters)
|
||||
RelayPool.sendFilter(subscriptionId)
|
||||
}
|
||||
@@ -62,11 +67,15 @@ object Client : RelayPool.Listener {
|
||||
subscriptionId: String = UUID.randomUUID().toString().substring(0..10),
|
||||
filters: List<TypedFilter> = listOf()
|
||||
) {
|
||||
checkNotInMainThread()
|
||||
|
||||
subscriptions = subscriptions + Pair(subscriptionId, filters)
|
||||
RelayPool.sendFilterOnlyIfDisconnected()
|
||||
}
|
||||
|
||||
fun send(signedEvent: EventInterface, relay: String? = null, feedTypes: Set<FeedType>? = null, onDone: (() -> Unit)? = null) {
|
||||
checkNotInMainThread()
|
||||
|
||||
if (relay == null) {
|
||||
RelayPool.send(signedEvent)
|
||||
} else {
|
||||
@@ -91,7 +100,7 @@ object Client : RelayPool.Listener {
|
||||
}
|
||||
|
||||
@OptIn(DelicateCoroutinesApi::class)
|
||||
fun newSporadicRelay(url: String, feedTypes: Set<FeedType>?, onConnected: (Relay) -> Unit, onDone: (() -> Unit)?) {
|
||||
private fun newSporadicRelay(url: String, feedTypes: Set<FeedType>?, onConnected: (Relay) -> Unit, onDone: (() -> Unit)?) {
|
||||
val relay = Relay(url, true, true, feedTypes ?: emptySet(), HttpClient.getProxy())
|
||||
RelayPool.addRelay(relay)
|
||||
|
||||
|
||||
@@ -3,6 +3,7 @@ package com.vitorpamplona.amethyst.service.relays
|
||||
import android.util.Log
|
||||
import com.google.gson.JsonElement
|
||||
import com.vitorpamplona.amethyst.BuildConfig
|
||||
import com.vitorpamplona.amethyst.service.checkNotInMainThread
|
||||
import com.vitorpamplona.amethyst.service.model.Event
|
||||
import com.vitorpamplona.amethyst.service.model.EventInterface
|
||||
import com.vitorpamplona.amethyst.service.model.RelayAuthEvent
|
||||
@@ -69,6 +70,7 @@ class Relay(
|
||||
|
||||
@Synchronized
|
||||
fun requestAndWatch() {
|
||||
checkNotInMainThread()
|
||||
requestAndWatch {
|
||||
// Sends everything.
|
||||
Client.allSubscriptions().forEach {
|
||||
@@ -79,6 +81,7 @@ class Relay(
|
||||
|
||||
@Synchronized
|
||||
fun requestAndWatch(onConnected: (Relay) -> Unit) {
|
||||
checkNotInMainThread()
|
||||
if (socket != null) return
|
||||
|
||||
try {
|
||||
|
||||
@@ -14,6 +14,7 @@ import androidx.compose.material.OutlinedTextField
|
||||
import androidx.compose.material.Surface
|
||||
import androidx.compose.material.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.rememberCoroutineScope
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.res.stringResource
|
||||
@@ -26,6 +27,8 @@ import androidx.lifecycle.viewmodel.compose.viewModel
|
||||
import com.vitorpamplona.amethyst.R
|
||||
import com.vitorpamplona.amethyst.model.Channel
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
@Composable
|
||||
fun NewChannelView(onClose: () -> Unit, accountViewModel: AccountViewModel, channel: Channel? = null) {
|
||||
@@ -53,10 +56,14 @@ fun NewChannelView(onClose: () -> Unit, accountViewModel: AccountViewModel, chan
|
||||
onClose()
|
||||
})
|
||||
|
||||
val scope = rememberCoroutineScope()
|
||||
|
||||
PostButton(
|
||||
onPost = {
|
||||
postViewModel.create()
|
||||
onClose()
|
||||
scope.launch(Dispatchers.IO) {
|
||||
postViewModel.create()
|
||||
onClose()
|
||||
}
|
||||
},
|
||||
postViewModel.channelName.value.text.isNotBlank()
|
||||
)
|
||||
@@ -100,7 +107,9 @@ fun NewChannelView(onClose: () -> Unit, accountViewModel: AccountViewModel, chan
|
||||
|
||||
OutlinedTextField(
|
||||
label = { Text(text = stringResource(R.string.description)) },
|
||||
modifier = Modifier.fillMaxWidth().height(100.dp),
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.height(100.dp),
|
||||
value = postViewModel.channelDescription.value,
|
||||
onValueChange = { postViewModel.channelDescription.value = it },
|
||||
placeholder = {
|
||||
|
||||
@@ -143,8 +143,10 @@ fun NewPostView(onClose: () -> Unit, baseReplyTo: Note? = null, quote: Note? = n
|
||||
|
||||
PostButton(
|
||||
onPost = {
|
||||
postViewModel.sendPost()
|
||||
onClose()
|
||||
scope.launch(Dispatchers.IO) {
|
||||
postViewModel.sendPost()
|
||||
onClose()
|
||||
}
|
||||
},
|
||||
isActive = postViewModel.canPost()
|
||||
)
|
||||
|
||||
@@ -24,7 +24,7 @@ import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.flow.MutableSharedFlow
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
open class NewPostViewModel : ViewModel() {
|
||||
open class NewPostViewModel() : ViewModel() {
|
||||
var account: Account? = null
|
||||
var originalNote: Note? = null
|
||||
|
||||
@@ -132,7 +132,7 @@ open class NewPostViewModel : ViewModel() {
|
||||
} else if (originalNote?.channelHex() != null) {
|
||||
account?.sendChannelMessage(tagger.message, tagger.channelHex!!, tagger.replyTos, tagger.mentions, zapReceiver, wantsToMarkAsSensitive)
|
||||
} else if (originalNote?.event is PrivateDmEvent) {
|
||||
account?.sendPrivateMessage(tagger.message, originalNote!!.author!!.pubkeyHex, originalNote!!, tagger.mentions, zapReceiver, wantsToMarkAsSensitive)
|
||||
account?.sendPrivateMessage(tagger.message, originalNote!!.author!!, originalNote!!, tagger.mentions, zapReceiver, wantsToMarkAsSensitive)
|
||||
} else {
|
||||
account?.sendPost(tagger.message, tagger.replyTos, tagger.mentions, null, zapReceiver, wantsToMarkAsSensitive)
|
||||
}
|
||||
@@ -226,7 +226,11 @@ open class NewPostViewModel : ViewModel() {
|
||||
userSuggestionsMainMessage = true
|
||||
if (lastWord.startsWith("@") && lastWord.length > 2) {
|
||||
NostrSearchEventOrUserDataSource.search(lastWord.removePrefix("@"))
|
||||
userSuggestions = LocalCache.findUsersStartingWith(lastWord.removePrefix("@")).sortedWith(compareBy({ account?.isFollowing(it) }, { it.toBestDisplayName() })).reversed()
|
||||
viewModelScope.launch(Dispatchers.IO) {
|
||||
userSuggestions = LocalCache.findUsersStartingWith(lastWord.removePrefix("@"))
|
||||
.sortedWith(compareBy({ account?.isFollowing(it) }, { it.toBestDisplayName() }))
|
||||
.reversed()
|
||||
}
|
||||
} else {
|
||||
NostrSearchEventOrUserDataSource.clear()
|
||||
userSuggestions = emptyList()
|
||||
@@ -242,7 +246,15 @@ open class NewPostViewModel : ViewModel() {
|
||||
userSuggestionsMainMessage = false
|
||||
if (lastWord.startsWith("@") && lastWord.length > 2) {
|
||||
NostrSearchEventOrUserDataSource.search(lastWord.removePrefix("@"))
|
||||
userSuggestions = LocalCache.findUsersStartingWith(lastWord.removePrefix("@")).sortedWith(compareBy({ account?.isFollowing(it) }, { it.toBestDisplayName() })).reversed()
|
||||
viewModelScope.launch(Dispatchers.IO) {
|
||||
userSuggestions = LocalCache.findUsersStartingWith(lastWord.removePrefix("@"))
|
||||
.sortedWith(
|
||||
compareBy(
|
||||
{ account?.isFollowing(it) },
|
||||
{ it.toBestDisplayName() }
|
||||
)
|
||||
).reversed()
|
||||
}
|
||||
} else {
|
||||
NostrSearchEventOrUserDataSource.clear()
|
||||
userSuggestions = emptyList()
|
||||
|
||||
@@ -68,7 +68,9 @@ fun ClickableRoute(
|
||||
DisplayEvent(nip19, nav)
|
||||
} else {
|
||||
Text(
|
||||
"@${nip19.hex}${nip19.additionalChars} "
|
||||
remember {
|
||||
"@${nip19.hex}${nip19.additionalChars} "
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -93,19 +95,20 @@ private fun DisplayEvent(
|
||||
val note = remember(noteState) { noteState?.note } ?: return
|
||||
val channelHex = remember(noteState) { note.channelHex() }
|
||||
val noteIdDisplayNote = remember(noteState) { "@${note.idDisplayNote()}" }
|
||||
val addedCharts = remember { "${nip19.additionalChars} " }
|
||||
|
||||
if (note.event is ChannelCreateEvent) {
|
||||
CreateClickableText(
|
||||
clickablePart = noteIdDisplayNote,
|
||||
suffix = "${nip19.additionalChars} ",
|
||||
route = "Channel/${nip19.hex}",
|
||||
suffix = addedCharts,
|
||||
route = remember(noteState) { "Channel/${nip19.hex}" },
|
||||
nav = nav
|
||||
)
|
||||
} else if (note.event is PrivateDmEvent) {
|
||||
CreateClickableText(
|
||||
clickablePart = noteIdDisplayNote,
|
||||
suffix = "${nip19.additionalChars} ",
|
||||
route = "Room/${note.author?.pubkeyHex}",
|
||||
suffix = addedCharts,
|
||||
route = remember(noteState) { "Room/${note.author?.pubkeyHex}" },
|
||||
nav = nav
|
||||
)
|
||||
} else if (channelHex != null) {
|
||||
@@ -119,16 +122,16 @@ private fun DisplayEvent(
|
||||
|
||||
CreateClickableText(
|
||||
clickablePart = channelDisplayName,
|
||||
suffix = "${nip19.additionalChars} ",
|
||||
route = "Channel/${baseChannel.idHex}",
|
||||
suffix = addedCharts,
|
||||
route = remember(noteState) { "Channel/${baseChannel.idHex}" },
|
||||
nav = nav
|
||||
)
|
||||
}
|
||||
} else {
|
||||
CreateClickableText(
|
||||
clickablePart = noteIdDisplayNote,
|
||||
suffix = "${nip19.additionalChars} ",
|
||||
route = "Event/${nip19.hex}",
|
||||
suffix = addedCharts,
|
||||
route = remember(noteState) { "Event/${nip19.hex}" },
|
||||
nav = nav
|
||||
)
|
||||
}
|
||||
@@ -136,7 +139,9 @@ private fun DisplayEvent(
|
||||
|
||||
if (noteBase == null) {
|
||||
Text(
|
||||
"@${nip19.hex}${nip19.additionalChars} "
|
||||
remember {
|
||||
"@${nip19.hex}${nip19.additionalChars} "
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -157,21 +162,22 @@ private fun DisplayNote(
|
||||
noteBase?.let {
|
||||
val noteState by it.live().metadata.observeAsState()
|
||||
val note = remember(noteState) { noteState?.note } ?: return
|
||||
val channelHex = note.channelHex()
|
||||
val channelHex = remember(noteState) { note.channelHex() }
|
||||
val noteIdDisplayNote = remember(noteState) { "@${note.idDisplayNote()}" }
|
||||
val addedCharts = remember { "${nip19.additionalChars} " }
|
||||
|
||||
if (note.event is ChannelCreateEvent) {
|
||||
CreateClickableText(
|
||||
clickablePart = noteIdDisplayNote,
|
||||
suffix = "${nip19.additionalChars} ",
|
||||
route = "Channel/${nip19.hex}",
|
||||
suffix = addedCharts,
|
||||
route = remember(noteState) { "Channel/${nip19.hex}" },
|
||||
nav = nav
|
||||
)
|
||||
} else if (note.event is PrivateDmEvent) {
|
||||
CreateClickableText(
|
||||
clickablePart = noteIdDisplayNote,
|
||||
suffix = "${nip19.additionalChars} ",
|
||||
route = "Room/${note.author?.pubkeyHex}",
|
||||
suffix = addedCharts,
|
||||
route = remember(noteState) { "Room/${note.author?.pubkeyHex}" },
|
||||
nav = nav
|
||||
)
|
||||
} else if (channelHex != null) {
|
||||
@@ -185,16 +191,16 @@ private fun DisplayNote(
|
||||
|
||||
CreateClickableText(
|
||||
clickablePart = channelDisplayName,
|
||||
suffix = "${nip19.additionalChars} ",
|
||||
route = "Channel/${baseChannel.idHex}",
|
||||
suffix = addedCharts,
|
||||
route = remember(noteState) { "Channel/${baseChannel.idHex}" },
|
||||
nav = nav
|
||||
)
|
||||
}
|
||||
} else {
|
||||
CreateClickableText(
|
||||
clickablePart = noteIdDisplayNote,
|
||||
suffix = "${nip19.additionalChars} ",
|
||||
route = "Note/${nip19.hex}",
|
||||
suffix = addedCharts,
|
||||
route = remember(noteState) { "Note/${nip19.hex}" },
|
||||
nav = nav
|
||||
)
|
||||
}
|
||||
@@ -202,7 +208,9 @@ private fun DisplayNote(
|
||||
|
||||
if (noteBase == null) {
|
||||
Text(
|
||||
"@${nip19.hex}${nip19.additionalChars} "
|
||||
remember {
|
||||
"@${nip19.hex}${nip19.additionalChars} "
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -222,19 +230,24 @@ private fun DisplayAddress(
|
||||
|
||||
noteBase?.let {
|
||||
val noteState by it.live().metadata.observeAsState()
|
||||
val note = remember(noteState) { noteState?.note } ?: return
|
||||
|
||||
val route = remember(noteState) { "Note/${nip19.hex}" }
|
||||
val displayName = remember(noteState) { "@${noteState?.note?.idDisplayNote()}" }
|
||||
val addedCharts = remember { "${nip19.additionalChars} " }
|
||||
|
||||
CreateClickableText(
|
||||
clickablePart = "@${note.idDisplayNote()}",
|
||||
suffix = "${nip19.additionalChars} ",
|
||||
route = "Note/${nip19.hex}",
|
||||
clickablePart = displayName,
|
||||
suffix = addedCharts,
|
||||
route = route,
|
||||
nav = nav
|
||||
)
|
||||
}
|
||||
|
||||
if (noteBase == null) {
|
||||
Text(
|
||||
"@${nip19.hex}${nip19.additionalChars} "
|
||||
remember {
|
||||
"@${nip19.hex}${nip19.additionalChars} "
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -257,11 +270,14 @@ private fun DisplayUser(
|
||||
val route = remember(userState) { "User/${it.pubkeyHex}" }
|
||||
val userDisplayName = remember(userState) { userState?.user?.toBestDisplayName() }
|
||||
val userTags = remember(userState) { userState?.user?.info?.latestMetadata?.tags }
|
||||
val addedCharts = remember {
|
||||
"${nip19.additionalChars} "
|
||||
}
|
||||
|
||||
if (userDisplayName != null) {
|
||||
CreateClickableTextWithEmoji(
|
||||
clickablePart = userDisplayName,
|
||||
suffix = "${nip19.additionalChars} ",
|
||||
suffix = addedCharts,
|
||||
tags = userTags,
|
||||
route = route,
|
||||
nav = nav
|
||||
@@ -271,7 +287,9 @@ private fun DisplayUser(
|
||||
|
||||
if (userBase == null) {
|
||||
Text(
|
||||
"@${nip19.hex}${nip19.additionalChars} "
|
||||
remember {
|
||||
"@${nip19.hex}${nip19.additionalChars} "
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ import androidx.compose.foundation.text.ClickableText
|
||||
import androidx.compose.material.LocalTextStyle
|
||||
import androidx.compose.material.MaterialTheme
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.platform.LocalUriHandler
|
||||
import androidx.compose.ui.text.AnnotatedString
|
||||
|
||||
@@ -11,10 +12,16 @@ import androidx.compose.ui.text.AnnotatedString
|
||||
fun ClickableUrl(urlText: String, url: String) {
|
||||
val uri = LocalUriHandler.current
|
||||
|
||||
val doubleCheckedUrl = if (url.contains("://")) url else "https://$url"
|
||||
val doubleCheckedUrl = remember(url) {
|
||||
if (url.contains("://")) url else "https://$url"
|
||||
}
|
||||
|
||||
val text = remember(urlText) {
|
||||
AnnotatedString(urlText)
|
||||
}
|
||||
|
||||
ClickableText(
|
||||
text = AnnotatedString(urlText),
|
||||
text = text,
|
||||
onClick = { runCatching { uri.openUri(doubleCheckedUrl) } },
|
||||
style = LocalTextStyle.current.copy(color = MaterialTheme.colors.primary)
|
||||
)
|
||||
|
||||
@@ -6,6 +6,7 @@ import androidx.compose.material.MaterialTheme
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.livedata.observeAsState
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.text.AnnotatedString
|
||||
import com.vitorpamplona.amethyst.model.User
|
||||
|
||||
@@ -14,10 +15,19 @@ fun ClickableUserTag(
|
||||
user: User,
|
||||
nav: (String) -> Unit
|
||||
) {
|
||||
val route = remember {
|
||||
"User/${user.pubkeyHex}"
|
||||
}
|
||||
|
||||
val innerUserState by user.live().metadata.observeAsState()
|
||||
|
||||
val userName = remember(innerUserState) {
|
||||
AnnotatedString("@${innerUserState?.user?.toBestDisplayName()}")
|
||||
}
|
||||
|
||||
ClickableText(
|
||||
text = AnnotatedString("@${innerUserState?.user?.toBestDisplayName()}"),
|
||||
onClick = { nav("User/${innerUserState?.user?.pubkeyHex}") },
|
||||
text = userName,
|
||||
onClick = { nav(route) },
|
||||
style = LocalTextStyle.current.copy(color = MaterialTheme.colors.primary)
|
||||
)
|
||||
}
|
||||
|
||||
@@ -13,14 +13,14 @@ import androidx.compose.ui.text.style.TextDirection
|
||||
import androidx.core.content.ContextCompat
|
||||
import com.vitorpamplona.amethyst.service.lnurl.LnWithdrawalUtil
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.withContext
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
@Composable
|
||||
fun MayBeWithdrawal(lnurlWord: String) {
|
||||
var lnWithdrawal by remember { mutableStateOf<String?>(null) }
|
||||
|
||||
LaunchedEffect(key1 = lnurlWord) {
|
||||
withContext(Dispatchers.IO) {
|
||||
launch(Dispatchers.IO) {
|
||||
lnWithdrawal = LnWithdrawalUtil.findWithdrawal(lnurlWord)
|
||||
}
|
||||
}
|
||||
@@ -38,11 +38,19 @@ fun MayBeWithdrawal(lnurlWord: String) {
|
||||
fun ClickableWithdrawal(withdrawalString: String) {
|
||||
val context = LocalContext.current
|
||||
|
||||
val uri = remember(withdrawalString) {
|
||||
Uri.parse("lightning:$withdrawalString")
|
||||
}
|
||||
|
||||
val withdraw = remember(withdrawalString) {
|
||||
AnnotatedString("$withdrawalString ")
|
||||
}
|
||||
|
||||
ClickableText(
|
||||
text = AnnotatedString("$withdrawalString "),
|
||||
text = withdraw,
|
||||
onClick = {
|
||||
runCatching {
|
||||
val intent = Intent(Intent.ACTION_VIEW, Uri.parse("lightning:$withdrawalString"))
|
||||
val intent = Intent(Intent.ACTION_VIEW, uri)
|
||||
ContextCompat.startActivity(context, intent, null)
|
||||
}
|
||||
},
|
||||
|
||||
+21
-12
@@ -27,6 +27,7 @@ import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.vitorpamplona.amethyst.R
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
|
||||
const val SHORT_TEXT_LENGTH = 350
|
||||
|
||||
@@ -34,8 +35,8 @@ const val SHORT_TEXT_LENGTH = 350
|
||||
fun ExpandableRichTextViewer(
|
||||
content: String,
|
||||
canPreview: Boolean,
|
||||
modifier: Modifier = Modifier,
|
||||
tags: List<List<String>>,
|
||||
modifier: Modifier,
|
||||
tags: ImmutableList<List<String>>,
|
||||
backgroundColor: Color,
|
||||
accountViewModel: AccountViewModel,
|
||||
nav: (String) -> Unit
|
||||
@@ -87,18 +88,26 @@ fun ExpandableRichTextViewer(
|
||||
)
|
||||
)
|
||||
) {
|
||||
Button(
|
||||
modifier = Modifier.padding(top = 10.dp),
|
||||
onClick = { showFullText = !showFullText },
|
||||
shape = RoundedCornerShape(20.dp),
|
||||
colors = ButtonDefaults.buttonColors(
|
||||
backgroundColor = MaterialTheme.colors.primary.copy(alpha = 0.32f).compositeOver(MaterialTheme.colors.background)
|
||||
),
|
||||
contentPadding = PaddingValues(vertical = 6.dp, horizontal = 16.dp)
|
||||
) {
|
||||
Text(text = stringResource(R.string.show_more), color = Color.White)
|
||||
ShowMoreButton() {
|
||||
showFullText = !showFullText
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun ShowMoreButton(onClick: () -> Unit) {
|
||||
Button(
|
||||
modifier = Modifier.padding(top = 10.dp),
|
||||
onClick = onClick,
|
||||
shape = RoundedCornerShape(20.dp),
|
||||
colors = ButtonDefaults.buttonColors(
|
||||
backgroundColor = MaterialTheme.colors.primary.copy(alpha = 0.32f)
|
||||
.compositeOver(MaterialTheme.colors.background)
|
||||
),
|
||||
contentPadding = PaddingValues(vertical = 6.dp, horizontal = 16.dp)
|
||||
) {
|
||||
Text(text = stringResource(R.string.show_more), color = Color.White)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -97,8 +97,8 @@ fun isMarkdown(content: String): Boolean {
|
||||
fun RichTextViewer(
|
||||
content: String,
|
||||
canPreview: Boolean,
|
||||
modifier: Modifier = Modifier,
|
||||
tags: List<List<String>>,
|
||||
modifier: Modifier,
|
||||
tags: ImmutableList<List<String>>,
|
||||
backgroundColor: Color,
|
||||
accountViewModel: AccountViewModel,
|
||||
nav: (String) -> Unit
|
||||
@@ -126,7 +126,7 @@ data class RichTextViewerState(
|
||||
@Composable
|
||||
private fun RenderRegular(
|
||||
content: String,
|
||||
tags: List<List<String>>,
|
||||
tags: ImmutableList<List<String>>,
|
||||
canPreview: Boolean,
|
||||
backgroundColor: Color,
|
||||
accountViewModel: AccountViewModel,
|
||||
@@ -217,7 +217,7 @@ private fun RenderWord(
|
||||
backgroundColor: Color,
|
||||
accountViewModel: AccountViewModel,
|
||||
nav: (String) -> Unit,
|
||||
tags: List<List<String>>
|
||||
tags: ImmutableList<List<String>>
|
||||
) {
|
||||
val type = remember(word) {
|
||||
if (state.imagesForPager[word] != null) {
|
||||
@@ -267,7 +267,7 @@ private fun RenderWordWithoutPreview(
|
||||
type: WordType,
|
||||
word: String,
|
||||
state: RichTextViewerState,
|
||||
tags: List<List<String>>,
|
||||
tags: ImmutableList<List<String>>,
|
||||
backgroundColor: Color,
|
||||
accountViewModel: AccountViewModel,
|
||||
nav: (String) -> Unit
|
||||
@@ -300,7 +300,7 @@ private fun RenderWordWithPreview(
|
||||
type: WordType,
|
||||
word: String,
|
||||
state: RichTextViewerState,
|
||||
tags: List<List<String>>,
|
||||
tags: ImmutableList<List<String>>,
|
||||
backgroundColor: Color,
|
||||
accountViewModel: AccountViewModel,
|
||||
nav: (String) -> Unit
|
||||
@@ -382,7 +382,7 @@ fun RenderCustomEmoji(word: String, state: RichTextViewerState) {
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun RenderContentAsMarkdown(content: String, backgroundColor: Color, tags: List<List<String>>?, nav: (String) -> Unit) {
|
||||
private fun RenderContentAsMarkdown(content: String, backgroundColor: Color, tags: ImmutableList<List<String>>?, nav: (String) -> Unit) {
|
||||
val myMarkDownStyle = richTextDefaults.copy(
|
||||
codeBlockStyle = richTextDefaults.codeBlockStyle?.copy(
|
||||
textStyle = TextStyle(
|
||||
|
||||
@@ -1,23 +1,12 @@
|
||||
package com.vitorpamplona.amethyst.ui.dal
|
||||
|
||||
import com.vitorpamplona.amethyst.model.Account
|
||||
import com.vitorpamplona.amethyst.model.LocalCache
|
||||
import com.vitorpamplona.amethyst.model.Channel
|
||||
import com.vitorpamplona.amethyst.model.Note
|
||||
|
||||
object ChannelFeedFilter : AdditiveFeedFilter<Note>() {
|
||||
lateinit var account: Account
|
||||
var channelId: String? = null
|
||||
|
||||
fun loadMessagesBetween(accountLoggedIn: Account, channelId: String?) {
|
||||
this.account = accountLoggedIn
|
||||
this.channelId = channelId
|
||||
}
|
||||
|
||||
class ChannelFeedFilter(val channel: Channel, val account: Account) : AdditiveFeedFilter<Note>() {
|
||||
// returns the last Note of each user.
|
||||
override fun feed(): List<Note> {
|
||||
val processingChannel = channelId ?: return emptyList()
|
||||
val channel = LocalCache.getOrCreateChannel(processingChannel)
|
||||
|
||||
return channel.notes
|
||||
.values
|
||||
.filter { account.isAcceptable(it) }
|
||||
@@ -26,9 +15,6 @@ object ChannelFeedFilter : AdditiveFeedFilter<Note>() {
|
||||
}
|
||||
|
||||
override fun applyFilter(collection: Set<Note>): Set<Note> {
|
||||
val processingChannel = channelId ?: return emptySet()
|
||||
val channel = LocalCache.getOrCreateChannel(processingChannel)
|
||||
|
||||
return collection
|
||||
.filter { it.idHex in channel.notes.keys && account.isAcceptable(it) }
|
||||
.toSet()
|
||||
|
||||
@@ -1,51 +1,29 @@
|
||||
package com.vitorpamplona.amethyst.ui.dal
|
||||
|
||||
import com.vitorpamplona.amethyst.model.Account
|
||||
import com.vitorpamplona.amethyst.model.LocalCache
|
||||
import com.vitorpamplona.amethyst.model.Note
|
||||
import com.vitorpamplona.amethyst.model.User
|
||||
|
||||
object ChatroomFeedFilter : AdditiveFeedFilter<Note>() {
|
||||
var account: Account? = null
|
||||
var withUser: String? = null
|
||||
|
||||
fun loadMessagesBetween(accountIn: Account, userId: String) {
|
||||
account = accountIn
|
||||
withUser = userId
|
||||
}
|
||||
|
||||
class ChatroomFeedFilter(val withUser: User, val account: Account) : AdditiveFeedFilter<Note>() {
|
||||
// returns the last Note of each user.
|
||||
override fun feed(): List<Note> {
|
||||
val processingUser = withUser ?: return emptyList()
|
||||
|
||||
val myAccount = account
|
||||
val myUser = LocalCache.checkGetOrCreateUser(processingUser)
|
||||
|
||||
if (myAccount == null || myUser == null) return emptyList()
|
||||
|
||||
val messages = myAccount
|
||||
val messages = account
|
||||
.userProfile()
|
||||
.privateChatrooms[myUser] ?: return emptyList()
|
||||
.privateChatrooms[withUser] ?: return emptyList()
|
||||
|
||||
return messages.roomMessages
|
||||
.filter { myAccount.isAcceptable(it) }
|
||||
.filter { account.isAcceptable(it) }
|
||||
.sortedWith(compareBy({ it.createdAt() }, { it.idHex }))
|
||||
.reversed()
|
||||
}
|
||||
|
||||
override fun applyFilter(collection: Set<Note>): Set<Note> {
|
||||
val processingUser = withUser ?: return emptySet()
|
||||
|
||||
val myAccount = account
|
||||
val myUser = LocalCache.checkGetOrCreateUser(processingUser)
|
||||
|
||||
if (myAccount == null || myUser == null) return emptySet()
|
||||
|
||||
val messages = myAccount
|
||||
val messages = account
|
||||
.userProfile()
|
||||
.privateChatrooms[myUser] ?: return emptySet()
|
||||
.privateChatrooms[withUser] ?: return emptySet()
|
||||
|
||||
return collection
|
||||
.filter { it in messages.roomMessages && account?.isAcceptable(it) == true }
|
||||
.filter { it in messages.roomMessages && account.isAcceptable(it) == true }
|
||||
.toSet()
|
||||
}
|
||||
|
||||
|
||||
@@ -1,12 +1,15 @@
|
||||
package com.vitorpamplona.amethyst.ui.dal
|
||||
|
||||
import android.util.Log
|
||||
import com.vitorpamplona.amethyst.service.checkNotInMainThread
|
||||
import kotlin.time.ExperimentalTime
|
||||
import kotlin.time.measureTimedValue
|
||||
|
||||
abstract class FeedFilter<T> {
|
||||
@OptIn(ExperimentalTime::class)
|
||||
fun loadTop(): List<T> {
|
||||
checkNotInMainThread()
|
||||
|
||||
val (feed, elapsed) = measureTimedValue {
|
||||
feed()
|
||||
}
|
||||
@@ -24,6 +27,8 @@ abstract class AdditiveFeedFilter<T> : FeedFilter<T>() {
|
||||
|
||||
@OptIn(ExperimentalTime::class)
|
||||
open fun updateListWith(oldList: List<T>, newItems: Set<T>): List<T> {
|
||||
checkNotInMainThread()
|
||||
|
||||
val (feed, elapsed) = measureTimedValue {
|
||||
val newItemsToBeAdded = applyFilter(newItems)
|
||||
if (newItemsToBeAdded.isNotEmpty()) {
|
||||
|
||||
@@ -67,6 +67,7 @@ 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.checkNotInMainThread
|
||||
import com.vitorpamplona.amethyst.service.model.PeopleListEvent
|
||||
import com.vitorpamplona.amethyst.service.relays.Client
|
||||
import com.vitorpamplona.amethyst.service.relays.RelayPool
|
||||
@@ -347,6 +348,8 @@ class FollowListViewModel(val account: Account) : ViewModel() {
|
||||
}
|
||||
|
||||
private suspend fun refreshFollows() {
|
||||
checkNotInMainThread()
|
||||
|
||||
val newFollowLists = LocalCache.addressables.mapNotNull {
|
||||
val event = (it.value.event as? PeopleListEvent)
|
||||
// Has to have an list
|
||||
|
||||
@@ -23,7 +23,6 @@ import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.livedata.observeAsState
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.rememberCoroutineScope
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
@@ -42,8 +41,10 @@ import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import com.vitorpamplona.amethyst.NotificationCache
|
||||
import com.vitorpamplona.amethyst.R
|
||||
import com.vitorpamplona.amethyst.model.Channel
|
||||
import com.vitorpamplona.amethyst.model.LocalCache
|
||||
import com.vitorpamplona.amethyst.model.Note
|
||||
import com.vitorpamplona.amethyst.model.User
|
||||
import com.vitorpamplona.amethyst.service.model.ChannelCreateEvent
|
||||
import com.vitorpamplona.amethyst.service.model.ChannelMetadataEvent
|
||||
import com.vitorpamplona.amethyst.service.model.PrivateDmEvent
|
||||
@@ -62,11 +63,6 @@ fun ChatroomCompose(
|
||||
val noteState by baseNote.live().metadata.observeAsState()
|
||||
val note = noteState?.note
|
||||
|
||||
val notificationCacheState = NotificationCache.live.observeAsState()
|
||||
val notificationCache = notificationCacheState.value ?: return
|
||||
|
||||
val scope = rememberCoroutineScope()
|
||||
|
||||
val channelHex by remember(noteState) {
|
||||
derivedStateOf {
|
||||
noteState?.note?.channelHex()
|
||||
@@ -77,125 +73,168 @@ fun ChatroomCompose(
|
||||
BlankNote(Modifier)
|
||||
} else if (channelHex != null) {
|
||||
LoadChannel(baseChannelHex = channelHex!!) { channel ->
|
||||
val authorState by note.author!!.live().metadata.observeAsState()
|
||||
val authorName = remember(authorState) {
|
||||
authorState?.user?.toBestDisplayName()
|
||||
}
|
||||
|
||||
val chanHex = remember { channel.idHex }
|
||||
|
||||
val channelState by channel.live.observeAsState()
|
||||
val channelPicture by remember(channelState) {
|
||||
derivedStateOf {
|
||||
channel.profilePicture()
|
||||
}
|
||||
}
|
||||
val channelName by remember(channelState) {
|
||||
derivedStateOf {
|
||||
channel.info.name
|
||||
}
|
||||
}
|
||||
|
||||
val noteEvent = note.event
|
||||
|
||||
val description = if (noteEvent is ChannelCreateEvent) {
|
||||
stringResource(R.string.channel_created)
|
||||
} else if (noteEvent is ChannelMetadataEvent) {
|
||||
"${stringResource(R.string.channel_information_changed_to)} "
|
||||
} else {
|
||||
noteEvent?.content()
|
||||
}
|
||||
|
||||
var hasNewMessages by remember { mutableStateOf<Boolean>(false) }
|
||||
|
||||
LaunchedEffect(key1 = notificationCache, key2 = note) {
|
||||
scope.launch(Dispatchers.IO) {
|
||||
note.createdAt()?.let { timestamp ->
|
||||
hasNewMessages =
|
||||
timestamp > notificationCache.cache.load("Channel/$chanHex")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ChannelName(
|
||||
channelIdHex = chanHex,
|
||||
channelPicture = channelPicture,
|
||||
channelTitle = { modifier ->
|
||||
Text(
|
||||
text = buildAnnotatedString {
|
||||
withStyle(
|
||||
SpanStyle(
|
||||
fontWeight = FontWeight.Bold
|
||||
)
|
||||
) {
|
||||
append(channelName)
|
||||
}
|
||||
|
||||
withStyle(
|
||||
SpanStyle(
|
||||
color = MaterialTheme.colors.onSurface.copy(alpha = 0.32f),
|
||||
fontWeight = FontWeight.Normal
|
||||
)
|
||||
) {
|
||||
append(" ${stringResource(id = R.string.public_chat)}")
|
||||
}
|
||||
},
|
||||
fontWeight = FontWeight.Bold,
|
||||
modifier = modifier,
|
||||
style = LocalTextStyle.current.copy(textDirection = TextDirection.Content)
|
||||
)
|
||||
},
|
||||
channelLastTime = note.createdAt(),
|
||||
channelLastContent = "$authorName: $description",
|
||||
hasNewMessages = hasNewMessages,
|
||||
onClick = { nav("Channel/$chanHex") }
|
||||
)
|
||||
ChannelRoomCompose(note, channel, nav)
|
||||
}
|
||||
} else {
|
||||
val replyAuthorBase =
|
||||
(note.event as? PrivateDmEvent)
|
||||
?.verifiedRecipientPubKey()
|
||||
?.let { LocalCache.getOrCreateUser(it) }
|
||||
val userRoomHex = remember(noteState, accountViewModel) {
|
||||
(note.event as? PrivateDmEvent)?.talkingWith(accountViewModel.userProfile().pubkeyHex)
|
||||
} ?: return
|
||||
|
||||
var userToComposeOn = note.author!!
|
||||
|
||||
if (replyAuthorBase != null) {
|
||||
if (note.author == accountViewModel.userProfile()) {
|
||||
userToComposeOn = replyAuthorBase
|
||||
}
|
||||
LoadUser(userRoomHex) { user ->
|
||||
UserRoomCompose(note, user, accountViewModel, nav)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
val noteEvent = note.event
|
||||
@Composable
|
||||
private fun ChannelRoomCompose(
|
||||
note: Note,
|
||||
channel: Channel,
|
||||
nav: (String) -> Unit
|
||||
) {
|
||||
val authorState by note.author!!.live().metadata.observeAsState()
|
||||
val authorName = remember(authorState) {
|
||||
authorState?.user?.toBestDisplayName()
|
||||
}
|
||||
|
||||
userToComposeOn.let { user ->
|
||||
var hasNewMessages by remember { mutableStateOf<Boolean>(false) }
|
||||
val chanHex = remember { channel.idHex }
|
||||
|
||||
LaunchedEffect(key1 = notificationCache, key2 = note) {
|
||||
scope.launch(Dispatchers.IO) {
|
||||
noteEvent?.let {
|
||||
hasNewMessages = it.createdAt() > notificationCache.cache.load(
|
||||
"Room/${userToComposeOn.pubkeyHex}"
|
||||
)
|
||||
}
|
||||
val channelState by channel.live.observeAsState()
|
||||
val channelPicture by remember(channelState) {
|
||||
derivedStateOf {
|
||||
channel.profilePicture()
|
||||
}
|
||||
}
|
||||
val channelName by remember(channelState) {
|
||||
derivedStateOf {
|
||||
channel.info.name
|
||||
}
|
||||
}
|
||||
|
||||
val noteEvent = note.event
|
||||
|
||||
val route = remember(note) {
|
||||
"Channel/$chanHex"
|
||||
}
|
||||
|
||||
val description = if (noteEvent is ChannelCreateEvent) {
|
||||
stringResource(R.string.channel_created)
|
||||
} else if (noteEvent is ChannelMetadataEvent) {
|
||||
"${stringResource(R.string.channel_information_changed_to)} "
|
||||
} else {
|
||||
noteEvent?.content()
|
||||
}
|
||||
|
||||
var hasNewMessages by remember { mutableStateOf<Boolean>(false) }
|
||||
|
||||
LaunchedEffect(key1 = note) {
|
||||
launch(Dispatchers.IO) {
|
||||
note.createdAt()?.let { timestamp ->
|
||||
val lastTime = NotificationCache.load(route)
|
||||
val newHasNewMessages = timestamp > lastTime
|
||||
if (hasNewMessages != newHasNewMessages) {
|
||||
hasNewMessages = newHasNewMessages
|
||||
}
|
||||
}
|
||||
|
||||
ChannelName(
|
||||
channelPicture = {
|
||||
UserPicture(
|
||||
userToComposeOn,
|
||||
accountViewModel = accountViewModel,
|
||||
size = 55.dp
|
||||
)
|
||||
},
|
||||
channelTitle = { UsernameDisplay(userToComposeOn, it) },
|
||||
channelLastTime = note.createdAt(),
|
||||
channelLastContent = accountViewModel.decrypt(note),
|
||||
hasNewMessages = hasNewMessages,
|
||||
onClick = { nav("Room/${user.pubkeyHex}") }
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
ChannelName(
|
||||
channelIdHex = chanHex,
|
||||
channelPicture = channelPicture,
|
||||
channelTitle = { modifier ->
|
||||
Text(
|
||||
text = buildAnnotatedString {
|
||||
withStyle(
|
||||
SpanStyle(
|
||||
fontWeight = FontWeight.Bold
|
||||
)
|
||||
) {
|
||||
append(channelName)
|
||||
}
|
||||
|
||||
withStyle(
|
||||
SpanStyle(
|
||||
color = MaterialTheme.colors.onSurface.copy(alpha = 0.32f),
|
||||
fontWeight = FontWeight.Normal
|
||||
)
|
||||
) {
|
||||
append(" ${stringResource(id = R.string.public_chat)}")
|
||||
}
|
||||
},
|
||||
fontWeight = FontWeight.Bold,
|
||||
modifier = modifier,
|
||||
style = LocalTextStyle.current.copy(textDirection = TextDirection.Content)
|
||||
)
|
||||
},
|
||||
channelLastTime = remember(note) { note.createdAt() },
|
||||
channelLastContent = remember(note) { "$authorName: $description" },
|
||||
hasNewMessages = hasNewMessages,
|
||||
onClick = { nav(route) }
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun UserRoomCompose(
|
||||
note: Note,
|
||||
user: User,
|
||||
accountViewModel: AccountViewModel,
|
||||
nav: (String) -> Unit
|
||||
) {
|
||||
val noteEvent = note.event
|
||||
|
||||
var hasNewMessages by remember { mutableStateOf<Boolean>(false) }
|
||||
|
||||
val route = remember(user) {
|
||||
"Room/${user.pubkeyHex}"
|
||||
}
|
||||
|
||||
LaunchedEffect(key1 = note) {
|
||||
launch(Dispatchers.IO) {
|
||||
noteEvent?.let {
|
||||
val lastTime = NotificationCache.load(route)
|
||||
|
||||
val newHasNewMessages = it.createdAt() > lastTime
|
||||
if (hasNewMessages != newHasNewMessages) {
|
||||
hasNewMessages = newHasNewMessages
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ChannelName(
|
||||
channelPicture = {
|
||||
UserPicture(
|
||||
user,
|
||||
accountViewModel = accountViewModel,
|
||||
size = 55.dp
|
||||
)
|
||||
},
|
||||
channelTitle = { UsernameDisplay(user, it) },
|
||||
channelLastTime = remember(note) { note.createdAt() },
|
||||
channelLastContent = remember(note) { accountViewModel.decrypt(note) },
|
||||
hasNewMessages = hasNewMessages,
|
||||
onClick = { nav(route) }
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun LoadUser(baseUserHex: String, content: @Composable (User) -> Unit) {
|
||||
var user by remember(baseUserHex) {
|
||||
mutableStateOf<User?>(null)
|
||||
}
|
||||
|
||||
LaunchedEffect(key1 = baseUserHex) {
|
||||
if (user == null) {
|
||||
launch(Dispatchers.IO) {
|
||||
user = LocalCache.checkGetOrCreateUser(baseUserHex)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
user?.let {
|
||||
content(it)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
|
||||
@@ -53,7 +53,6 @@ import androidx.compose.ui.unit.sp
|
||||
import com.vitorpamplona.amethyst.NotificationCache
|
||||
import com.vitorpamplona.amethyst.R
|
||||
import com.vitorpamplona.amethyst.model.Note
|
||||
import com.vitorpamplona.amethyst.model.User
|
||||
import com.vitorpamplona.amethyst.service.model.ChannelCreateEvent
|
||||
import com.vitorpamplona.amethyst.service.model.ChannelMessageEvent
|
||||
import com.vitorpamplona.amethyst.service.model.ChannelMetadataEvent
|
||||
@@ -66,6 +65,7 @@ import com.vitorpamplona.amethyst.ui.components.SensitivityWarning
|
||||
import com.vitorpamplona.amethyst.ui.components.TranslatableRichTextViewer
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
||||
import com.vitorpamplona.amethyst.ui.theme.RelayIconFilter
|
||||
import kotlinx.collections.immutable.toImmutableList
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
@@ -265,7 +265,6 @@ fun ChatroomMessageCompose(
|
||||
else -> {
|
||||
RenderRegularTextNote(
|
||||
note,
|
||||
loggedIn,
|
||||
isAcceptableAndCanPreview.second,
|
||||
backgroundBubbleColor,
|
||||
accountViewModel,
|
||||
@@ -359,13 +358,12 @@ fun ChatTimeAgo(time: Long) {
|
||||
@Composable
|
||||
private fun RenderRegularTextNote(
|
||||
note: Note,
|
||||
loggedIn: User,
|
||||
canPreview: Boolean,
|
||||
backgroundBubbleColor: Color,
|
||||
accountViewModel: AccountViewModel,
|
||||
nav: (String) -> Unit
|
||||
) {
|
||||
val tags = remember { note.event?.tags() ?: emptyList() }
|
||||
val tags = remember(note.event) { note.event?.tags()?.toImmutableList() ?: emptyList<List<String>>().toImmutableList() }
|
||||
val eventContent = remember { accountViewModel.decrypt(note) }
|
||||
val modifier = remember { Modifier.padding(top = 5.dp) }
|
||||
|
||||
|
||||
@@ -55,6 +55,7 @@ import com.vitorpamplona.amethyst.ui.theme.BitcoinOrange
|
||||
import com.vitorpamplona.amethyst.ui.theme.newItemBackgroundColor
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
import kotlinx.collections.immutable.ImmutableMap
|
||||
import kotlinx.collections.immutable.persistentListOf
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
@@ -361,7 +362,7 @@ private fun AuthorPictureAndComment(
|
||||
TranslatableRichTextViewer(
|
||||
content = it,
|
||||
canPreview = true,
|
||||
tags = remember { emptyList() },
|
||||
tags = remember { persistentListOf() },
|
||||
modifier = remember { Modifier.fillMaxWidth() },
|
||||
backgroundColor = backgroundColor,
|
||||
accountViewModel = accountViewModel,
|
||||
|
||||
@@ -139,6 +139,7 @@ import com.vitorpamplona.amethyst.ui.theme.BitcoinOrange
|
||||
import com.vitorpamplona.amethyst.ui.theme.Following
|
||||
import com.vitorpamplona.amethyst.ui.theme.newItemBackgroundColor
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
import kotlinx.collections.immutable.persistentListOf
|
||||
import kotlinx.collections.immutable.toImmutableList
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
@@ -155,7 +156,7 @@ import kotlin.time.ExperimentalTime
|
||||
fun NoteCompose(
|
||||
baseNote: Note,
|
||||
routeForLastRead: String? = null,
|
||||
modifier: Modifier = Modifier,
|
||||
modifier: Modifier = remember { Modifier },
|
||||
isBoostedNote: Boolean = false,
|
||||
isQuotedNote: Boolean = false,
|
||||
unPackReply: Boolean = true,
|
||||
@@ -214,13 +215,12 @@ fun CheckHiddenNoteCompose(
|
||||
nav: (String) -> Unit
|
||||
) {
|
||||
val accountState by accountViewModel.accountLiveData.observeAsState()
|
||||
val account = remember(accountState) { accountState?.account } ?: return
|
||||
|
||||
val isHidden by remember(accountState) {
|
||||
derivedStateOf {
|
||||
val isSensitive = note.event?.isSensitive() ?: false
|
||||
|
||||
account.isHidden(note.author!!) || (isSensitive && account.showSensitiveContent == false)
|
||||
accountState?.account?.isHidden(note.author!!) == true || (isSensitive && accountState?.account?.showSensitiveContent == false)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -347,7 +347,7 @@ private fun WatchForReports(
|
||||
}
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalFoundationApi::class, ExperimentalTime::class)
|
||||
@OptIn(ExperimentalFoundationApi::class)
|
||||
@Composable
|
||||
fun NormalNote(
|
||||
baseNote: Note,
|
||||
@@ -366,8 +366,6 @@ fun NormalNote(
|
||||
val noteEvent = remember { baseNote.event }
|
||||
val channelHex = remember { baseNote.channelHex() }
|
||||
|
||||
var popupExpanded by remember { mutableStateOf(false) }
|
||||
|
||||
if ((noteEvent is ChannelCreateEvent || noteEvent is ChannelMetadataEvent) && channelHex != null) {
|
||||
ChannelHeader(channelHex = channelHex, accountViewModel = accountViewModel, nav = nav)
|
||||
} else if (noteEvent is BadgeDefinitionEvent) {
|
||||
@@ -378,11 +376,12 @@ fun NormalNote(
|
||||
FileStorageHeaderDisplay(baseNote)
|
||||
} else {
|
||||
var isNew by remember { mutableStateOf<Boolean>(false) }
|
||||
var popupExpanded by remember { mutableStateOf(false) }
|
||||
|
||||
val scope = rememberCoroutineScope()
|
||||
|
||||
LaunchedEffect(key1 = routeForLastRead) {
|
||||
scope.launch(Dispatchers.IO) {
|
||||
launch(Dispatchers.IO) {
|
||||
routeForLastRead?.let {
|
||||
val lastTime = NotificationCache.load(it)
|
||||
|
||||
@@ -596,8 +595,8 @@ private fun RenderTextEvent(
|
||||
hasSensitiveContent = hasSensitiveContent,
|
||||
accountViewModel = accountViewModel
|
||||
) {
|
||||
val modifier = remember(note.event) { Modifier.fillMaxWidth() }
|
||||
val tags = remember(note.event) { note.event?.tags() ?: emptyList() }
|
||||
val modifier = remember(note) { Modifier.fillMaxWidth() }
|
||||
val tags = remember(note) { note.event?.tags()?.toImmutableList() ?: emptyList<List<String>>().toImmutableList() }
|
||||
|
||||
TranslatableRichTextViewer(
|
||||
content = eventContent,
|
||||
@@ -646,18 +645,21 @@ private fun RenderPoll(
|
||||
)
|
||||
} else {
|
||||
val hasSensitiveContent = remember(note.event) { note.event?.isSensitive() ?: false }
|
||||
|
||||
val tags = remember(note) { note.event?.tags()?.toImmutableList() ?: emptyList<List<String>>().toImmutableList() }
|
||||
|
||||
SensitivityWarning(
|
||||
hasSensitiveContent = hasSensitiveContent,
|
||||
accountViewModel = accountViewModel
|
||||
) {
|
||||
TranslatableRichTextViewer(
|
||||
eventContent,
|
||||
content = eventContent,
|
||||
canPreview = canPreview && !makeItShort,
|
||||
Modifier.fillMaxWidth(),
|
||||
noteEvent.tags(),
|
||||
backgroundColor,
|
||||
accountViewModel,
|
||||
nav
|
||||
modifier = remember { Modifier.fillMaxWidth() },
|
||||
tags = tags,
|
||||
backgroundColor = backgroundColor,
|
||||
accountViewModel = accountViewModel,
|
||||
nav = nav
|
||||
)
|
||||
|
||||
PollNote(
|
||||
@@ -669,7 +671,8 @@ private fun RenderPoll(
|
||||
)
|
||||
}
|
||||
|
||||
DisplayUncitedHashtags(noteEvent.hashtags(), eventContent, nav)
|
||||
var hashtags = remember { noteEvent.hashtags() }
|
||||
DisplayUncitedHashtags(hashtags, eventContent, nav)
|
||||
}
|
||||
|
||||
if (!makeItShort) {
|
||||
@@ -823,10 +826,11 @@ fun RenderAppDefinition(
|
||||
Row(
|
||||
modifier = Modifier.padding(top = 5.dp, bottom = 5.dp)
|
||||
) {
|
||||
val tags = remember(note) { note.event?.tags()?.toImmutableList() ?: emptyList<List<String>>().toImmutableList() }
|
||||
TranslatableRichTextViewer(
|
||||
content = it,
|
||||
canPreview = false,
|
||||
tags = remember { note.event?.tags() ?: emptyList() },
|
||||
tags = tags,
|
||||
backgroundColor = MaterialTheme.colors.background,
|
||||
accountViewModel = accountViewModel,
|
||||
nav = nav
|
||||
@@ -889,7 +893,8 @@ private fun RenderPrivateMessage(
|
||||
val hashtags = remember(note.event?.id()) { note.event?.hashtags() ?: emptyList() }
|
||||
val modifier = remember(note.event?.id()) { Modifier.fillMaxWidth() }
|
||||
val isAuthorTheLoggedUser = remember(note.event?.id()) { accountViewModel.isLoggedUser(note.author) }
|
||||
val tags = remember(note.event?.id()) { note.event?.tags() ?: emptyList() }
|
||||
|
||||
val tags = remember(note) { note.event?.tags()?.toImmutableList() ?: emptyList<List<String>>().toImmutableList() }
|
||||
|
||||
if (eventContent != null) {
|
||||
if (makeItShort && isAuthorTheLoggedUser) {
|
||||
@@ -930,7 +935,7 @@ private fun RenderPrivateMessage(
|
||||
),
|
||||
canPreview = !makeItShort,
|
||||
Modifier.fillMaxWidth(),
|
||||
emptyList(),
|
||||
persistentListOf(),
|
||||
backgroundColor,
|
||||
accountViewModel,
|
||||
nav
|
||||
@@ -1203,7 +1208,7 @@ fun PinListHeader(
|
||||
) {
|
||||
val noteEvent = baseNote.event as? PinListEvent ?: return
|
||||
|
||||
var pins by remember { mutableStateOf(noteEvent.pins()) }
|
||||
val pins by remember { mutableStateOf(noteEvent.pins()) }
|
||||
|
||||
var expanded by remember {
|
||||
mutableStateOf(false)
|
||||
@@ -1242,7 +1247,7 @@ fun PinListHeader(
|
||||
TranslatableRichTextViewer(
|
||||
content = pin,
|
||||
canPreview = true,
|
||||
tags = remember { emptyList() },
|
||||
tags = remember { persistentListOf() },
|
||||
backgroundColor = backgroundColor,
|
||||
accountViewModel = accountViewModel,
|
||||
nav = nav
|
||||
@@ -1352,7 +1357,7 @@ private fun RenderReport(
|
||||
content = content,
|
||||
canPreview = true,
|
||||
modifier = remember { Modifier },
|
||||
tags = remember { emptyList() },
|
||||
tags = remember { persistentListOf() },
|
||||
backgroundColor = backgroundColor,
|
||||
accountViewModel = accountViewModel,
|
||||
nav = nav
|
||||
@@ -1690,7 +1695,7 @@ fun DisplayHighlight(
|
||||
quote,
|
||||
canPreview = canPreview && !makeItShort,
|
||||
Modifier.fillMaxWidth(),
|
||||
emptyList(),
|
||||
persistentListOf(),
|
||||
backgroundColor,
|
||||
accountViewModel,
|
||||
nav
|
||||
@@ -2600,6 +2605,8 @@ fun NoteDropDownMenu(note: Note, popupExpanded: Boolean, onDismiss: () -> Unit,
|
||||
}
|
||||
}
|
||||
|
||||
val scope = rememberCoroutineScope()
|
||||
|
||||
if (!state.isFollowingAuthor) {
|
||||
DropdownMenuItem(onClick = {
|
||||
accountViewModel.follow(
|
||||
@@ -2610,13 +2617,32 @@ fun NoteDropDownMenu(note: Note, popupExpanded: Boolean, onDismiss: () -> Unit,
|
||||
}
|
||||
Divider()
|
||||
}
|
||||
DropdownMenuItem(onClick = { clipboardManager.setText(AnnotatedString(accountViewModel.decrypt(note) ?: "")); onDismiss() }) {
|
||||
DropdownMenuItem(
|
||||
onClick = {
|
||||
scope.launch(Dispatchers.IO) {
|
||||
clipboardManager.setText(AnnotatedString(accountViewModel.decrypt(note) ?: ""))
|
||||
onDismiss()
|
||||
}
|
||||
}
|
||||
) {
|
||||
Text(stringResource(R.string.copy_text))
|
||||
}
|
||||
DropdownMenuItem(onClick = { clipboardManager.setText(AnnotatedString("nostr:${note.author?.pubkeyNpub()}")); onDismiss() }) {
|
||||
DropdownMenuItem(
|
||||
onClick = {
|
||||
scope.launch(Dispatchers.IO) {
|
||||
clipboardManager.setText(AnnotatedString("nostr:${note.author?.pubkeyNpub()}"))
|
||||
onDismiss()
|
||||
}
|
||||
}
|
||||
) {
|
||||
Text(stringResource(R.string.copy_user_pubkey))
|
||||
}
|
||||
DropdownMenuItem(onClick = { clipboardManager.setText(AnnotatedString("nostr:" + note.toNEvent())); onDismiss() }) {
|
||||
DropdownMenuItem(onClick = {
|
||||
scope.launch(Dispatchers.IO) {
|
||||
clipboardManager.setText(AnnotatedString("nostr:" + note.toNEvent()))
|
||||
onDismiss()
|
||||
}
|
||||
}) {
|
||||
Text(stringResource(R.string.copy_note_id))
|
||||
}
|
||||
DropdownMenuItem(onClick = {
|
||||
@@ -2638,30 +2664,30 @@ fun NoteDropDownMenu(note: Note, popupExpanded: Boolean, onDismiss: () -> Unit,
|
||||
}
|
||||
Divider()
|
||||
if (state.isPrivateBookmarkNote) {
|
||||
DropdownMenuItem(onClick = { accountViewModel.removePrivateBookmark(note); onDismiss() }) {
|
||||
DropdownMenuItem(onClick = { scope.launch(Dispatchers.IO) { accountViewModel.removePrivateBookmark(note); onDismiss() } }) {
|
||||
Text(stringResource(R.string.remove_from_private_bookmarks))
|
||||
}
|
||||
} else {
|
||||
DropdownMenuItem(onClick = { accountViewModel.addPrivateBookmark(note); onDismiss() }) {
|
||||
DropdownMenuItem(onClick = { scope.launch(Dispatchers.IO) { accountViewModel.addPrivateBookmark(note); onDismiss() } }) {
|
||||
Text(stringResource(R.string.add_to_private_bookmarks))
|
||||
}
|
||||
}
|
||||
if (state.isPublicBookmarkNote) {
|
||||
DropdownMenuItem(onClick = { accountViewModel.removePublicBookmark(note); onDismiss() }) {
|
||||
DropdownMenuItem(onClick = { scope.launch(Dispatchers.IO) { accountViewModel.removePublicBookmark(note); onDismiss() } }) {
|
||||
Text(stringResource(R.string.remove_from_public_bookmarks))
|
||||
}
|
||||
} else {
|
||||
DropdownMenuItem(onClick = { accountViewModel.addPublicBookmark(note); onDismiss() }) {
|
||||
DropdownMenuItem(onClick = { scope.launch(Dispatchers.IO) { accountViewModel.addPublicBookmark(note); onDismiss() } }) {
|
||||
Text(stringResource(R.string.add_to_public_bookmarks))
|
||||
}
|
||||
}
|
||||
Divider()
|
||||
DropdownMenuItem(onClick = { accountViewModel.broadcast(note); onDismiss() }) {
|
||||
DropdownMenuItem(onClick = { scope.launch(Dispatchers.IO) { accountViewModel.broadcast(note); onDismiss() } }) {
|
||||
Text(stringResource(R.string.broadcast))
|
||||
}
|
||||
Divider()
|
||||
if (state.isLoggedUser) {
|
||||
DropdownMenuItem(onClick = { accountViewModel.delete(note); onDismiss() }) {
|
||||
DropdownMenuItem(onClick = { scope.launch(Dispatchers.IO) { accountViewModel.delete(note); onDismiss() } }) {
|
||||
Text(stringResource(R.string.request_deletion))
|
||||
}
|
||||
} else {
|
||||
@@ -2671,17 +2697,17 @@ fun NoteDropDownMenu(note: Note, popupExpanded: Boolean, onDismiss: () -> Unit,
|
||||
}
|
||||
Divider()
|
||||
if (state.showSensitiveContent == null || state.showSensitiveContent == true) {
|
||||
DropdownMenuItem(onClick = { accountViewModel.hideSensitiveContent(); onDismiss() }) {
|
||||
DropdownMenuItem(onClick = { scope.launch(Dispatchers.IO) { accountViewModel.hideSensitiveContent(); onDismiss() } }) {
|
||||
Text(stringResource(R.string.content_warning_hide_all_sensitive_content))
|
||||
}
|
||||
}
|
||||
if (state.showSensitiveContent == null || state.showSensitiveContent == false) {
|
||||
DropdownMenuItem(onClick = { accountViewModel.disableContentWarnings(); onDismiss() }) {
|
||||
DropdownMenuItem(onClick = { scope.launch(Dispatchers.IO) { accountViewModel.disableContentWarnings(); onDismiss() } }) {
|
||||
Text(stringResource(R.string.content_warning_show_all_sensitive_content))
|
||||
}
|
||||
}
|
||||
if (state.showSensitiveContent != null) {
|
||||
DropdownMenuItem(onClick = { accountViewModel.seeContentWarnings(); onDismiss() }) {
|
||||
DropdownMenuItem(onClick = { scope.launch(Dispatchers.IO) { accountViewModel.seeContentWarnings(); onDismiss() } }) {
|
||||
Text(stringResource(R.string.content_warning_see_warnings))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -69,6 +69,7 @@ import com.vitorpamplona.amethyst.ui.components.SelectTextDialog
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.ReportNoteDialog
|
||||
import com.vitorpamplona.amethyst.ui.theme.WarningColor
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
private fun lightenColor(color: Color, amount: Float): Color {
|
||||
@@ -154,25 +155,31 @@ fun NoteQuickActionMenu(note: Note, popupExpanded: Boolean, onDismiss: () -> Uni
|
||||
icon = Icons.Default.ContentCopy,
|
||||
label = stringResource(R.string.quick_action_copy_text)
|
||||
) {
|
||||
clipboardManager.setText(
|
||||
AnnotatedString(
|
||||
accountViewModel.decrypt(note) ?: ""
|
||||
scope.launch(Dispatchers.IO) {
|
||||
clipboardManager.setText(
|
||||
AnnotatedString(
|
||||
accountViewModel.decrypt(note) ?: ""
|
||||
)
|
||||
)
|
||||
)
|
||||
showToast(R.string.copied_note_text_to_clipboard)
|
||||
onDismiss()
|
||||
showToast(R.string.copied_note_text_to_clipboard)
|
||||
onDismiss()
|
||||
}
|
||||
}
|
||||
VerticalDivider(primaryLight)
|
||||
NoteQuickActionItem(Icons.Default.AlternateEmail, stringResource(R.string.quick_action_copy_user_id)) {
|
||||
clipboardManager.setText(AnnotatedString("nostr:${note.author?.pubkeyNpub()}"))
|
||||
showToast(R.string.copied_user_id_to_clipboard)
|
||||
onDismiss()
|
||||
scope.launch(Dispatchers.IO) {
|
||||
clipboardManager.setText(AnnotatedString("nostr:${note.author?.pubkeyNpub()}"))
|
||||
showToast(R.string.copied_user_id_to_clipboard)
|
||||
onDismiss()
|
||||
}
|
||||
}
|
||||
VerticalDivider(primaryLight)
|
||||
NoteQuickActionItem(Icons.Default.FormatQuote, stringResource(R.string.quick_action_copy_note_id)) {
|
||||
clipboardManager.setText(AnnotatedString("nostr:${note.toNEvent()}"))
|
||||
showToast(R.string.copied_note_id_to_clipboard)
|
||||
onDismiss()
|
||||
scope.launch(Dispatchers.IO) {
|
||||
clipboardManager.setText(AnnotatedString("nostr:${note.toNEvent()}"))
|
||||
showToast(R.string.copied_note_id_to_clipboard)
|
||||
onDismiss()
|
||||
}
|
||||
}
|
||||
|
||||
if (!isOwnNote) {
|
||||
@@ -180,8 +187,10 @@ fun NoteQuickActionMenu(note: Note, popupExpanded: Boolean, onDismiss: () -> Uni
|
||||
|
||||
NoteQuickActionItem(Icons.Default.Block, stringResource(R.string.quick_action_block)) {
|
||||
if (accountViewModel.hideBlockAlertDialog) {
|
||||
note.author?.let { accountViewModel.hide(it) }
|
||||
onDismiss()
|
||||
scope.launch(Dispatchers.IO) {
|
||||
note.author?.let { accountViewModel.hide(it) }
|
||||
onDismiss()
|
||||
}
|
||||
} else {
|
||||
showBlockAlertDialog = true
|
||||
}
|
||||
@@ -198,21 +207,27 @@ fun NoteQuickActionMenu(note: Note, popupExpanded: Boolean, onDismiss: () -> Uni
|
||||
if (isOwnNote) {
|
||||
NoteQuickActionItem(Icons.Default.Delete, stringResource(R.string.quick_action_delete)) {
|
||||
if (accountViewModel.hideDeleteRequestDialog) {
|
||||
accountViewModel.delete(note)
|
||||
onDismiss()
|
||||
scope.launch(Dispatchers.IO) {
|
||||
accountViewModel.delete(note)
|
||||
onDismiss()
|
||||
}
|
||||
} else {
|
||||
showDeleteAlertDialog = true
|
||||
}
|
||||
}
|
||||
} else if (isFollowingUser) {
|
||||
NoteQuickActionItem(Icons.Default.PersonRemove, stringResource(R.string.quick_action_unfollow)) {
|
||||
accountViewModel.unfollow(note.author!!)
|
||||
onDismiss()
|
||||
scope.launch(Dispatchers.IO) {
|
||||
accountViewModel.unfollow(note.author!!)
|
||||
onDismiss()
|
||||
}
|
||||
}
|
||||
} else {
|
||||
NoteQuickActionItem(Icons.Default.PersonAdd, stringResource(R.string.quick_action_follow)) {
|
||||
accountViewModel.follow(note.author!!)
|
||||
onDismiss()
|
||||
scope.launch(Dispatchers.IO) {
|
||||
accountViewModel.follow(note.author!!)
|
||||
onDismiss()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -221,9 +236,11 @@ fun NoteQuickActionMenu(note: Note, popupExpanded: Boolean, onDismiss: () -> Uni
|
||||
icon = ImageVector.vectorResource(id = R.drawable.relays),
|
||||
label = stringResource(R.string.broadcast)
|
||||
) {
|
||||
accountViewModel.broadcast(note)
|
||||
// showSelectTextDialog = true
|
||||
onDismiss()
|
||||
scope.launch(Dispatchers.IO) {
|
||||
accountViewModel.broadcast(note)
|
||||
// showSelectTextDialog = true
|
||||
onDismiss()
|
||||
}
|
||||
}
|
||||
VerticalDivider(primaryLight)
|
||||
NoteQuickActionItem(icon = Icons.Default.Share, label = stringResource(R.string.quick_action_share)) {
|
||||
|
||||
@@ -30,6 +30,8 @@ import com.vitorpamplona.amethyst.service.model.LnZapEvent
|
||||
import com.vitorpamplona.amethyst.ui.components.TranslatableRichTextViewer
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
||||
import com.vitorpamplona.amethyst.ui.theme.BitcoinOrange
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
import kotlinx.collections.immutable.toImmutableList
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import java.util.*
|
||||
@@ -109,7 +111,7 @@ private fun OptionNote(
|
||||
backgroundColor: Color,
|
||||
nav: (String) -> Unit
|
||||
) {
|
||||
val tags = remember { baseNote.event?.tags() ?: emptyList() }
|
||||
val tags = remember(baseNote) { baseNote.event?.tags()?.toImmutableList() ?: emptyList<List<String>>().toImmutableList() }
|
||||
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
@@ -163,7 +165,7 @@ private fun RenderOptionAfterVote(
|
||||
totalRatio: Float,
|
||||
color: Color,
|
||||
canPreview: Boolean,
|
||||
tags: List<List<String>>,
|
||||
tags: ImmutableList<List<String>>,
|
||||
backgroundColor: Color,
|
||||
accountViewModel: AccountViewModel,
|
||||
nav: (String) -> Unit
|
||||
@@ -230,7 +232,7 @@ private fun RenderOptionAfterVote(
|
||||
private fun RenderOptionBeforeVote(
|
||||
description: String,
|
||||
canPreview: Boolean,
|
||||
tags: List<List<String>>,
|
||||
tags: ImmutableList<List<String>>,
|
||||
backgroundColor: Color,
|
||||
accountViewModel: AccountViewModel,
|
||||
nav: (String) -> Unit
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
package com.vitorpamplona.amethyst.ui.note
|
||||
|
||||
import android.content.Context
|
||||
import android.widget.Toast
|
||||
import androidx.compose.animation.core.animateFloatAsState
|
||||
import androidx.compose.foundation.ExperimentalFoundationApi
|
||||
@@ -62,6 +63,7 @@ import com.vitorpamplona.amethyst.model.User
|
||||
import com.vitorpamplona.amethyst.ui.actions.NewPostView
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
||||
import com.vitorpamplona.amethyst.ui.theme.BitcoinOrange
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import java.math.BigDecimal
|
||||
@@ -201,7 +203,9 @@ fun BoostReaction(
|
||||
onClick = {
|
||||
if (accountViewModel.isWriteable()) {
|
||||
if (accountViewModel.hasBoosted(baseNote)) {
|
||||
accountViewModel.deleteBoostsTo(baseNote)
|
||||
scope.launch(Dispatchers.IO) {
|
||||
accountViewModel.deleteBoostsTo(baseNote)
|
||||
}
|
||||
} else {
|
||||
wantsToBoost = true
|
||||
}
|
||||
@@ -293,10 +297,12 @@ fun LikeReaction(
|
||||
modifier = iconButtonModifier,
|
||||
onClick = {
|
||||
if (accountViewModel.isWriteable()) {
|
||||
if (accountViewModel.hasReactedTo(baseNote)) {
|
||||
accountViewModel.deleteReactionTo(baseNote)
|
||||
} else {
|
||||
accountViewModel.reactTo(baseNote)
|
||||
scope.launch(Dispatchers.IO) {
|
||||
if (accountViewModel.hasReactedTo(baseNote)) {
|
||||
accountViewModel.deleteReactionTo(baseNote)
|
||||
} else {
|
||||
accountViewModel.reactTo(baseNote)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
scope.launch {
|
||||
@@ -386,9 +392,6 @@ fun ZapReaction(
|
||||
iconSize: Dp = 20.dp,
|
||||
animationSize: Dp = 14.dp
|
||||
) {
|
||||
val accountState by accountViewModel.accountLiveData.observeAsState()
|
||||
val account = remember(accountState) { accountState?.account } ?: return
|
||||
|
||||
var wantsToZap by remember { mutableStateOf(false) }
|
||||
var wantsToChangeZapAmount by remember { mutableStateOf(false) }
|
||||
var wantsToSetCustomZap by remember { mutableStateOf(false) }
|
||||
@@ -407,53 +410,18 @@ fun ZapReaction(
|
||||
interactionSource = remember { MutableInteractionSource() },
|
||||
indication = rememberRipple(bounded = false, radius = 24.dp),
|
||||
onClick = {
|
||||
if (account.zapAmountChoices.isEmpty()) {
|
||||
scope.launch {
|
||||
Toast
|
||||
.makeText(
|
||||
context,
|
||||
context.getString(R.string.no_zap_amount_setup_long_press_to_change),
|
||||
Toast.LENGTH_SHORT
|
||||
)
|
||||
.show()
|
||||
zapClick(
|
||||
baseNote,
|
||||
accountViewModel,
|
||||
scope,
|
||||
context,
|
||||
onZappingProgress = { progress: Float ->
|
||||
zappingProgress = progress
|
||||
},
|
||||
onMultipleChoices = {
|
||||
wantsToZap = true
|
||||
}
|
||||
} else if (!accountViewModel.isWriteable()) {
|
||||
scope.launch {
|
||||
Toast
|
||||
.makeText(
|
||||
context,
|
||||
context.getString(R.string.login_with_a_private_key_to_be_able_to_send_zaps),
|
||||
Toast.LENGTH_SHORT
|
||||
)
|
||||
.show()
|
||||
}
|
||||
} else if (account.zapAmountChoices.size == 1) {
|
||||
scope.launch(Dispatchers.IO) {
|
||||
accountViewModel.zap(
|
||||
baseNote,
|
||||
account.zapAmountChoices.first() * 1000,
|
||||
null,
|
||||
"",
|
||||
context,
|
||||
onError = {
|
||||
scope.launch {
|
||||
zappingProgress = 0f
|
||||
Toast
|
||||
.makeText(context, it, Toast.LENGTH_SHORT)
|
||||
.show()
|
||||
}
|
||||
},
|
||||
onProgress = {
|
||||
scope.launch(Dispatchers.Main) {
|
||||
zappingProgress = it
|
||||
}
|
||||
},
|
||||
zapType = account.defaultZapType
|
||||
)
|
||||
}
|
||||
} else if (account.zapAmountChoices.size > 1) {
|
||||
wantsToZap = true
|
||||
}
|
||||
)
|
||||
},
|
||||
onLongClick = {
|
||||
wantsToChangeZapAmount = true
|
||||
@@ -494,7 +462,7 @@ fun ZapReaction(
|
||||
}
|
||||
|
||||
if (wantsToSetCustomZap) {
|
||||
ZapCustomDialog({ wantsToSetCustomZap = false }, account = account, accountViewModel, baseNote)
|
||||
ZapCustomDialog({ wantsToSetCustomZap = false }, accountViewModel, baseNote)
|
||||
}
|
||||
|
||||
if (zappingProgress > 0.00001 && zappingProgress < 0.99999) {
|
||||
@@ -507,7 +475,7 @@ fun ZapReaction(
|
||||
|
||||
CircularProgressIndicator(
|
||||
progress = animatedProgress,
|
||||
modifier = Modifier.size(animationSize),
|
||||
modifier = remember { Modifier.size(animationSize) },
|
||||
strokeWidth = 2.dp
|
||||
)
|
||||
} else {
|
||||
@@ -523,6 +491,63 @@ fun ZapReaction(
|
||||
ZapAmountText(baseNote, grayTint, accountViewModel)
|
||||
}
|
||||
|
||||
private fun zapClick(
|
||||
baseNote: Note,
|
||||
accountViewModel: AccountViewModel,
|
||||
scope: CoroutineScope,
|
||||
context: Context,
|
||||
onZappingProgress: (Float) -> Unit,
|
||||
onMultipleChoices: () -> Unit
|
||||
) {
|
||||
if (accountViewModel.account.zapAmountChoices.isEmpty()) {
|
||||
scope.launch {
|
||||
Toast
|
||||
.makeText(
|
||||
context,
|
||||
context.getString(R.string.no_zap_amount_setup_long_press_to_change),
|
||||
Toast.LENGTH_SHORT
|
||||
)
|
||||
.show()
|
||||
}
|
||||
} else if (!accountViewModel.isWriteable()) {
|
||||
scope.launch {
|
||||
Toast
|
||||
.makeText(
|
||||
context,
|
||||
context.getString(R.string.login_with_a_private_key_to_be_able_to_send_zaps),
|
||||
Toast.LENGTH_SHORT
|
||||
)
|
||||
.show()
|
||||
}
|
||||
} else if (accountViewModel.account.zapAmountChoices.size == 1) {
|
||||
scope.launch(Dispatchers.IO) {
|
||||
accountViewModel.zap(
|
||||
baseNote,
|
||||
accountViewModel.account.zapAmountChoices.first() * 1000,
|
||||
null,
|
||||
"",
|
||||
context,
|
||||
onError = {
|
||||
scope.launch {
|
||||
onZappingProgress(0f)
|
||||
Toast
|
||||
.makeText(context, it, Toast.LENGTH_SHORT)
|
||||
.show()
|
||||
}
|
||||
},
|
||||
onProgress = {
|
||||
scope.launch(Dispatchers.Main) {
|
||||
onZappingProgress(it)
|
||||
}
|
||||
},
|
||||
zapType = accountViewModel.account.defaultZapType
|
||||
)
|
||||
}
|
||||
} else if (accountViewModel.account.zapAmountChoices.size > 1) {
|
||||
onMultipleChoices()
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun ZapIcon(
|
||||
baseNote: Note,
|
||||
@@ -593,7 +618,7 @@ private fun ZapAmountText(
|
||||
}
|
||||
|
||||
@Composable
|
||||
public fun ViewCountReaction(
|
||||
fun ViewCountReaction(
|
||||
idHex: String,
|
||||
grayTint: Color,
|
||||
iconSize: Dp = 20.dp,
|
||||
@@ -653,12 +678,15 @@ private fun BoostTypeChoicePopup(baseNote: Note, accountViewModel: AccountViewMo
|
||||
offset = IntOffset(0, -50),
|
||||
onDismissRequest = { onDismiss() }
|
||||
) {
|
||||
FlowRow() {
|
||||
FlowRow {
|
||||
val scope = rememberCoroutineScope()
|
||||
Button(
|
||||
modifier = Modifier.padding(horizontal = 3.dp),
|
||||
onClick = {
|
||||
accountViewModel.boost(baseNote)
|
||||
onDismiss()
|
||||
scope.launch(Dispatchers.IO) {
|
||||
accountViewModel.boost(baseNote)
|
||||
onDismiss()
|
||||
}
|
||||
},
|
||||
shape = RoundedCornerShape(20.dp),
|
||||
colors = ButtonDefaults
|
||||
|
||||
@@ -56,13 +56,13 @@ class ZapOptionstViewModel : ViewModel() {
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun ZapCustomDialog(onClose: () -> Unit, account: Account, accountViewModel: AccountViewModel, baseNote: Note) {
|
||||
fun ZapCustomDialog(onClose: () -> Unit, accountViewModel: AccountViewModel, baseNote: Note) {
|
||||
val context = LocalContext.current
|
||||
val scope = rememberCoroutineScope()
|
||||
val postViewModel: ZapOptionstViewModel = viewModel()
|
||||
|
||||
LaunchedEffect(account) {
|
||||
postViewModel.load(account)
|
||||
LaunchedEffect(accountViewModel) {
|
||||
postViewModel.load(accountViewModel.account)
|
||||
}
|
||||
|
||||
var zappingProgress by remember { mutableStateOf(0f) }
|
||||
@@ -76,7 +76,7 @@ fun ZapCustomDialog(onClose: () -> Unit, account: Account, accountViewModel: Acc
|
||||
|
||||
val zapOptions = zapTypes.map { it.second }
|
||||
val zapOptionExplainers = zapTypes.map { it.third }
|
||||
var selectedZapType by remember { mutableStateOf(account.defaultZapType) }
|
||||
var selectedZapType by remember(accountViewModel) { mutableStateOf(accountViewModel.account.defaultZapType) }
|
||||
|
||||
Dialog(
|
||||
onDismissRequest = { onClose() },
|
||||
@@ -158,7 +158,7 @@ fun ZapCustomDialog(onClose: () -> Unit, account: Account, accountViewModel: Acc
|
||||
|
||||
TextSpinner(
|
||||
label = stringResource(id = R.string.zap_type),
|
||||
placeholder = zapTypes.filter { it.first == account.defaultZapType }.first().second,
|
||||
placeholder = zapTypes.filter { it.first == accountViewModel.account.defaultZapType }.first().second,
|
||||
options = zapOptions,
|
||||
explainers = zapOptionExplainers,
|
||||
onSelect = {
|
||||
|
||||
@@ -150,7 +150,7 @@ fun UserActionOptions(
|
||||
baseAuthor: User,
|
||||
accountViewModel: AccountViewModel
|
||||
) {
|
||||
val coroutineScope = rememberCoroutineScope()
|
||||
val scope = rememberCoroutineScope()
|
||||
|
||||
val accountState by accountViewModel.accountLiveData.observeAsState()
|
||||
val isHidden by remember(accountState) {
|
||||
@@ -168,12 +168,14 @@ fun UserActionOptions(
|
||||
|
||||
if (isHidden) {
|
||||
ShowUserButton {
|
||||
accountViewModel.show(baseAuthor)
|
||||
scope.launch(Dispatchers.IO) {
|
||||
accountViewModel.show(baseAuthor)
|
||||
}
|
||||
}
|
||||
} else if (isFollowing) {
|
||||
UnfollowButton { coroutineScope.launch(Dispatchers.IO) { accountViewModel.unfollow(baseAuthor) } }
|
||||
UnfollowButton { scope.launch(Dispatchers.IO) { accountViewModel.unfollow(baseAuthor) } }
|
||||
} else {
|
||||
FollowButton({ coroutineScope.launch(Dispatchers.IO) { accountViewModel.follow(baseAuthor) } })
|
||||
FollowButton({ scope.launch(Dispatchers.IO) { accountViewModel.follow(baseAuthor) } })
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -26,6 +26,7 @@ import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.vitorpamplona.amethyst.ui.note.BadgeCompose
|
||||
import com.vitorpamplona.amethyst.ui.note.MessageSetCompose
|
||||
@@ -45,8 +46,15 @@ fun RefresheableCardView(
|
||||
enablePullRefresh: Boolean = true
|
||||
) {
|
||||
var refreshing by remember { mutableStateOf(false) }
|
||||
val refresh = { refreshing = true; viewModel.invalidateData(); refreshing = false }
|
||||
val pullRefreshState = rememberPullRefreshState(refreshing, onRefresh = refresh)
|
||||
val pullRefreshState = rememberPullRefreshState(
|
||||
refreshing,
|
||||
onRefresh =
|
||||
{
|
||||
refreshing = true
|
||||
viewModel.invalidateData()
|
||||
refreshing = false
|
||||
}
|
||||
)
|
||||
|
||||
val modifier = if (enablePullRefresh) {
|
||||
Modifier.pullRefresh(pullRefreshState)
|
||||
@@ -151,7 +159,9 @@ private fun FeedLoaded(
|
||||
routeForLastRead: String
|
||||
) {
|
||||
val defaultModifier = remember {
|
||||
Modifier.fillMaxWidth().defaultMinSize(minHeight = 100.dp)
|
||||
Modifier
|
||||
.fillMaxWidth()
|
||||
.defaultMinSize(minHeight = 100.dp)
|
||||
}
|
||||
|
||||
LazyColumn(
|
||||
@@ -168,7 +178,7 @@ private fun FeedLoaded(
|
||||
Row(defaultModifier) {
|
||||
when (item) {
|
||||
is NoteCard -> NoteCompose(
|
||||
item.note,
|
||||
item,
|
||||
isBoostedNote = false,
|
||||
accountViewModel = accountViewModel,
|
||||
nav = nav,
|
||||
@@ -208,3 +218,36 @@ private fun FeedLoaded(
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun NoteCompose(
|
||||
baseNote: NoteCard,
|
||||
routeForLastRead: String? = null,
|
||||
modifier: Modifier = remember { Modifier },
|
||||
isBoostedNote: Boolean = false,
|
||||
isQuotedNote: Boolean = false,
|
||||
unPackReply: Boolean = true,
|
||||
makeItShort: Boolean = false,
|
||||
addMarginTop: Boolean = true,
|
||||
parentBackgroundColor: Color? = null,
|
||||
accountViewModel: AccountViewModel,
|
||||
nav: (String) -> Unit
|
||||
) {
|
||||
val note = remember(baseNote) {
|
||||
baseNote.note
|
||||
}
|
||||
|
||||
NoteCompose(
|
||||
baseNote = note,
|
||||
routeForLastRead = routeForLastRead,
|
||||
modifier = modifier,
|
||||
isBoostedNote = isBoostedNote,
|
||||
isQuotedNote = isQuotedNote,
|
||||
unPackReply = unPackReply,
|
||||
makeItShort = makeItShort,
|
||||
addMarginTop = addMarginTop,
|
||||
parentBackgroundColor = parentBackgroundColor,
|
||||
accountViewModel = accountViewModel,
|
||||
nav = nav
|
||||
)
|
||||
}
|
||||
|
||||
@@ -2,16 +2,14 @@ package com.vitorpamplona.amethyst.ui.screen
|
||||
|
||||
import androidx.compose.animation.Crossfade
|
||||
import androidx.compose.animation.core.tween
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.PaddingValues
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.LazyListState
|
||||
import androidx.compose.foundation.lazy.itemsIndexed
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.collectAsState
|
||||
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 com.vitorpamplona.amethyst.model.Note
|
||||
@@ -19,57 +17,86 @@ import com.vitorpamplona.amethyst.ui.note.ChatroomMessageCompose
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
||||
|
||||
@Composable
|
||||
fun ChatroomFeedView(viewModel: FeedViewModel, accountViewModel: AccountViewModel, nav: (String) -> Unit, routeForLastRead: String, onWantsToReply: (Note) -> Unit) {
|
||||
val feedState by viewModel.feedContent.collectAsState()
|
||||
|
||||
var isRefreshing by remember { mutableStateOf(false) }
|
||||
|
||||
val listState = rememberForeverLazyListState(routeForLastRead)
|
||||
|
||||
LaunchedEffect(isRefreshing) {
|
||||
if (isRefreshing) {
|
||||
viewModel.invalidateData()
|
||||
isRefreshing = false
|
||||
fun RefreshingChatroomFeedView(
|
||||
viewModel: FeedViewModel,
|
||||
accountViewModel: AccountViewModel,
|
||||
nav: (String) -> Unit,
|
||||
routeForLastRead: String,
|
||||
onWantsToReply: (Note) -> Unit,
|
||||
scrollStateKey: String? = null,
|
||||
enablePullRefresh: Boolean = true
|
||||
) {
|
||||
RefresheableView(viewModel, enablePullRefresh) {
|
||||
SaveableFeedState(viewModel, scrollStateKey) { listState ->
|
||||
RenderChatroomFeedView(viewModel, accountViewModel, listState, nav, routeForLastRead, onWantsToReply)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Column() {
|
||||
Crossfade(targetState = feedState, animationSpec = tween(durationMillis = 100)) { state ->
|
||||
when (state) {
|
||||
is FeedState.Empty -> {
|
||||
FeedEmpty {
|
||||
isRefreshing = true
|
||||
}
|
||||
}
|
||||
is FeedState.FeedError -> {
|
||||
FeedError(state.errorMessage) {
|
||||
isRefreshing = true
|
||||
}
|
||||
}
|
||||
is FeedState.Loaded -> {
|
||||
LaunchedEffect(state.feed.value.firstOrNull()) {
|
||||
if (listState.firstVisibleItemIndex <= 1) {
|
||||
listState.animateScrollToItem(0)
|
||||
}
|
||||
}
|
||||
@Composable
|
||||
fun RenderChatroomFeedView(
|
||||
viewModel: FeedViewModel,
|
||||
accountViewModel: AccountViewModel,
|
||||
listState: LazyListState,
|
||||
nav: (String) -> Unit,
|
||||
routeForLastRead: String,
|
||||
onWantsToReply: (Note) -> Unit
|
||||
) {
|
||||
val feedState by viewModel.feedContent.collectAsState()
|
||||
|
||||
LazyColumn(
|
||||
contentPadding = PaddingValues(
|
||||
top = 10.dp,
|
||||
bottom = 10.dp
|
||||
),
|
||||
reverseLayout = true,
|
||||
state = listState
|
||||
) {
|
||||
itemsIndexed(state.feed.value, key = { _, item -> item.idHex }) { _, item ->
|
||||
ChatroomMessageCompose(item, routeForLastRead, accountViewModel = accountViewModel, nav = nav, onWantsToReply = onWantsToReply)
|
||||
}
|
||||
}
|
||||
Crossfade(targetState = feedState, animationSpec = tween(durationMillis = 100)) { state ->
|
||||
when (state) {
|
||||
is FeedState.Empty -> {
|
||||
FeedEmpty {
|
||||
viewModel.invalidateData()
|
||||
}
|
||||
is FeedState.Loading -> {
|
||||
LoadingFeed()
|
||||
}
|
||||
is FeedState.FeedError -> {
|
||||
FeedError(state.errorMessage) {
|
||||
viewModel.invalidateData()
|
||||
}
|
||||
}
|
||||
is FeedState.Loaded -> {
|
||||
ChatroomFeedLoaded(state, accountViewModel, listState, nav, routeForLastRead, onWantsToReply)
|
||||
}
|
||||
is FeedState.Loading -> {
|
||||
LoadingFeed()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun ChatroomFeedLoaded(
|
||||
state: FeedState.Loaded,
|
||||
accountViewModel: AccountViewModel,
|
||||
listState: LazyListState,
|
||||
nav: (String) -> Unit,
|
||||
routeForLastRead: String,
|
||||
onWantsToReply: (Note) -> Unit
|
||||
) {
|
||||
LaunchedEffect(state.feed.value.firstOrNull()) {
|
||||
if (listState.firstVisibleItemIndex <= 1) {
|
||||
listState.animateScrollToItem(0)
|
||||
}
|
||||
}
|
||||
|
||||
LazyColumn(
|
||||
contentPadding = PaddingValues(
|
||||
top = 10.dp,
|
||||
bottom = 10.dp
|
||||
),
|
||||
reverseLayout = true,
|
||||
state = listState
|
||||
) {
|
||||
itemsIndexed(state.feed.value, key = { _, item -> item.idHex }) { _, item ->
|
||||
ChatroomMessageCompose(
|
||||
baseNote = item,
|
||||
routeForLastRead = routeForLastRead,
|
||||
accountViewModel = accountViewModel,
|
||||
nav = nav,
|
||||
onWantsToReply = onWantsToReply
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,6 +5,9 @@ import androidx.compose.animation.core.tween
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.PaddingValues
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.defaultMinSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.itemsIndexed
|
||||
import androidx.compose.foundation.lazy.rememberLazyListState
|
||||
@@ -16,7 +19,6 @@ import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.MutableState
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.livedata.observeAsState
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.setValue
|
||||
@@ -25,7 +27,6 @@ import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import com.vitorpamplona.amethyst.NotificationCache
|
||||
import com.vitorpamplona.amethyst.model.LocalCache
|
||||
import com.vitorpamplona.amethyst.service.model.PrivateDmEvent
|
||||
import com.vitorpamplona.amethyst.ui.note.ChatroomCompose
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
||||
@@ -90,12 +91,6 @@ private fun FeedLoaded(
|
||||
) {
|
||||
val listState = rememberLazyListState()
|
||||
|
||||
val accountState by accountViewModel.accountLiveData.observeAsState()
|
||||
val account = accountState?.account ?: return
|
||||
|
||||
val notificationCacheState = NotificationCache.live.observeAsState()
|
||||
val notificationCache = notificationCacheState.value ?: return
|
||||
|
||||
LaunchedEffect(key1 = markAsRead.value) {
|
||||
if (markAsRead.value) {
|
||||
for (note in state.feed.value) {
|
||||
@@ -104,21 +99,11 @@ private fun FeedLoaded(
|
||||
val route = if (channelHex != null) {
|
||||
"Channel/$channelHex"
|
||||
} else {
|
||||
val replyAuthorBase =
|
||||
(note.event as? PrivateDmEvent)
|
||||
?.verifiedRecipientPubKey()
|
||||
?.let { LocalCache.getOrCreateUser(it) }
|
||||
|
||||
var userToComposeOn = note.author!!
|
||||
if (replyAuthorBase != null) {
|
||||
if (note.author == account.userProfile()) {
|
||||
userToComposeOn = replyAuthorBase
|
||||
}
|
||||
}
|
||||
"Room/${userToComposeOn.pubkeyHex}"
|
||||
val roomUser = (note.event as? PrivateDmEvent)?.talkingWith(accountViewModel.account.userProfile().pubkeyHex)
|
||||
"Room/$roomUser"
|
||||
}
|
||||
|
||||
notificationCache.cache.markAsRead(route, it.createdAt())
|
||||
NotificationCache.markAsRead(route, it.createdAt())
|
||||
}
|
||||
}
|
||||
markAsRead.value = false
|
||||
@@ -136,11 +121,13 @@ private fun FeedLoaded(
|
||||
state.feed.value,
|
||||
key = { index, item -> if (index == 0) index else item.idHex }
|
||||
) { _, item ->
|
||||
ChatroomCompose(
|
||||
item,
|
||||
accountViewModel = accountViewModel,
|
||||
nav = nav
|
||||
)
|
||||
Row(Modifier.fillMaxWidth().defaultMinSize(minHeight = 75.dp)) {
|
||||
ChatroomCompose(
|
||||
item,
|
||||
accountViewModel = accountViewModel,
|
||||
nav = nav
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,6 +6,8 @@ import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.PaddingValues
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.defaultMinSize
|
||||
import androidx.compose.foundation.layout.fillMaxHeight
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
@@ -34,15 +36,27 @@ import com.vitorpamplona.amethyst.R
|
||||
import com.vitorpamplona.amethyst.ui.note.NoteCompose
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
||||
|
||||
@Composable
|
||||
fun RefresheableFeedView(
|
||||
viewModel: FeedViewModel,
|
||||
routeForLastRead: String?,
|
||||
accountViewModel: AccountViewModel,
|
||||
nav: (String) -> Unit,
|
||||
|
||||
scrollStateKey: String? = null,
|
||||
enablePullRefresh: Boolean = true
|
||||
) {
|
||||
RefresheableView(viewModel, enablePullRefresh) {
|
||||
SaveableFeedState(viewModel, accountViewModel, nav, routeForLastRead, scrollStateKey)
|
||||
}
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalMaterialApi::class)
|
||||
@Composable
|
||||
fun RefresheableView(
|
||||
viewModel: FeedViewModel,
|
||||
accountViewModel: AccountViewModel,
|
||||
nav: (String) -> Unit,
|
||||
routeForLastRead: String?,
|
||||
scrollStateKey: String? = null,
|
||||
enablePullRefresh: Boolean = true
|
||||
viewModel: InvalidatableViewModel,
|
||||
enablePullRefresh: Boolean = true,
|
||||
content: @Composable () -> Unit
|
||||
) {
|
||||
var refreshing by remember { mutableStateOf(false) }
|
||||
val refresh = { refreshing = true; viewModel.invalidateData(); refreshing = false }
|
||||
@@ -56,7 +70,7 @@ fun RefresheableView(
|
||||
|
||||
Box(modifier) {
|
||||
Column {
|
||||
SaveableFeedState(viewModel, accountViewModel, nav, routeForLastRead, scrollStateKey)
|
||||
content()
|
||||
}
|
||||
|
||||
if (enablePullRefresh) {
|
||||
@@ -72,6 +86,17 @@ private fun SaveableFeedState(
|
||||
nav: (String) -> Unit,
|
||||
routeForLastRead: String?,
|
||||
scrollStateKey: String? = null
|
||||
) {
|
||||
SaveableFeedState(viewModel, scrollStateKey) { listState ->
|
||||
RenderFeed(viewModel, accountViewModel, listState, nav, routeForLastRead)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun SaveableFeedState(
|
||||
viewModel: FeedViewModel,
|
||||
scrollStateKey: String? = null,
|
||||
content: @Composable (LazyListState) -> Unit
|
||||
) {
|
||||
val listState = if (scrollStateKey != null) {
|
||||
rememberForeverLazyListState(scrollStateKey)
|
||||
@@ -81,7 +106,7 @@ private fun SaveableFeedState(
|
||||
|
||||
WatchScrollToTop(viewModel, listState)
|
||||
|
||||
RenderFeed(viewModel, accountViewModel, listState, nav, routeForLastRead)
|
||||
content(listState)
|
||||
}
|
||||
|
||||
@Composable
|
||||
@@ -163,14 +188,16 @@ private fun FeedLoaded(
|
||||
state = listState
|
||||
) {
|
||||
itemsIndexed(state.feed.value, key = { _, item -> item.idHex }) { _, item ->
|
||||
NoteCompose(
|
||||
item,
|
||||
routeForLastRead = routeForLastRead,
|
||||
modifier = baseModifier,
|
||||
isBoostedNote = false,
|
||||
accountViewModel = accountViewModel,
|
||||
nav = nav
|
||||
)
|
||||
Row(Modifier.fillMaxWidth().defaultMinSize(minHeight = 75.dp)) {
|
||||
NoteCompose(
|
||||
item,
|
||||
routeForLastRead = routeForLastRead,
|
||||
modifier = baseModifier,
|
||||
isBoostedNote = false,
|
||||
accountViewModel = accountViewModel,
|
||||
nav = nav
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@ import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.ViewModelProvider
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import com.vitorpamplona.amethyst.model.Account
|
||||
import com.vitorpamplona.amethyst.model.Channel
|
||||
import com.vitorpamplona.amethyst.model.LocalCache
|
||||
import com.vitorpamplona.amethyst.model.Note
|
||||
import com.vitorpamplona.amethyst.model.User
|
||||
@@ -41,8 +42,21 @@ import kotlinx.coroutines.flow.asStateFlow
|
||||
import kotlinx.coroutines.flow.update
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
class NostrChannelFeedViewModel : FeedViewModel(ChannelFeedFilter)
|
||||
class NostrChatRoomFeedViewModel : FeedViewModel(ChatroomFeedFilter)
|
||||
class NostrChannelFeedViewModel(val channel: Channel, val account: Account) : FeedViewModel(ChannelFeedFilter(channel, account)) {
|
||||
class Factory(val channel: Channel, val account: Account) : ViewModelProvider.Factory {
|
||||
override fun <NostrChannelFeedViewModel : ViewModel> create(modelClass: Class<NostrChannelFeedViewModel>): NostrChannelFeedViewModel {
|
||||
return NostrChannelFeedViewModel(channel, account) as NostrChannelFeedViewModel
|
||||
}
|
||||
}
|
||||
}
|
||||
class NostrChatroomFeedViewModel(val user: User, val account: Account) : FeedViewModel(ChatroomFeedFilter(user, account)) {
|
||||
class Factory(val user: User, val account: Account) : ViewModelProvider.Factory {
|
||||
override fun <NostrChatRoomFeedViewModel : ViewModel> create(modelClass: Class<NostrChatRoomFeedViewModel>): NostrChatRoomFeedViewModel {
|
||||
return NostrChatroomFeedViewModel(user, account) as NostrChatRoomFeedViewModel
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class NostrGlobalFeedViewModel(val account: Account) : FeedViewModel(GlobalFeedFilter(account)) {
|
||||
class Factory(val account: Account) : ViewModelProvider.Factory {
|
||||
override fun <NostrGlobalFeedViewModel : ViewModel> create(modelClass: Class<NostrGlobalFeedViewModel>): NostrGlobalFeedViewModel {
|
||||
@@ -115,7 +129,7 @@ class NostrUserAppRecommendationsFeedViewModel(val user: User) : FeedViewModel(U
|
||||
}
|
||||
|
||||
@Stable
|
||||
abstract class FeedViewModel(val localFilter: FeedFilter<Note>) : ViewModel() {
|
||||
abstract class FeedViewModel(val localFilter: FeedFilter<Note>) : ViewModel(), InvalidatableViewModel {
|
||||
private val _feedContent = MutableStateFlow<FeedState>(FeedState.Loading)
|
||||
val feedContent = _feedContent.asStateFlow()
|
||||
|
||||
@@ -204,7 +218,7 @@ abstract class FeedViewModel(val localFilter: FeedFilter<Note>) : ViewModel() {
|
||||
private val bundler = BundledUpdate(250, Dispatchers.IO)
|
||||
private val bundlerInsert = BundledInsert<Set<Note>>(250, Dispatchers.IO)
|
||||
|
||||
fun invalidateData(ignoreIfDoing: Boolean = false) {
|
||||
override fun invalidateData(ignoreIfDoing: Boolean) {
|
||||
bundler.invalidate(ignoreIfDoing) {
|
||||
// adds the time to perform the refresh into this delay
|
||||
// holding off new updates in case of heavy refresh routines.
|
||||
|
||||
@@ -80,6 +80,7 @@ import com.vitorpamplona.amethyst.ui.note.ReactionsRow
|
||||
import com.vitorpamplona.amethyst.ui.note.timeAgo
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
||||
import com.vitorpamplona.amethyst.ui.theme.newItemBackgroundColor
|
||||
import kotlinx.collections.immutable.toImmutableList
|
||||
import kotlinx.coroutines.delay
|
||||
|
||||
@OptIn(ExperimentalMaterialApi::class)
|
||||
@@ -388,10 +389,7 @@ fun NoteMaster(
|
||||
|
||||
if (eventContent != null) {
|
||||
val hasSensitiveContent = remember(note.event) { note.event?.isSensitive() ?: false }
|
||||
|
||||
val tags = remember(note) {
|
||||
note.event?.tags() ?: emptyList()
|
||||
}
|
||||
val tags = remember(note) { note.event?.tags()?.toImmutableList() ?: emptyList<List<String>>().toImmutableList() }
|
||||
|
||||
SensitivityWarning(
|
||||
hasSensitiveContent = hasSensitiveContent,
|
||||
|
||||
@@ -2,29 +2,18 @@ package com.vitorpamplona.amethyst.ui.screen
|
||||
|
||||
import androidx.compose.animation.Crossfade
|
||||
import androidx.compose.animation.core.tween
|
||||
import androidx.compose.foundation.layout.Box
|
||||
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.material.ExperimentalMaterialApi
|
||||
import androidx.compose.material.pullrefresh.PullRefreshIndicator
|
||||
import androidx.compose.material.pullrefresh.pullRefresh
|
||||
import androidx.compose.material.pullrefresh.rememberPullRefreshState
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.collectAsState
|
||||
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 com.vitorpamplona.amethyst.ui.note.UserCompose
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
||||
|
||||
@OptIn(ExperimentalMaterialApi::class)
|
||||
@Composable
|
||||
fun RefreshingFeedUserFeedView(
|
||||
viewModel: UserFeedViewModel,
|
||||
@@ -32,24 +21,8 @@ fun RefreshingFeedUserFeedView(
|
||||
nav: (String) -> Unit,
|
||||
enablePullRefresh: Boolean = true
|
||||
) {
|
||||
var refreshing by remember { mutableStateOf(false) }
|
||||
val refresh = { refreshing = true; viewModel.invalidateData(); refreshing = false }
|
||||
val pullRefreshState = rememberPullRefreshState(refreshing, onRefresh = refresh)
|
||||
|
||||
val modifier = if (enablePullRefresh) {
|
||||
Modifier.pullRefresh(pullRefreshState)
|
||||
} else {
|
||||
Modifier
|
||||
}
|
||||
|
||||
Box(modifier) {
|
||||
Column() {
|
||||
UserFeedView(viewModel, accountViewModel, nav)
|
||||
}
|
||||
|
||||
if (enablePullRefresh) {
|
||||
PullRefreshIndicator(refreshing, pullRefreshState, Modifier.align(Alignment.TopCenter))
|
||||
}
|
||||
RefresheableView(viewModel, enablePullRefresh) {
|
||||
UserFeedView(viewModel, accountViewModel, nav)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -47,7 +47,7 @@ class NostrHiddenAccountsFeedViewModel(val account: Account) : UserFeedViewModel
|
||||
}
|
||||
}
|
||||
|
||||
open class UserFeedViewModel(val dataSource: FeedFilter<User>) : ViewModel() {
|
||||
open class UserFeedViewModel(val dataSource: FeedFilter<User>) : ViewModel(), InvalidatableViewModel {
|
||||
private val _feedContent = MutableStateFlow<UserFeedState>(UserFeedState.Loading)
|
||||
val feedContent = _feedContent.asStateFlow()
|
||||
|
||||
@@ -91,8 +91,8 @@ open class UserFeedViewModel(val dataSource: FeedFilter<User>) : ViewModel() {
|
||||
|
||||
private val bundler = BundledUpdate(250, Dispatchers.IO)
|
||||
|
||||
fun invalidateData() {
|
||||
bundler.invalidate() {
|
||||
override fun invalidateData(ignoreIfDoing: Boolean) {
|
||||
bundler.invalidate(ignoreIfDoing) {
|
||||
// adds the time to perform the refresh into this delay
|
||||
// holding off new updates in case of heavy refresh routines.
|
||||
refreshSuspended()
|
||||
@@ -114,3 +114,7 @@ open class UserFeedViewModel(val dataSource: FeedFilter<User>) : ViewModel() {
|
||||
super.onCleared()
|
||||
}
|
||||
}
|
||||
|
||||
interface InvalidatableViewModel {
|
||||
fun invalidateData(ignoreIfDoing: Boolean = false)
|
||||
}
|
||||
|
||||
@@ -59,10 +59,6 @@ class AccountViewModel(val account: Account) : ViewModel() {
|
||||
account.delete(account.boostsTo(note))
|
||||
}
|
||||
|
||||
fun zap(note: Note, amount: Long, pollOption: Int?, message: String, context: Context, onError: (String) -> Unit, onProgress: (percent: Float) -> Unit) {
|
||||
zap(note, amount, pollOption, message, context, onError, onProgress, account.defaultZapType)
|
||||
}
|
||||
|
||||
fun calculateIfNoteWasZappedByAccount(zappedNote: Note): Boolean {
|
||||
return account.calculateIfNoteWasZappedByAccount(zappedNote)
|
||||
}
|
||||
|
||||
+3
-3
@@ -24,7 +24,7 @@ import com.vitorpamplona.amethyst.ui.dal.BookmarkPrivateFeedFilter
|
||||
import com.vitorpamplona.amethyst.ui.dal.BookmarkPublicFeedFilter
|
||||
import com.vitorpamplona.amethyst.ui.screen.NostrBookmarkPrivateFeedViewModel
|
||||
import com.vitorpamplona.amethyst.ui.screen.NostrBookmarkPublicFeedViewModel
|
||||
import com.vitorpamplona.amethyst.ui.screen.RefresheableView
|
||||
import com.vitorpamplona.amethyst.ui.screen.RefresheableFeedView
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
@OptIn(ExperimentalFoundationApi::class)
|
||||
@@ -73,8 +73,8 @@ fun BookmarkListScreen(accountViewModel: AccountViewModel, nav: (String) -> Unit
|
||||
}
|
||||
HorizontalPager(pageCount = 2, state = pagerState) { page ->
|
||||
when (page) {
|
||||
0 -> RefresheableView(privateFeedViewModel, accountViewModel, nav, null)
|
||||
1 -> RefresheableView(publicFeedViewModel, accountViewModel, nav, null)
|
||||
0 -> RefresheableFeedView(privateFeedViewModel, null, accountViewModel, nav)
|
||||
1 -> RefresheableFeedView(publicFeedViewModel, null, accountViewModel, nav)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+238
-156
@@ -74,13 +74,13 @@ import com.vitorpamplona.amethyst.ui.actions.PostButton
|
||||
import com.vitorpamplona.amethyst.ui.actions.UploadFromGallery
|
||||
import com.vitorpamplona.amethyst.ui.components.ResizeImage
|
||||
import com.vitorpamplona.amethyst.ui.components.RobohashAsyncImageProxy
|
||||
import com.vitorpamplona.amethyst.ui.dal.ChannelFeedFilter
|
||||
import com.vitorpamplona.amethyst.ui.navigation.Route
|
||||
import com.vitorpamplona.amethyst.ui.note.ChatroomMessageCompose
|
||||
import com.vitorpamplona.amethyst.ui.screen.ChatroomFeedView
|
||||
import com.vitorpamplona.amethyst.ui.screen.NostrChannelFeedViewModel
|
||||
import com.vitorpamplona.amethyst.ui.screen.RefreshingChatroomFeedView
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
|
||||
@Composable
|
||||
fun ChannelScreen(
|
||||
@@ -88,174 +88,256 @@ fun ChannelScreen(
|
||||
accountViewModel: AccountViewModel,
|
||||
nav: (String) -> Unit
|
||||
) {
|
||||
val accountState by accountViewModel.accountLiveData.observeAsState()
|
||||
val account = accountState?.account
|
||||
val context = LocalContext.current
|
||||
if (channelId == null) return
|
||||
|
||||
var channelBase by remember { mutableStateOf<Channel?>(null) }
|
||||
|
||||
LaunchedEffect(channelId) {
|
||||
withContext(Dispatchers.IO) {
|
||||
val newChannelBase = LocalCache.checkGetOrCreateChannel(channelId)
|
||||
if (newChannelBase != channelBase) {
|
||||
channelBase = newChannelBase
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
channelBase?.let {
|
||||
PrepareChannelViewModels(
|
||||
baseChannel = it,
|
||||
accountViewModel = accountViewModel,
|
||||
nav = nav
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun PrepareChannelViewModels(baseChannel: Channel, accountViewModel: AccountViewModel, nav: (String) -> Unit) {
|
||||
val feedViewModel: NostrChannelFeedViewModel = viewModel(
|
||||
key = baseChannel.idHex + "ChannelFeedViewModel",
|
||||
factory = NostrChannelFeedViewModel.Factory(
|
||||
baseChannel,
|
||||
accountViewModel.account
|
||||
)
|
||||
)
|
||||
|
||||
val channelScreenModel: NewPostViewModel = viewModel()
|
||||
channelScreenModel.account = accountViewModel.account
|
||||
|
||||
channelScreenModel.account = account
|
||||
ChannelScreen(
|
||||
channel = baseChannel,
|
||||
feedViewModel = feedViewModel,
|
||||
newPostModel = channelScreenModel,
|
||||
accountViewModel = accountViewModel,
|
||||
nav = nav
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun ChannelScreen(
|
||||
channel: Channel,
|
||||
feedViewModel: NostrChannelFeedViewModel,
|
||||
newPostModel: NewPostViewModel,
|
||||
accountViewModel: AccountViewModel,
|
||||
nav: (String) -> Unit
|
||||
) {
|
||||
val context = LocalContext.current
|
||||
|
||||
NostrChannelDataSource.loadMessagesBetween(accountViewModel.account, channel)
|
||||
|
||||
val lifeCycleOwner = LocalLifecycleOwner.current
|
||||
|
||||
LaunchedEffect(Unit) {
|
||||
NostrChannelDataSource.start()
|
||||
|
||||
feedViewModel.invalidateData()
|
||||
newPostModel.imageUploadingError.collect { error ->
|
||||
Toast.makeText(context, error, Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
}
|
||||
|
||||
DisposableEffect(accountViewModel) {
|
||||
val observer = LifecycleEventObserver { _, event ->
|
||||
if (event == Lifecycle.Event.ON_RESUME) {
|
||||
println("Channel Start")
|
||||
NostrChannelDataSource.start()
|
||||
feedViewModel.invalidateData()
|
||||
}
|
||||
if (event == Lifecycle.Event.ON_PAUSE) {
|
||||
println("Channel Stop")
|
||||
|
||||
NostrChannelDataSource.clear()
|
||||
NostrChannelDataSource.stop()
|
||||
}
|
||||
}
|
||||
|
||||
lifeCycleOwner.lifecycle.addObserver(observer)
|
||||
onDispose {
|
||||
lifeCycleOwner.lifecycle.removeObserver(observer)
|
||||
}
|
||||
}
|
||||
|
||||
Column(Modifier.fillMaxHeight()) {
|
||||
ChannelHeader(
|
||||
channel,
|
||||
accountViewModel,
|
||||
nav = nav
|
||||
)
|
||||
|
||||
if (account != null && channelId != null) {
|
||||
val replyTo = remember { mutableStateOf<Note?>(null) }
|
||||
|
||||
ChannelFeedFilter.loadMessagesBetween(account, channelId)
|
||||
NostrChannelDataSource.loadMessagesBetween(account, channelId)
|
||||
|
||||
val channelState by NostrChannelDataSource.channel!!.live.observeAsState()
|
||||
val channel = channelState?.channel ?: return
|
||||
|
||||
ChannelFeedFilter.loadMessagesBetween(account, channelId)
|
||||
|
||||
val feedViewModel: NostrChannelFeedViewModel = viewModel()
|
||||
val lifeCycleOwner = LocalLifecycleOwner.current
|
||||
|
||||
LaunchedEffect(Unit) {
|
||||
ChannelFeedFilter.loadMessagesBetween(account, channelId)
|
||||
NostrChannelDataSource.loadMessagesBetween(account, channelId)
|
||||
|
||||
feedViewModel.invalidateData()
|
||||
channelScreenModel.imageUploadingError.collect { error ->
|
||||
Toast.makeText(context, error, Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
}
|
||||
|
||||
DisposableEffect(channelId) {
|
||||
val observer = LifecycleEventObserver { _, event ->
|
||||
if (event == Lifecycle.Event.ON_RESUME) {
|
||||
println("Channel Start")
|
||||
ChannelFeedFilter.loadMessagesBetween(account, channelId)
|
||||
NostrChannelDataSource.loadMessagesBetween(account, channelId)
|
||||
|
||||
feedViewModel.invalidateData()
|
||||
}
|
||||
if (event == Lifecycle.Event.ON_PAUSE) {
|
||||
println("Channel Stop")
|
||||
|
||||
NostrChannelDataSource.clear()
|
||||
NostrChannelDataSource.stop()
|
||||
}
|
||||
}
|
||||
|
||||
lifeCycleOwner.lifecycle.addObserver(observer)
|
||||
onDispose {
|
||||
lifeCycleOwner.lifecycle.removeObserver(observer)
|
||||
}
|
||||
}
|
||||
|
||||
Column(Modifier.fillMaxHeight()) {
|
||||
ChannelHeader(
|
||||
channel,
|
||||
accountViewModel,
|
||||
nav = nav
|
||||
)
|
||||
|
||||
Column(
|
||||
modifier = Modifier
|
||||
Column(
|
||||
modifier = remember {
|
||||
Modifier
|
||||
.fillMaxHeight()
|
||||
.padding(vertical = 0.dp)
|
||||
.weight(1f, true)
|
||||
) {
|
||||
ChatroomFeedView(feedViewModel, accountViewModel, nav, "Channel/$channelId") {
|
||||
}
|
||||
) {
|
||||
RefreshingChatroomFeedView(
|
||||
viewModel = feedViewModel,
|
||||
accountViewModel = accountViewModel,
|
||||
nav = nav,
|
||||
routeForLastRead = "Channel/${channel.idHex}",
|
||||
onWantsToReply = {
|
||||
replyTo.value = it
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
Spacer(modifier = Modifier.height(10.dp))
|
||||
Spacer(modifier = Modifier.height(10.dp))
|
||||
|
||||
Row(
|
||||
Modifier
|
||||
.padding(horizontal = 10.dp)
|
||||
.animateContentSize(),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
val replyingNote = replyTo.value
|
||||
if (replyingNote != null) {
|
||||
Column(Modifier.weight(1f)) {
|
||||
ChatroomMessageCompose(
|
||||
baseNote = replyingNote,
|
||||
null,
|
||||
innerQuote = true,
|
||||
accountViewModel = accountViewModel,
|
||||
nav = nav,
|
||||
onWantsToReply = {
|
||||
replyTo.value = it
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
Column(Modifier.padding(end = 10.dp)) {
|
||||
IconButton(
|
||||
modifier = Modifier.size(30.dp),
|
||||
onClick = { replyTo.value = null }
|
||||
) {
|
||||
Icon(
|
||||
imageVector = Icons.Default.Cancel,
|
||||
null,
|
||||
modifier = Modifier
|
||||
.padding(end = 5.dp)
|
||||
.size(30.dp),
|
||||
tint = MaterialTheme.colors.onSurface.copy(alpha = 0.32f)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// LAST ROW
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.padding(start = 10.dp, end = 10.dp, bottom = 10.dp, top = 5.dp)
|
||||
.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.SpaceBetween,
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
TextField(
|
||||
value = channelScreenModel.message,
|
||||
onValueChange = {
|
||||
channelScreenModel.updateMessage(it)
|
||||
},
|
||||
keyboardOptions = KeyboardOptions.Default.copy(
|
||||
capitalization = KeyboardCapitalization.Sentences
|
||||
),
|
||||
shape = RoundedCornerShape(25.dp),
|
||||
modifier = Modifier.weight(1f, true),
|
||||
placeholder = {
|
||||
Text(
|
||||
text = stringResource(R.string.reply_here),
|
||||
color = MaterialTheme.colors.onSurface.copy(alpha = 0.32f)
|
||||
)
|
||||
},
|
||||
textStyle = LocalTextStyle.current.copy(textDirection = TextDirection.Content),
|
||||
trailingIcon = {
|
||||
PostButton(
|
||||
onPost = {
|
||||
val tagger = NewMessageTagger(channel.idHex, listOfNotNull(replyTo.value?.author), listOfNotNull(replyTo.value), channelScreenModel.message.text)
|
||||
tagger.run()
|
||||
account.sendChannelMessage(tagger.message, channel.idHex, tagger.replyTos, tagger.mentions, wantsToMarkAsSensitive = false)
|
||||
channelScreenModel.message = TextFieldValue("")
|
||||
replyTo.value = null
|
||||
feedViewModel.invalidateData() // Don't wait a full second before updating
|
||||
},
|
||||
isActive = channelScreenModel.message.text.isNotBlank() && !channelScreenModel.isUploadingImage,
|
||||
modifier = Modifier.padding(end = 10.dp)
|
||||
)
|
||||
},
|
||||
leadingIcon = {
|
||||
UploadFromGallery(
|
||||
isUploading = channelScreenModel.isUploadingImage,
|
||||
tint = MaterialTheme.colors.onSurface.copy(alpha = 0.32f),
|
||||
modifier = Modifier.padding(start = 5.dp)
|
||||
) {
|
||||
channelScreenModel.upload(it, "", account.defaultFileServer, context)
|
||||
}
|
||||
},
|
||||
colors = TextFieldDefaults.textFieldColors(
|
||||
focusedIndicatorColor = Color.Transparent,
|
||||
unfocusedIndicatorColor = Color.Transparent
|
||||
)
|
||||
)
|
||||
replyTo.value?.let {
|
||||
DisplayReplyingToNote(it, accountViewModel, nav) {
|
||||
replyTo.value = null
|
||||
}
|
||||
}
|
||||
|
||||
val scope = rememberCoroutineScope()
|
||||
|
||||
// LAST ROW
|
||||
EditFieldRow(newPostModel, accountViewModel) {
|
||||
scope.launch {
|
||||
val tagger = NewMessageTagger(
|
||||
channelHex = channel.idHex,
|
||||
mentions = listOfNotNull(replyTo.value?.author),
|
||||
replyTos = listOfNotNull(replyTo.value),
|
||||
message = newPostModel.message.text
|
||||
)
|
||||
tagger.run()
|
||||
accountViewModel.account.sendChannelMessage(
|
||||
message = tagger.message,
|
||||
toChannel = channel.idHex,
|
||||
replyTo = tagger.replyTos,
|
||||
mentions = tagger.mentions,
|
||||
wantsToMarkAsSensitive = false
|
||||
)
|
||||
newPostModel.message = TextFieldValue("")
|
||||
replyTo.value = null
|
||||
feedViewModel.sendToTop()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun DisplayReplyingToNote(
|
||||
replyingNote: Note?,
|
||||
accountViewModel: AccountViewModel,
|
||||
nav: (String) -> Unit,
|
||||
onCancel: () -> Unit
|
||||
) {
|
||||
Row(
|
||||
Modifier
|
||||
.padding(horizontal = 10.dp)
|
||||
.animateContentSize(),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
if (replyingNote != null) {
|
||||
Column(remember { Modifier.weight(1f) }) {
|
||||
ChatroomMessageCompose(
|
||||
baseNote = replyingNote,
|
||||
null,
|
||||
innerQuote = true,
|
||||
accountViewModel = accountViewModel,
|
||||
nav = nav,
|
||||
onWantsToReply = {}
|
||||
)
|
||||
}
|
||||
|
||||
Column(Modifier.padding(end = 10.dp)) {
|
||||
IconButton(
|
||||
modifier = Modifier.size(30.dp),
|
||||
onClick = onCancel
|
||||
) {
|
||||
Icon(
|
||||
imageVector = Icons.Default.Cancel,
|
||||
null,
|
||||
modifier = Modifier
|
||||
.padding(end = 5.dp)
|
||||
.size(30.dp),
|
||||
tint = MaterialTheme.colors.onSurface.copy(alpha = 0.32f)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun EditFieldRow(
|
||||
channelScreenModel: NewPostViewModel,
|
||||
accountViewModel: AccountViewModel,
|
||||
onSendNewMessage: () -> Unit
|
||||
) {
|
||||
val context = LocalContext.current
|
||||
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.padding(start = 10.dp, end = 10.dp, bottom = 10.dp, top = 5.dp)
|
||||
.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.SpaceBetween,
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
TextField(
|
||||
value = channelScreenModel.message,
|
||||
onValueChange = {
|
||||
channelScreenModel.updateMessage(it)
|
||||
},
|
||||
keyboardOptions = KeyboardOptions.Default.copy(
|
||||
capitalization = KeyboardCapitalization.Sentences
|
||||
),
|
||||
shape = RoundedCornerShape(25.dp),
|
||||
modifier = Modifier.weight(1f, true),
|
||||
placeholder = {
|
||||
Text(
|
||||
text = stringResource(R.string.reply_here),
|
||||
color = MaterialTheme.colors.onSurface.copy(alpha = 0.32f)
|
||||
)
|
||||
},
|
||||
textStyle = LocalTextStyle.current.copy(textDirection = TextDirection.Content),
|
||||
trailingIcon = {
|
||||
PostButton(
|
||||
onPost = {
|
||||
onSendNewMessage()
|
||||
},
|
||||
isActive = channelScreenModel.message.text.isNotBlank() && !channelScreenModel.isUploadingImage,
|
||||
modifier = Modifier.padding(end = 10.dp)
|
||||
)
|
||||
},
|
||||
leadingIcon = {
|
||||
UploadFromGallery(
|
||||
isUploading = channelScreenModel.isUploadingImage,
|
||||
tint = MaterialTheme.colors.onSurface.copy(alpha = 0.32f),
|
||||
modifier = Modifier.padding(start = 5.dp)
|
||||
) {
|
||||
channelScreenModel.upload(it, "", accountViewModel.account.defaultFileServer, context)
|
||||
}
|
||||
},
|
||||
colors = TextFieldDefaults.textFieldColors(
|
||||
focusedIndicatorColor = Color.Transparent,
|
||||
unfocusedIndicatorColor = Color.Transparent
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+135
-155
@@ -1,202 +1,182 @@
|
||||
package com.vitorpamplona.amethyst.ui.screen.loggedIn
|
||||
|
||||
import android.widget.Toast
|
||||
import androidx.compose.animation.animateContentSize
|
||||
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.Spacer
|
||||
import androidx.compose.foundation.layout.fillMaxHeight
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.foundation.text.KeyboardOptions
|
||||
import androidx.compose.material.Divider
|
||||
import androidx.compose.material.Icon
|
||||
import androidx.compose.material.IconButton
|
||||
import androidx.compose.material.LocalTextStyle
|
||||
import androidx.compose.material.MaterialTheme
|
||||
import androidx.compose.material.Text
|
||||
import androidx.compose.material.TextField
|
||||
import androidx.compose.material.TextFieldDefaults
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.Cancel
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.DisposableEffect
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.rememberCoroutineScope
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.platform.LocalLifecycleOwner
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.input.KeyboardCapitalization
|
||||
import androidx.compose.ui.text.input.TextFieldValue
|
||||
import androidx.compose.ui.text.style.TextDirection
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.lifecycle.Lifecycle
|
||||
import androidx.lifecycle.LifecycleEventObserver
|
||||
import androidx.lifecycle.viewmodel.compose.viewModel
|
||||
import com.vitorpamplona.amethyst.R
|
||||
import com.vitorpamplona.amethyst.model.LocalCache
|
||||
import com.vitorpamplona.amethyst.model.Note
|
||||
import com.vitorpamplona.amethyst.model.User
|
||||
import com.vitorpamplona.amethyst.service.NostrChatroomDataSource
|
||||
import com.vitorpamplona.amethyst.ui.actions.NewPostViewModel
|
||||
import com.vitorpamplona.amethyst.ui.actions.PostButton
|
||||
import com.vitorpamplona.amethyst.ui.actions.UploadFromGallery
|
||||
import com.vitorpamplona.amethyst.ui.components.ObserveDisplayNip05Status
|
||||
import com.vitorpamplona.amethyst.ui.dal.ChatroomFeedFilter
|
||||
import com.vitorpamplona.amethyst.ui.note.ChatroomMessageCompose
|
||||
import com.vitorpamplona.amethyst.ui.note.UserPicture
|
||||
import com.vitorpamplona.amethyst.ui.note.UsernameDisplay
|
||||
import com.vitorpamplona.amethyst.ui.screen.ChatroomFeedView
|
||||
import com.vitorpamplona.amethyst.ui.screen.NostrChatRoomFeedViewModel
|
||||
import com.vitorpamplona.amethyst.ui.screen.NostrChatroomFeedViewModel
|
||||
import com.vitorpamplona.amethyst.ui.screen.RefreshingChatroomFeedView
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
|
||||
@Composable
|
||||
fun ChatroomScreen(userId: String?, accountViewModel: AccountViewModel, nav: (String) -> Unit) {
|
||||
val context = LocalContext.current
|
||||
val chatRoomScreenModel: NewPostViewModel = viewModel()
|
||||
chatRoomScreenModel.account = accountViewModel.account
|
||||
fun ChatroomScreen(
|
||||
userId: String?,
|
||||
accountViewModel: AccountViewModel,
|
||||
nav: (String) -> Unit
|
||||
) {
|
||||
if (userId == null) return
|
||||
|
||||
var userRoom by remember { mutableStateOf<User?>(null) }
|
||||
|
||||
LaunchedEffect(userId) {
|
||||
withContext(Dispatchers.IO) {
|
||||
val newUser = LocalCache.checkGetOrCreateUser(userId)
|
||||
if (newUser != userRoom) {
|
||||
userRoom = newUser
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
userRoom?.let {
|
||||
PrepareChatroomViewModels(
|
||||
baseUser = it,
|
||||
accountViewModel = accountViewModel,
|
||||
nav = nav
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun PrepareChatroomViewModels(baseUser: User, accountViewModel: AccountViewModel, nav: (String) -> Unit) {
|
||||
val feedViewModel: NostrChatroomFeedViewModel = viewModel(
|
||||
key = baseUser.pubkeyHex + "ChatroomViewModels",
|
||||
factory = NostrChatroomFeedViewModel.Factory(
|
||||
baseUser,
|
||||
accountViewModel.account
|
||||
)
|
||||
)
|
||||
|
||||
val newPostModel: NewPostViewModel = viewModel()
|
||||
newPostModel.account = accountViewModel.account
|
||||
|
||||
ChatroomScreen(
|
||||
baseUser = baseUser,
|
||||
feedViewModel = feedViewModel,
|
||||
newPostModel = newPostModel,
|
||||
accountViewModel = accountViewModel,
|
||||
nav = nav
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun ChatroomScreen(
|
||||
baseUser: User,
|
||||
feedViewModel: NostrChatroomFeedViewModel,
|
||||
newPostModel: NewPostViewModel,
|
||||
accountViewModel: AccountViewModel,
|
||||
nav: (String) -> Unit
|
||||
) {
|
||||
val context = LocalContext.current
|
||||
|
||||
NostrChatroomDataSource.loadMessagesBetween(accountViewModel.account, baseUser)
|
||||
|
||||
val lifeCycleOwner = LocalLifecycleOwner.current
|
||||
|
||||
LaunchedEffect(baseUser, accountViewModel) {
|
||||
NostrChatroomDataSource.start()
|
||||
|
||||
feedViewModel.invalidateData()
|
||||
newPostModel.imageUploadingError.collect { error ->
|
||||
Toast.makeText(context, error, Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
}
|
||||
|
||||
DisposableEffect(baseUser, accountViewModel) {
|
||||
val observer = LifecycleEventObserver { _, event ->
|
||||
if (event == Lifecycle.Event.ON_RESUME) {
|
||||
println("Private Message Start")
|
||||
NostrChatroomDataSource.start()
|
||||
feedViewModel.invalidateData()
|
||||
}
|
||||
if (event == Lifecycle.Event.ON_PAUSE) {
|
||||
println("Private Message Stop")
|
||||
NostrChatroomDataSource.stop()
|
||||
}
|
||||
}
|
||||
|
||||
lifeCycleOwner.lifecycle.addObserver(observer)
|
||||
onDispose {
|
||||
lifeCycleOwner.lifecycle.removeObserver(observer)
|
||||
}
|
||||
}
|
||||
|
||||
Column(Modifier.fillMaxHeight()) {
|
||||
ChatroomHeader(baseUser, accountViewModel, nav = nav)
|
||||
|
||||
if (userId != null) {
|
||||
val replyTo = remember { mutableStateOf<Note?>(null) }
|
||||
|
||||
ChatroomFeedFilter.loadMessagesBetween(accountViewModel.account, userId)
|
||||
NostrChatroomDataSource.loadMessagesBetween(accountViewModel.account, userId)
|
||||
|
||||
val feedViewModel: NostrChatRoomFeedViewModel = viewModel()
|
||||
val lifeCycleOwner = LocalLifecycleOwner.current
|
||||
|
||||
LaunchedEffect(userId) {
|
||||
feedViewModel.invalidateData()
|
||||
chatRoomScreenModel.imageUploadingError.collect { error ->
|
||||
Toast.makeText(context, error, Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
}
|
||||
|
||||
DisposableEffect(userId) {
|
||||
val observer = LifecycleEventObserver { _, event ->
|
||||
if (event == Lifecycle.Event.ON_RESUME) {
|
||||
println("Private Message Start")
|
||||
NostrChatroomDataSource.start()
|
||||
feedViewModel.invalidateData()
|
||||
}
|
||||
if (event == Lifecycle.Event.ON_PAUSE) {
|
||||
println("Private Message Stop")
|
||||
NostrChatroomDataSource.stop()
|
||||
}
|
||||
}
|
||||
|
||||
lifeCycleOwner.lifecycle.addObserver(observer)
|
||||
onDispose {
|
||||
lifeCycleOwner.lifecycle.removeObserver(observer)
|
||||
}
|
||||
}
|
||||
|
||||
Column(Modifier.fillMaxHeight()) {
|
||||
NostrChatroomDataSource.withUser?.let {
|
||||
ChatroomHeader(it, accountViewModel, nav = nav)
|
||||
}
|
||||
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxHeight()
|
||||
.padding(vertical = 0.dp)
|
||||
.weight(1f, true)
|
||||
) {
|
||||
ChatroomFeedView(feedViewModel, accountViewModel, nav, "Room/$userId") {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxHeight()
|
||||
.padding(vertical = 0.dp)
|
||||
.weight(1f, true)
|
||||
) {
|
||||
RefreshingChatroomFeedView(
|
||||
viewModel = feedViewModel,
|
||||
accountViewModel = accountViewModel,
|
||||
nav = nav,
|
||||
routeForLastRead = "Room/${baseUser.pubkeyHex}",
|
||||
onWantsToReply = {
|
||||
replyTo.value = it
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
Spacer(modifier = Modifier.height(10.dp))
|
||||
|
||||
replyTo.value?.let {
|
||||
DisplayReplyingToNote(it, accountViewModel, nav) {
|
||||
replyTo.value = null
|
||||
}
|
||||
}
|
||||
|
||||
Spacer(modifier = Modifier.height(10.dp))
|
||||
val scope = rememberCoroutineScope()
|
||||
|
||||
Row(Modifier.padding(horizontal = 10.dp).animateContentSize(), verticalAlignment = Alignment.CenterVertically) {
|
||||
val replyingNote = replyTo.value
|
||||
if (replyingNote != null) {
|
||||
Column(Modifier.weight(1f)) {
|
||||
ChatroomMessageCompose(
|
||||
baseNote = replyingNote,
|
||||
null,
|
||||
innerQuote = true,
|
||||
accountViewModel = accountViewModel,
|
||||
nav = nav,
|
||||
onWantsToReply = {
|
||||
replyTo.value = it
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
Column(Modifier.padding(end = 10.dp)) {
|
||||
IconButton(
|
||||
modifier = Modifier.size(30.dp),
|
||||
onClick = { replyTo.value = null }
|
||||
) {
|
||||
Icon(
|
||||
imageVector = Icons.Default.Cancel,
|
||||
null,
|
||||
modifier = Modifier.padding(end = 5.dp).size(30.dp),
|
||||
tint = MaterialTheme.colors.onSurface.copy(alpha = 0.32f)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// LAST ROW
|
||||
Row(
|
||||
modifier = Modifier.padding(start = 10.dp, end = 10.dp, bottom = 10.dp, top = 5.dp)
|
||||
.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.SpaceBetween,
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
TextField(
|
||||
value = chatRoomScreenModel.message,
|
||||
onValueChange = { chatRoomScreenModel.updateMessage(it) },
|
||||
keyboardOptions = KeyboardOptions.Default.copy(
|
||||
capitalization = KeyboardCapitalization.Sentences
|
||||
),
|
||||
modifier = Modifier.weight(1f, true),
|
||||
shape = RoundedCornerShape(25.dp),
|
||||
placeholder = {
|
||||
Text(
|
||||
text = stringResource(id = R.string.reply_here),
|
||||
color = MaterialTheme.colors.onSurface.copy(alpha = 0.32f)
|
||||
)
|
||||
},
|
||||
textStyle = LocalTextStyle.current.copy(textDirection = TextDirection.Content),
|
||||
trailingIcon = {
|
||||
PostButton(
|
||||
onPost = {
|
||||
accountViewModel.account.sendPrivateMessage(chatRoomScreenModel.message.text, userId, replyTo.value, null, wantsToMarkAsSensitive = false)
|
||||
chatRoomScreenModel.message = TextFieldValue("")
|
||||
replyTo.value = null
|
||||
feedViewModel.invalidateData() // Don't wait a full second before updating
|
||||
},
|
||||
isActive = chatRoomScreenModel.message.text.isNotBlank() && !chatRoomScreenModel.isUploadingImage,
|
||||
modifier = Modifier.padding(end = 10.dp)
|
||||
)
|
||||
},
|
||||
leadingIcon = {
|
||||
UploadFromGallery(
|
||||
isUploading = chatRoomScreenModel.isUploadingImage,
|
||||
tint = MaterialTheme.colors.onSurface.copy(alpha = 0.32f),
|
||||
modifier = Modifier.padding(start = 5.dp)
|
||||
) {
|
||||
chatRoomScreenModel.upload(it, "", accountViewModel.account.defaultFileServer, context)
|
||||
}
|
||||
},
|
||||
colors = TextFieldDefaults.textFieldColors(
|
||||
focusedIndicatorColor = Color.Transparent,
|
||||
unfocusedIndicatorColor = Color.Transparent
|
||||
)
|
||||
// LAST ROW
|
||||
EditFieldRow(newPostModel, accountViewModel) {
|
||||
scope.launch(Dispatchers.IO) {
|
||||
accountViewModel.account.sendPrivateMessage(
|
||||
message = newPostModel.message.text,
|
||||
toUser = baseUser,
|
||||
replyingTo = replyTo.value,
|
||||
mentions = null,
|
||||
wantsToMarkAsSensitive = false
|
||||
)
|
||||
newPostModel.message = TextFieldValue("")
|
||||
replyTo.value = null
|
||||
feedViewModel.sendToTop()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -27,7 +27,7 @@ import androidx.lifecycle.viewmodel.compose.viewModel
|
||||
import com.vitorpamplona.amethyst.service.NostrHashtagDataSource
|
||||
import com.vitorpamplona.amethyst.ui.dal.HashtagFeedFilter
|
||||
import com.vitorpamplona.amethyst.ui.screen.NostrHashtagFeedViewModel
|
||||
import com.vitorpamplona.amethyst.ui.screen.RefresheableView
|
||||
import com.vitorpamplona.amethyst.ui.screen.RefresheableFeedView
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
@@ -73,7 +73,7 @@ fun HashtagScreen(tag: String?, accountViewModel: AccountViewModel, nav: (String
|
||||
modifier = Modifier.padding(vertical = 0.dp)
|
||||
) {
|
||||
HashtagHeader(tag, accountViewModel)
|
||||
RefresheableView(feedViewModel, accountViewModel, nav, null)
|
||||
RefresheableFeedView(feedViewModel, null, accountViewModel, nav)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -34,7 +34,7 @@ import com.vitorpamplona.amethyst.ui.screen.FeedViewModel
|
||||
import com.vitorpamplona.amethyst.ui.screen.NostrHomeFeedViewModel
|
||||
import com.vitorpamplona.amethyst.ui.screen.NostrHomeRepliesFeedViewModel
|
||||
import com.vitorpamplona.amethyst.ui.screen.PagerStateKeys
|
||||
import com.vitorpamplona.amethyst.ui.screen.RefresheableView
|
||||
import com.vitorpamplona.amethyst.ui.screen.RefresheableFeedView
|
||||
import com.vitorpamplona.amethyst.ui.screen.ScrollStateKeys
|
||||
import com.vitorpamplona.amethyst.ui.screen.rememberForeverPagerState
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
@@ -121,7 +121,7 @@ private fun HomePages(
|
||||
}
|
||||
|
||||
HorizontalPager(pageCount = 2, state = pagerState) { page ->
|
||||
RefresheableView(
|
||||
RefresheableFeedView(
|
||||
viewModel = tabs[page].viewModel,
|
||||
accountViewModel = accountViewModel,
|
||||
nav = nav,
|
||||
|
||||
@@ -91,12 +91,13 @@ import com.vitorpamplona.amethyst.ui.screen.NostrUserProfileFollowsUserFeedViewM
|
||||
import com.vitorpamplona.amethyst.ui.screen.NostrUserProfileNewThreadsFeedViewModel
|
||||
import com.vitorpamplona.amethyst.ui.screen.NostrUserProfileReportFeedViewModel
|
||||
import com.vitorpamplona.amethyst.ui.screen.NostrUserProfileZapsFeedViewModel
|
||||
import com.vitorpamplona.amethyst.ui.screen.RefresheableView
|
||||
import com.vitorpamplona.amethyst.ui.screen.RefresheableFeedView
|
||||
import com.vitorpamplona.amethyst.ui.screen.RefreshingFeedUserFeedView
|
||||
import com.vitorpamplona.amethyst.ui.screen.RelayFeedView
|
||||
import com.vitorpamplona.amethyst.ui.screen.RelayFeedViewModel
|
||||
import com.vitorpamplona.amethyst.ui.screen.UserFeedViewModel
|
||||
import com.vitorpamplona.amethyst.ui.theme.BitcoinOrange
|
||||
import kotlinx.collections.immutable.persistentListOf
|
||||
import kotlinx.collections.immutable.toImmutableList
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
@@ -717,7 +718,7 @@ private fun DrawAdditionalInfo(
|
||||
TranslatableRichTextViewer(
|
||||
content = it,
|
||||
canPreview = false,
|
||||
tags = remember { emptyList() },
|
||||
tags = remember { persistentListOf() },
|
||||
backgroundColor = MaterialTheme.colors.background,
|
||||
accountViewModel = accountViewModel,
|
||||
nav = nav
|
||||
@@ -767,6 +768,7 @@ private fun DisplayLNAddress(
|
||||
userHex,
|
||||
account,
|
||||
onSuccess = {
|
||||
zapExpanded = false
|
||||
// pay directly
|
||||
if (account.hasWalletConnectSetup()) {
|
||||
account.sendZapPaymentRequestFor(it, null) { response ->
|
||||
@@ -1053,7 +1055,7 @@ fun TabNotesNewThreads(accountViewModel: AccountViewModel, nav: (String) -> Unit
|
||||
Column(
|
||||
modifier = Modifier.padding(vertical = 0.dp)
|
||||
) {
|
||||
RefresheableView(feedViewModel, accountViewModel, nav, null, enablePullRefresh = false)
|
||||
RefresheableFeedView(feedViewModel, null, accountViewModel, nav, enablePullRefresh = false)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1070,7 +1072,7 @@ fun TabNotesConversations(accountViewModel: AccountViewModel, nav: (String) -> U
|
||||
Column(
|
||||
modifier = Modifier.padding(vertical = 0.dp)
|
||||
) {
|
||||
RefresheableView(feedViewModel, accountViewModel, nav, null, enablePullRefresh = false)
|
||||
RefresheableFeedView(feedViewModel, null, accountViewModel, nav, enablePullRefresh = false)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1087,7 +1089,7 @@ fun TabBookmarks(baseUser: User, accountViewModel: AccountViewModel, nav: (Strin
|
||||
Column(
|
||||
modifier = Modifier.padding(vertical = 0.dp)
|
||||
) {
|
||||
RefresheableView(feedViewModel, accountViewModel, nav, null, enablePullRefresh = false)
|
||||
RefresheableFeedView(feedViewModel, null, accountViewModel, nav, enablePullRefresh = false)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1154,7 +1156,7 @@ fun TabReports(baseUser: User, accountViewModel: AccountViewModel, nav: (String)
|
||||
Column(
|
||||
modifier = Modifier.padding(vertical = 0.dp)
|
||||
) {
|
||||
RefresheableView(feedViewModel, accountViewModel, nav, null, enablePullRefresh = false)
|
||||
RefresheableFeedView(feedViewModel, null, accountViewModel, nav, enablePullRefresh = false)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1310,6 +1312,8 @@ fun UserProfileDropDownMenu(user: User, popupExpanded: Boolean, onDismiss: () ->
|
||||
val accountState by accountViewModel.accountLiveData.observeAsState()
|
||||
val account = accountState?.account ?: return
|
||||
|
||||
val scope = rememberCoroutineScope()
|
||||
|
||||
DropdownMenu(
|
||||
expanded = popupExpanded,
|
||||
onDismissRequest = onDismiss
|
||||
@@ -1322,51 +1326,65 @@ fun UserProfileDropDownMenu(user: User, popupExpanded: Boolean, onDismiss: () ->
|
||||
Divider()
|
||||
if (account.isHidden(user)) {
|
||||
DropdownMenuItem(onClick = {
|
||||
accountViewModel.show(user)
|
||||
onDismiss()
|
||||
scope.launch(Dispatchers.IO) {
|
||||
accountViewModel.show(user)
|
||||
onDismiss()
|
||||
}
|
||||
}) {
|
||||
Text(stringResource(R.string.unblock_user))
|
||||
}
|
||||
} else {
|
||||
DropdownMenuItem(onClick = {
|
||||
accountViewModel.hide(user)
|
||||
onDismiss()
|
||||
scope.launch(Dispatchers.IO) {
|
||||
accountViewModel.hide(user)
|
||||
onDismiss()
|
||||
}
|
||||
}) {
|
||||
Text(stringResource(id = R.string.block_hide_user))
|
||||
}
|
||||
}
|
||||
Divider()
|
||||
DropdownMenuItem(onClick = {
|
||||
accountViewModel.report(user, ReportEvent.ReportType.SPAM)
|
||||
accountViewModel.hide(user)
|
||||
scope.launch(Dispatchers.IO) {
|
||||
accountViewModel.report(user, ReportEvent.ReportType.SPAM)
|
||||
accountViewModel.hide(user)
|
||||
}
|
||||
onDismiss()
|
||||
}) {
|
||||
Text(stringResource(id = R.string.report_spam_scam))
|
||||
}
|
||||
DropdownMenuItem(onClick = {
|
||||
accountViewModel.report(user, ReportEvent.ReportType.PROFANITY)
|
||||
accountViewModel.hide(user)
|
||||
scope.launch(Dispatchers.IO) {
|
||||
accountViewModel.report(user, ReportEvent.ReportType.PROFANITY)
|
||||
accountViewModel.hide(user)
|
||||
}
|
||||
onDismiss()
|
||||
}) {
|
||||
Text(stringResource(R.string.report_hateful_speech))
|
||||
}
|
||||
DropdownMenuItem(onClick = {
|
||||
accountViewModel.report(user, ReportEvent.ReportType.IMPERSONATION)
|
||||
accountViewModel.hide(user)
|
||||
scope.launch(Dispatchers.IO) {
|
||||
accountViewModel.report(user, ReportEvent.ReportType.IMPERSONATION)
|
||||
accountViewModel.hide(user)
|
||||
}
|
||||
onDismiss()
|
||||
}) {
|
||||
Text(stringResource(id = R.string.report_impersonation))
|
||||
}
|
||||
DropdownMenuItem(onClick = {
|
||||
accountViewModel.report(user, ReportEvent.ReportType.NUDITY)
|
||||
accountViewModel.hide(user)
|
||||
scope.launch(Dispatchers.IO) {
|
||||
accountViewModel.report(user, ReportEvent.ReportType.NUDITY)
|
||||
accountViewModel.hide(user)
|
||||
}
|
||||
onDismiss()
|
||||
}) {
|
||||
Text(stringResource(R.string.report_nudity_porn))
|
||||
}
|
||||
DropdownMenuItem(onClick = {
|
||||
accountViewModel.report(user, ReportEvent.ReportType.ILLEGAL)
|
||||
accountViewModel.hide(user)
|
||||
scope.launch(Dispatchers.IO) {
|
||||
accountViewModel.report(user, ReportEvent.ReportType.ILLEGAL)
|
||||
accountViewModel.hide(user)
|
||||
}
|
||||
onDismiss()
|
||||
}) {
|
||||
Text(stringResource(id = R.string.report_illegal_behaviour))
|
||||
|
||||
+19
-5
@@ -26,6 +26,7 @@ import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.rememberCoroutineScope
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
@@ -41,6 +42,8 @@ import com.vitorpamplona.amethyst.R
|
||||
import com.vitorpamplona.amethyst.model.Note
|
||||
import com.vitorpamplona.amethyst.service.model.ReportEvent
|
||||
import com.vitorpamplona.amethyst.ui.theme.WarningColor
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
@Composable
|
||||
fun ReportNoteDialog(note: Note, accountViewModel: AccountViewModel, onDismiss: () -> Unit) {
|
||||
@@ -78,6 +81,8 @@ fun ReportNoteDialog(note: Note, accountViewModel: AccountViewModel, onDismiss:
|
||||
)
|
||||
}
|
||||
) { pad ->
|
||||
val scope = rememberCoroutineScope()
|
||||
|
||||
Column(
|
||||
modifier = Modifier.padding(16.dp, pad.calculateTopPadding(), 16.dp, pad.calculateBottomPadding()),
|
||||
verticalArrangement = Arrangement.SpaceAround
|
||||
@@ -93,8 +98,10 @@ fun ReportNoteDialog(note: Note, accountViewModel: AccountViewModel, onDismiss:
|
||||
text = stringResource(R.string.report_dialog_block_hide_user_btn),
|
||||
icon = Icons.Default.Block,
|
||||
onClick = {
|
||||
note.author?.let { accountViewModel.hide(it) }
|
||||
onDismiss()
|
||||
scope.launch(Dispatchers.IO) {
|
||||
note.author?.let { accountViewModel.hide(it) }
|
||||
onDismiss()
|
||||
}
|
||||
}
|
||||
)
|
||||
SpacerH16()
|
||||
@@ -124,14 +131,21 @@ fun ReportNoteDialog(note: Note, accountViewModel: AccountViewModel, onDismiss:
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
)
|
||||
SpacerH16()
|
||||
|
||||
ActionButton(
|
||||
text = stringResource(R.string.report_dialog_post_report_btn),
|
||||
icon = Icons.Default.Report,
|
||||
enabled = selectedReason in 0..reportTypes.lastIndex,
|
||||
onClick = {
|
||||
accountViewModel.report(note, reportTypes[selectedReason].first, additionalReason)
|
||||
note.author?.let { accountViewModel.hide(it) }
|
||||
onDismiss()
|
||||
scope.launch(Dispatchers.IO) {
|
||||
accountViewModel.report(
|
||||
note,
|
||||
reportTypes[selectedReason].first,
|
||||
additionalReason
|
||||
)
|
||||
note.author?.let { accountViewModel.hide(it) }
|
||||
onDismiss()
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
@@ -62,7 +62,7 @@ import com.vitorpamplona.amethyst.ui.note.UserCompose
|
||||
import com.vitorpamplona.amethyst.ui.note.UserPicture
|
||||
import com.vitorpamplona.amethyst.ui.note.UsernameDisplay
|
||||
import com.vitorpamplona.amethyst.ui.screen.NostrGlobalFeedViewModel
|
||||
import com.vitorpamplona.amethyst.ui.screen.RefresheableView
|
||||
import com.vitorpamplona.amethyst.ui.screen.RefresheableFeedView
|
||||
import com.vitorpamplona.amethyst.ui.screen.ScrollStateKeys
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.FlowPreview
|
||||
@@ -113,7 +113,7 @@ fun SearchScreen(
|
||||
modifier = Modifier.padding(vertical = 0.dp)
|
||||
) {
|
||||
SearchBar(accountViewModel, nav)
|
||||
RefresheableView(searchFeedViewModel, accountViewModel, nav, null, ScrollStateKeys.GLOBAL_SCREEN)
|
||||
RefresheableFeedView(searchFeedViewModel, null, accountViewModel, nav, ScrollStateKeys.GLOBAL_SCREEN)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -253,11 +253,9 @@ private fun RenderVideoOrPictureNote(
|
||||
accountViewModel: AccountViewModel,
|
||||
nav: (String) -> Unit
|
||||
) {
|
||||
val noteEvent = note.event
|
||||
var moreActionsExpanded by remember { mutableStateOf(false) }
|
||||
|
||||
Column(Modifier.fillMaxSize(1f)) {
|
||||
Row(Modifier.weight(1f), verticalAlignment = Alignment.CenterVertically) {
|
||||
Column(remember { Modifier.fillMaxSize(1f) }) {
|
||||
Row(remember { Modifier.weight(1f) }, verticalAlignment = Alignment.CenterVertically) {
|
||||
val noteEvent = remember { note.event }
|
||||
if (noteEvent is FileHeaderEvent) {
|
||||
FileHeaderDisplay(note)
|
||||
} else if (noteEvent is FileStorageHeaderEvent) {
|
||||
@@ -266,51 +264,17 @@ private fun RenderVideoOrPictureNote(
|
||||
}
|
||||
}
|
||||
|
||||
Row(verticalAlignment = Alignment.Bottom, modifier = Modifier.fillMaxSize(1f)) {
|
||||
Column(Modifier.weight(1f)) {
|
||||
Row(Modifier.padding(10.dp), verticalAlignment = Alignment.Bottom) {
|
||||
Column(Modifier.size(55.dp), verticalArrangement = Arrangement.Center) {
|
||||
NoteAuthorPicture(note, nav, accountViewModel, 55.dp)
|
||||
}
|
||||
|
||||
Column(
|
||||
Modifier
|
||||
.padding(start = 10.dp, end = 10.dp)
|
||||
.height(60.dp)
|
||||
.weight(1f),
|
||||
verticalArrangement = Arrangement.Center
|
||||
) {
|
||||
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||
NoteUsernameDisplay(note, Modifier.weight(1f))
|
||||
|
||||
IconButton(
|
||||
modifier = Modifier.size(24.dp),
|
||||
onClick = { moreActionsExpanded = true }
|
||||
) {
|
||||
Icon(
|
||||
imageVector = Icons.Default.MoreVert,
|
||||
null,
|
||||
modifier = Modifier.size(20.dp),
|
||||
tint = MaterialTheme.colors.onSurface.copy(alpha = 0.32f)
|
||||
)
|
||||
|
||||
NoteDropDownMenu(note, moreActionsExpanded, { moreActionsExpanded = false }, accountViewModel)
|
||||
}
|
||||
}
|
||||
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||
ObserveDisplayNip05Status(note.author!!, Modifier.weight(1f))
|
||||
}
|
||||
Row(verticalAlignment = Alignment.CenterVertically, modifier = Modifier.padding(top = 5.dp)) {
|
||||
RelayBadges(baseNote = note)
|
||||
}
|
||||
}
|
||||
}
|
||||
Row(verticalAlignment = Alignment.Bottom, modifier = remember { Modifier.fillMaxSize(1f) }) {
|
||||
Column(remember { Modifier.weight(1f) }) {
|
||||
RenderVideoOrPicture(note, nav, accountViewModel)
|
||||
}
|
||||
|
||||
Column(
|
||||
Modifier
|
||||
.width(65.dp)
|
||||
.padding(bottom = 10.dp),
|
||||
remember {
|
||||
Modifier
|
||||
.width(65.dp)
|
||||
.padding(bottom = 10.dp)
|
||||
},
|
||||
verticalArrangement = Arrangement.Center
|
||||
) {
|
||||
Row(horizontalArrangement = Arrangement.Center) {
|
||||
@@ -320,6 +284,73 @@ private fun RenderVideoOrPictureNote(
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun RenderVideoOrPicture(
|
||||
note: Note,
|
||||
nav: (String) -> Unit,
|
||||
accountViewModel: AccountViewModel
|
||||
) {
|
||||
Row(remember { Modifier.padding(10.dp) }, verticalAlignment = Alignment.Bottom) {
|
||||
Column(remember { Modifier.size(55.dp) }, verticalArrangement = Arrangement.Center) {
|
||||
NoteAuthorPicture(note, nav, accountViewModel, 55.dp)
|
||||
}
|
||||
|
||||
Column(
|
||||
remember {
|
||||
Modifier
|
||||
.padding(start = 10.dp, end = 10.dp)
|
||||
.height(60.dp)
|
||||
.weight(1f)
|
||||
},
|
||||
verticalArrangement = Arrangement.Center
|
||||
) {
|
||||
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||
NoteUsernameDisplay(note, remember { Modifier.weight(1f) })
|
||||
VideoUserOptionAction(note, accountViewModel)
|
||||
}
|
||||
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||
ObserveDisplayNip05Status(
|
||||
remember { note.author!! },
|
||||
remember { Modifier.weight(1f) }
|
||||
)
|
||||
}
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
modifier = Modifier.padding(top = 5.dp)
|
||||
) {
|
||||
RelayBadges(baseNote = note)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun VideoUserOptionAction(
|
||||
note: Note,
|
||||
accountViewModel: AccountViewModel
|
||||
) {
|
||||
var moreActionsExpanded by remember { mutableStateOf(false) }
|
||||
|
||||
IconButton(
|
||||
modifier = remember { Modifier.size(24.dp) },
|
||||
onClick = { moreActionsExpanded = true }
|
||||
) {
|
||||
Icon(
|
||||
imageVector = Icons.Default.MoreVert,
|
||||
null,
|
||||
modifier = remember { Modifier.size(20.dp) },
|
||||
tint = MaterialTheme.colors.onSurface.copy(alpha = 0.32f)
|
||||
)
|
||||
|
||||
NoteDropDownMenu(
|
||||
note,
|
||||
moreActionsExpanded,
|
||||
{ moreActionsExpanded = false },
|
||||
accountViewModel
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalLayoutApi::class)
|
||||
@Composable
|
||||
private fun RelayBadges(baseNote: Note) {
|
||||
|
||||
+21
-10
@@ -34,6 +34,7 @@ import com.vitorpamplona.amethyst.R
|
||||
import com.vitorpamplona.amethyst.service.lang.LanguageTranslatorService
|
||||
import com.vitorpamplona.amethyst.service.lang.ResultOrError
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import java.util.Locale
|
||||
@@ -43,7 +44,7 @@ fun TranslatableRichTextViewer(
|
||||
content: String,
|
||||
canPreview: Boolean,
|
||||
modifier: Modifier = Modifier,
|
||||
tags: List<List<String>>,
|
||||
tags: ImmutableList<List<String>>,
|
||||
backgroundColor: Color,
|
||||
accountViewModel: AccountViewModel,
|
||||
nav: (String) -> Unit
|
||||
@@ -74,7 +75,7 @@ fun TranslatableRichTextViewer(
|
||||
}
|
||||
}
|
||||
|
||||
Column() {
|
||||
Column {
|
||||
ExpandableRichTextViewer(
|
||||
toBeViewed,
|
||||
canPreview,
|
||||
@@ -89,6 +90,8 @@ fun TranslatableRichTextViewer(
|
||||
val source = translatedTextState.sourceLang
|
||||
|
||||
if (source != null && target != null && source != target) {
|
||||
val scope = rememberCoroutineScope()
|
||||
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
@@ -140,8 +143,10 @@ fun TranslatableRichTextViewer(
|
||||
onDismissRequest = { langSettingsPopupExpanded = false }
|
||||
) {
|
||||
DropdownMenuItem(onClick = {
|
||||
accountViewModel.dontTranslateFrom(source)
|
||||
langSettingsPopupExpanded = false
|
||||
scope.launch(Dispatchers.IO) {
|
||||
accountViewModel.dontTranslateFrom(source)
|
||||
langSettingsPopupExpanded = false
|
||||
}
|
||||
}) {
|
||||
if (source in accountViewModel.account.dontTranslateFrom) {
|
||||
Icon(
|
||||
@@ -159,8 +164,10 @@ fun TranslatableRichTextViewer(
|
||||
}
|
||||
Divider()
|
||||
DropdownMenuItem(onClick = {
|
||||
accountViewModel.prefer(source, target, source)
|
||||
langSettingsPopupExpanded = false
|
||||
scope.launch(Dispatchers.IO) {
|
||||
accountViewModel.prefer(source, target, source)
|
||||
langSettingsPopupExpanded = false
|
||||
}
|
||||
}) {
|
||||
if (accountViewModel.account.preferenceBetween(source, target) == source) {
|
||||
Icon(
|
||||
@@ -177,8 +184,10 @@ fun TranslatableRichTextViewer(
|
||||
Text(stringResource(R.string.translations_show_in_lang_first, Locale(source).displayName))
|
||||
}
|
||||
DropdownMenuItem(onClick = {
|
||||
accountViewModel.prefer(source, target, target)
|
||||
langSettingsPopupExpanded = false
|
||||
scope.launch(Dispatchers.IO) {
|
||||
accountViewModel.prefer(source, target, target)
|
||||
langSettingsPopupExpanded = false
|
||||
}
|
||||
}) {
|
||||
if (accountViewModel.account.preferenceBetween(source, target) == target) {
|
||||
Icon(
|
||||
@@ -201,8 +210,10 @@ fun TranslatableRichTextViewer(
|
||||
for (i in 0 until languageList.size()) {
|
||||
languageList.get(i)?.let { lang ->
|
||||
DropdownMenuItem(onClick = {
|
||||
accountViewModel.translateTo(lang)
|
||||
langSettingsPopupExpanded = false
|
||||
scope.launch(Dispatchers.IO) {
|
||||
accountViewModel.translateTo(lang)
|
||||
langSettingsPopupExpanded = false
|
||||
}
|
||||
}) {
|
||||
if (lang.language in accountViewModel.account.translateTo) {
|
||||
Icon(
|
||||
|
||||
Reference in New Issue
Block a user