amethyst/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/AccountViewModel.kt

760 wiersze
25 KiB
Kotlin

package com.vitorpamplona.amethyst.ui.screen.loggedIn
import android.content.Context
import androidx.compose.runtime.Immutable
import androidx.compose.runtime.Stable
import androidx.lifecycle.LiveData
import androidx.lifecycle.ViewModel
import androidx.lifecycle.ViewModelProvider
import androidx.lifecycle.distinctUntilChanged
import androidx.lifecycle.map
import androidx.lifecycle.viewModelScope
import com.vitorpamplona.amethyst.model.Account
import com.vitorpamplona.amethyst.model.AccountState
import com.vitorpamplona.amethyst.model.AddressableNote
import com.vitorpamplona.amethyst.model.BooleanType
import com.vitorpamplona.amethyst.model.Channel
import com.vitorpamplona.amethyst.model.ConnectivityType
import com.vitorpamplona.amethyst.model.LocalCache
import com.vitorpamplona.amethyst.model.Note
import com.vitorpamplona.amethyst.model.RelayInformation
import com.vitorpamplona.amethyst.model.UrlCachedPreviewer
import com.vitorpamplona.amethyst.model.User
import com.vitorpamplona.amethyst.model.UserState
import com.vitorpamplona.amethyst.service.Nip05Verifier
import com.vitorpamplona.amethyst.service.Nip11CachedRetriever
import com.vitorpamplona.amethyst.service.Nip11Retriever
import com.vitorpamplona.amethyst.service.OnlineChecker
import com.vitorpamplona.amethyst.service.ZapPaymentHandler
import com.vitorpamplona.amethyst.service.checkNotInMainThread
import com.vitorpamplona.amethyst.ui.actions.Dao
import com.vitorpamplona.amethyst.ui.components.MarkdownParser
import com.vitorpamplona.amethyst.ui.components.UrlPreviewState
import com.vitorpamplona.amethyst.ui.note.ZapAmountCommentNotification
import com.vitorpamplona.amethyst.ui.note.ZapraiserStatus
import com.vitorpamplona.amethyst.ui.note.showAmount
import com.vitorpamplona.amethyst.ui.screen.CombinedZap
import com.vitorpamplona.quartz.encoders.ATag
import com.vitorpamplona.quartz.encoders.HexKey
import com.vitorpamplona.quartz.encoders.Nip19
import com.vitorpamplona.quartz.events.ChatroomKey
import com.vitorpamplona.quartz.events.Event
import com.vitorpamplona.quartz.events.GiftWrapEvent
import com.vitorpamplona.quartz.events.ImmutableListOfLists
import com.vitorpamplona.quartz.events.LnZapEvent
import com.vitorpamplona.quartz.events.LnZapRequestEvent
import com.vitorpamplona.quartz.events.Participant
import com.vitorpamplona.quartz.events.ReportEvent
import com.vitorpamplona.quartz.events.SealedGossipEvent
import com.vitorpamplona.quartz.events.UserMetadata
import com.vitorpamplona.quartz.utils.TimeUtils
import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.ImmutableSet
import kotlinx.collections.immutable.persistentSetOf
import kotlinx.collections.immutable.toImmutableList
import kotlinx.collections.immutable.toImmutableSet
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import java.math.BigDecimal
import java.util.Locale
@Stable
class AccountViewModel(val account: Account) : ViewModel(), Dao {
val accountLiveData: LiveData<AccountState> = account.live.map { it }
val accountLanguagesLiveData: LiveData<AccountState> = account.liveLanguages.map { it }
val accountLastReadLiveData: LiveData<AccountState> = account.liveLastRead.map { it }
val userFollows: LiveData<UserState> = account.userProfile().live().follows.map { it }
val userRelays: LiveData<UserState> = account.userProfile().live().relays.map { it }
val discoveryListLiveData = account.live.map {
it.account.defaultDiscoveryFollowList
}.distinctUntilChanged()
val homeListLiveData = account.live.map {
it.account.defaultHomeFollowList
}.distinctUntilChanged()
val notificationListLiveData = account.live.map {
it.account.defaultNotificationFollowList
}.distinctUntilChanged()
val storiesListLiveData = account.live.map {
it.account.defaultStoriesFollowList
}.distinctUntilChanged()
val showSensitiveContentChanges = account.live.map {
it.account.showSensitiveContent
}.distinctUntilChanged()
fun updateAutomaticallyStartPlayback(
automaticallyStartPlayback: ConnectivityType
) {
account.updateAutomaticallyStartPlayback(automaticallyStartPlayback)
}
fun updateAutomaticallyShowUrlPreview(
automaticallyShowUrlPreview: ConnectivityType
) {
account.updateAutomaticallyShowUrlPreview(automaticallyShowUrlPreview)
}
fun updateAutomaticallyShowProfilePicture(
automaticallyShowProfilePicture: ConnectivityType
) {
account.updateAutomaticallyShowProfilePicture(automaticallyShowProfilePicture)
}
fun updateAutomaticallyHideNavBars(
automaticallyHideHavBars: BooleanType
) {
account.updateAutomaticallyHideHavBars(automaticallyHideHavBars)
}
fun updateAutomaticallyShowImages(
automaticallyShowImages: ConnectivityType
) {
account.updateAutomaticallyShowImages(automaticallyShowImages)
}
fun isWriteable(): Boolean {
return account.isWriteable()
}
fun loggedInWithExternalSigner(): Boolean {
return account.loginWithExternalSigner
}
fun userProfile(): User {
return account.userProfile()
}
fun reactTo(note: Note, reaction: String) {
account.reactTo(note, reaction)
}
fun reactToOrDelete(note: Note, reaction: String) {
viewModelScope.launch(Dispatchers.IO) {
val currentReactions = account.reactionTo(note, reaction)
if (currentReactions.isNotEmpty()) {
account.delete(currentReactions)
} else {
account.reactTo(note, reaction)
}
}
}
fun isNoteHidden(note: Note): Boolean {
val isSensitive = note.event?.isSensitive() ?: false
return account.isHidden(note.author!!) || (isSensitive && account.showSensitiveContent == false)
}
fun hasReactedTo(baseNote: Note, reaction: String): Boolean {
return account.hasReacted(baseNote, reaction)
}
fun deleteReactionTo(note: Note, reaction: String) {
account.delete(account.reactionTo(note, reaction))
}
fun hasBoosted(baseNote: Note): Boolean {
return account.hasBoosted(baseNote)
}
fun deleteBoostsTo(note: Note) {
account.delete(account.boostsTo(note))
}
fun calculateIfNoteWasZappedByAccount(zappedNote: Note, onWasZapped: (Boolean) -> Unit) {
viewModelScope.launch(Dispatchers.Default) {
onWasZapped(account.calculateIfNoteWasZappedByAccount(zappedNote))
}
}
suspend fun calculateZapAmount(zappedNote: Note): BigDecimal {
return account.calculateZappedAmount(zappedNote)
}
fun calculateZapAmount(zappedNote: Note, onZapAmount: (String) -> Unit) {
viewModelScope.launch(Dispatchers.IO) {
onZapAmount(showAmount(account.calculateZappedAmount(zappedNote)))
}
}
fun calculateZapraiser(zappedNote: Note, onZapraiserStatus: (ZapraiserStatus) -> Unit) {
viewModelScope.launch(Dispatchers.IO) {
val zapraiserAmount = zappedNote.event?.zapraiserAmount() ?: 0
val newZapAmount = calculateZapAmount(zappedNote)
var percentage = newZapAmount.div(zapraiserAmount.toBigDecimal()).toFloat()
if (percentage > 1) {
percentage = 1f
}
val newZapraiserProgress = percentage
val newZapraiserLeft = if (percentage > 0.99) {
"0"
} else {
showAmount((zapraiserAmount * (1 - percentage)).toBigDecimal())
}
onZapraiserStatus(ZapraiserStatus(newZapraiserProgress, newZapraiserLeft))
}
}
fun decryptAmountMessageInGroup(
zaps: ImmutableList<CombinedZap>,
onNewState: (ImmutableList<ZapAmountCommentNotification>) -> Unit
) {
viewModelScope.launch(Dispatchers.IO) {
val list = ArrayList<ZapAmountCommentNotification>(zaps.size)
zaps.forEach {
innerDecryptAmountMessage(it.request, it.response)?.let {
list.add(it)
}
}
onNewState(list.toImmutableList())
}
}
fun decryptAmountMessageInGroup(
baseNote: Note,
onNewState: (ImmutableList<ZapAmountCommentNotification>) -> Unit
) {
viewModelScope.launch(Dispatchers.IO) {
val list = ArrayList<ZapAmountCommentNotification>(baseNote.zaps.size)
baseNote.zaps.forEach {
innerDecryptAmountMessage(it.key, it.value)?.let {
list.add(it)
}
}
onNewState(list.toImmutableList())
}
}
fun decryptAmountMessage(
zapRequest: Note,
zapEvent: Note?,
onNewState: (ZapAmountCommentNotification?) -> Unit
) {
viewModelScope.launch(Dispatchers.IO) {
onNewState(innerDecryptAmountMessage(zapRequest, zapEvent))
}
}
private suspend fun innerDecryptAmountMessage(
zapRequest: Note,
zapEvent: Note?
): ZapAmountCommentNotification? {
checkNotInMainThread()
(zapRequest.event as? LnZapRequestEvent)?.let {
val decryptedContent = decryptZap(zapRequest)
val amount = (zapEvent?.event as? LnZapEvent)?.amount
if (decryptedContent != null) {
val newAuthor = LocalCache.getOrCreateUser(decryptedContent.pubKey)
return ZapAmountCommentNotification(
newAuthor,
decryptedContent.content.ifBlank { null },
showAmountAxis(amount)
)
} else {
if (!zapRequest.event?.content().isNullOrBlank() || amount != null) {
return ZapAmountCommentNotification(
zapRequest.author,
zapRequest.event?.content()?.ifBlank { null },
showAmountAxis(amount)
)
}
}
}
return null
}
fun zap(
note: Note,
amount: Long,
pollOption: Int?,
message: String,
context: Context,
onError: (String) -> Unit,
onProgress: (percent: Float) -> Unit,
onPayViaIntent: (ImmutableList<ZapPaymentHandler.Payable>) -> Unit,
zapType: LnZapEvent.ZapType
) {
viewModelScope.launch(Dispatchers.IO) {
ZapPaymentHandler(account).zap(note, amount, pollOption, message, context, onError, onProgress, onPayViaIntent, zapType)
}
}
fun report(note: Note, type: ReportEvent.ReportType, content: String = "") {
account.report(note, type, content)
}
fun report(user: User, type: ReportEvent.ReportType) {
viewModelScope.launch(Dispatchers.IO) {
account.report(user, type)
account.hideUser(user.pubkeyHex)
}
}
fun boost(note: Note) {
account.boost(note)
}
fun removeEmojiPack(usersEmojiList: Note, emojiList: Note) {
account.removeEmojiPack(usersEmojiList, emojiList)
}
fun addEmojiPack(usersEmojiList: Note, emojiList: Note) {
account.addEmojiPack(usersEmojiList, emojiList)
}
fun addPrivateBookmark(note: Note) {
account.addPrivateBookmark(note)
}
fun addPrivateBookmark(note: Note, decryptedContent: String) {
account.addPrivateBookmark(note, decryptedContent)
}
fun addPublicBookmark(note: Note, decryptedContent: String) {
account.addPublicBookmark(note, decryptedContent)
}
fun removePublicBookmark(note: Note, decryptedContent: String) {
account.removePublicBookmark(note, decryptedContent)
}
fun addPublicBookmark(note: Note) {
account.addPublicBookmark(note)
}
fun removePrivateBookmark(note: Note, decryptedContent: String) {
account.removePrivateBookmark(note, decryptedContent)
}
fun removePrivateBookmark(note: Note) {
account.removePrivateBookmark(note)
}
fun removePublicBookmark(note: Note) {
account.removePublicBookmark(note)
}
fun isInPrivateBookmarks(note: Note): Boolean {
return account.isInPrivateBookmarks(note)
}
fun isInPublicBookmarks(note: Note): Boolean {
return account.isInPublicBookmarks(note)
}
fun broadcast(note: Note) {
account.broadcast(note)
}
fun delete(note: Note) {
account.delete(note)
}
fun decrypt(note: Note): String? {
return account.decryptContent(note)
}
fun decryptZap(note: Note): Event? {
return account.decryptZapContentAuthor(note)
}
fun translateTo(lang: Locale) {
account.updateTranslateTo(lang.language)
}
fun dontTranslateFrom(lang: String) {
account.addDontTranslateFrom(lang)
}
fun prefer(source: String, target: String, preference: String) {
account.prefer(source, target, preference)
}
fun follow(user: User) {
account.follow(user)
}
fun unfollow(user: User) {
account.unfollow(user)
}
fun isLoggedUser(user: User?): Boolean {
return account.userProfile().pubkeyHex == user?.pubkeyHex
}
fun isFollowing(user: User?): Boolean {
if (user == null) return false
return account.userProfile().isFollowingCached(user)
}
fun isFollowing(user: HexKey): Boolean {
return account.userProfile().isFollowingCached(user)
}
val hideDeleteRequestDialog: Boolean
get() = account.hideDeleteRequestDialog
fun dontShowDeleteRequestDialog() {
account.setHideDeleteRequestDialog()
}
val hideNIP24WarningDialog: Boolean
get() = account.hideNIP24WarningDialog
fun dontShowNIP24WarningDialog() {
account.setHideNIP24WarningDialog()
}
val hideBlockAlertDialog: Boolean
get() = account.hideBlockAlertDialog
fun dontShowBlockAlertDialog() {
account.setHideBlockAlertDialog()
}
fun hideSensitiveContent() {
account.updateShowSensitiveContent(false)
}
fun disableContentWarnings() {
account.updateShowSensitiveContent(true)
}
fun seeContentWarnings() {
account.updateShowSensitiveContent(null)
}
fun defaultZapType(): LnZapEvent.ZapType {
return account.defaultZapType
}
@Immutable
data class NoteComposeReportState(
val isAcceptable: Boolean = true,
val canPreview: Boolean = true,
val isHiddenAuthor: Boolean = false,
val relevantReports: ImmutableSet<Note> = persistentSetOf()
)
fun isNoteAcceptable(note: Note, onReady: (NoteComposeReportState) -> Unit) {
viewModelScope.launch(Dispatchers.IO) {
val isFromLoggedIn = note.author?.pubkeyHex == userProfile().pubkeyHex
val isFromLoggedInFollow = note.author?.let { userProfile().isFollowingCached(it) } ?: true
if (isFromLoggedIn || isFromLoggedInFollow) {
// No need to process if from trusted people
onReady(NoteComposeReportState(true, true, false, persistentSetOf()))
} else if (note.author?.let { account.isHidden(it) } == true) {
onReady(NoteComposeReportState(false, false, true, persistentSetOf()))
} else {
val newCanPreview = !note.hasAnyReports()
val newIsAcceptable = account.isAcceptable(note)
if (newCanPreview && newIsAcceptable) {
// No need to process reports if nothing is wrong
onReady(NoteComposeReportState(true, true, false, persistentSetOf()))
} else {
val newRelevantReports = account.getRelevantReports(note)
onReady(
NoteComposeReportState(
newIsAcceptable,
newCanPreview,
false,
newRelevantReports.toImmutableSet()
)
)
}
}
}
}
fun unwrap(event: GiftWrapEvent): Event? {
return account.unwrap(event)
}
fun unseal(event: SealedGossipEvent): Event? {
return account.unseal(event)
}
fun show(user: User) {
viewModelScope.launch(Dispatchers.IO) {
account.showUser(user.pubkeyHex)
}
}
fun hide(user: User) {
viewModelScope.launch(Dispatchers.IO) {
account.hideUser(user.pubkeyHex)
}
}
fun hide(word: String) {
viewModelScope.launch(Dispatchers.IO) {
account.hideWord(word)
}
}
fun showUser(pubkeyHex: String) {
viewModelScope.launch(Dispatchers.IO) {
account.showUser(pubkeyHex)
}
}
fun createStatus(newStatus: String) {
viewModelScope.launch(Dispatchers.IO) {
account.createStatus(newStatus)
}
}
fun updateStatus(it: AddressableNote, newStatus: String) {
viewModelScope.launch(Dispatchers.IO) {
account.updateStatus(it, newStatus)
}
}
fun deleteStatus(it: AddressableNote) {
viewModelScope.launch(Dispatchers.IO) {
account.deleteStatus(it)
}
}
fun checkIfOnline(url: String, onResult: (Boolean) -> Unit) {
viewModelScope.launch(Dispatchers.IO) {
val isOnline = OnlineChecker.isOnline(url)
onResult(isOnline)
}
}
fun urlPreview(url: String, onResult: suspend (UrlPreviewState) -> Unit) {
viewModelScope.launch(Dispatchers.IO) {
UrlCachedPreviewer.previewInfo(url, onResult)
}
}
fun loadReactionTo(note: Note?, onNewReactionType: (String?) -> Unit) {
if (note == null) return
viewModelScope.launch(Dispatchers.Default) {
onNewReactionType(note.getReactionBy(userProfile()))
}
}
fun verifyNip05(userMetadata: UserMetadata, pubkeyHex: String, onResult: (Boolean) -> Unit) {
val nip05 = userMetadata.nip05?.ifBlank { null } ?: return
viewModelScope.launch(Dispatchers.IO) {
Nip05Verifier().verifyNip05(
nip05,
onSuccess = {
// Marks user as verified
if (it == pubkeyHex) {
userMetadata.nip05Verified = true
userMetadata.nip05LastVerificationTime = TimeUtils.now()
onResult(userMetadata.nip05Verified)
} else {
userMetadata.nip05Verified = false
userMetadata.nip05LastVerificationTime = 0
onResult(userMetadata.nip05Verified)
}
},
onError = {
userMetadata.nip05LastVerificationTime = 0
userMetadata.nip05Verified = false
onResult(userMetadata.nip05Verified)
}
)
}
}
fun retrieveRelayDocument(
dirtyUrl: String,
onInfo: (RelayInformation) -> Unit,
onError: (String, Nip11Retriever.ErrorCode, String?) -> Unit
) {
viewModelScope.launch(Dispatchers.IO) {
Nip11CachedRetriever.loadRelayInfo(dirtyUrl, onInfo, onError)
}
}
fun runOnIO(runOnIO: () -> Unit) {
viewModelScope.launch(Dispatchers.IO) {
runOnIO()
}
}
suspend fun checkGetOrCreateUser(key: HexKey): User? {
return LocalCache.checkGetOrCreateUser(key)
}
override suspend fun getOrCreateUser(key: HexKey): User {
return LocalCache.getOrCreateUser(key)
}
fun checkGetOrCreateUser(key: HexKey, onResult: (User?) -> Unit) {
viewModelScope.launch(Dispatchers.IO) {
onResult(checkGetOrCreateUser(key))
}
}
fun getUserIfExists(hex: HexKey): User? {
return LocalCache.getUserIfExists(hex)
}
private suspend fun checkGetOrCreateNote(key: HexKey): Note? {
return LocalCache.checkGetOrCreateNote(key)
}
override suspend fun getOrCreateNote(key: HexKey): Note {
return LocalCache.getOrCreateNote(key)
}
fun checkGetOrCreateNote(key: HexKey, onResult: (Note?) -> Unit) {
viewModelScope.launch(Dispatchers.IO) {
onResult(checkGetOrCreateNote(key))
}
}
fun getNoteIfExists(hex: HexKey): Note? {
return LocalCache.getNoteIfExists(hex)
}
override suspend fun checkGetOrCreateAddressableNote(key: HexKey): AddressableNote? {
return LocalCache.checkGetOrCreateAddressableNote(key)
}
fun checkGetOrCreateAddressableNote(key: HexKey, onResult: (AddressableNote?) -> Unit) {
viewModelScope.launch(Dispatchers.IO) {
onResult(checkGetOrCreateAddressableNote(key))
}
}
private suspend fun getOrCreateAddressableNote(key: ATag): AddressableNote? {
return LocalCache.getOrCreateAddressableNote(key)
}
fun getOrCreateAddressableNote(key: ATag, onResult: (AddressableNote?) -> Unit) {
viewModelScope.launch(Dispatchers.IO) {
onResult(getOrCreateAddressableNote(key))
}
}
fun getAddressableNoteIfExists(key: String): AddressableNote? {
return LocalCache.addressables[key]
}
fun findStatusesForUser(myUser: User, onResult: (ImmutableList<AddressableNote>) -> Unit) {
viewModelScope.launch(Dispatchers.IO) {
onResult(LocalCache.findStatusesForUser(myUser))
}
}
private suspend fun checkGetOrCreateChannel(key: HexKey): Channel? {
return LocalCache.checkGetOrCreateChannel(key)
}
fun checkGetOrCreateChannel(key: HexKey, onResult: (Channel?) -> Unit) {
viewModelScope.launch(Dispatchers.IO) {
onResult(checkGetOrCreateChannel(key))
}
}
fun getChannelIfExists(hex: HexKey): Channel? {
return LocalCache.getChannelIfExists(hex)
}
fun loadParticipants(participants: List<Participant>, onReady: (ImmutableList<Pair<Participant, User>>) -> Unit) {
viewModelScope.launch(Dispatchers.IO) {
val participantUsers = participants.mapNotNull { part ->
checkGetOrCreateUser(part.key)?.let {
Pair(
part,
it
)
}
}.toImmutableList()
onReady(participantUsers)
}
}
fun loadUsers(hexList: List<String>, onReady: (ImmutableList<User>) -> Unit) {
viewModelScope.launch(Dispatchers.IO) {
onReady(
hexList.mapNotNull { hex ->
checkGetOrCreateUser(hex)
}.sortedBy { account.isFollowing(it) }.reversed().toImmutableList()
)
}
}
fun returnNIP19References(content: String, tags: ImmutableListOfLists<String>?, onNewReferences: (List<Nip19.Return>) -> Unit) {
viewModelScope.launch(Dispatchers.IO) {
onNewReferences(MarkdownParser().returnNIP19References(content, tags))
}
}
fun returnMarkdownWithSpecialContent(content: String, tags: ImmutableListOfLists<String>?, onNewContent: (String) -> Unit) {
viewModelScope.launch(Dispatchers.IO) {
onNewContent(MarkdownParser().returnMarkdownWithSpecialContent(content, tags))
}
}
fun parseNIP19(str: String, onNote: (LoadedBechLink) -> Unit) {
viewModelScope.launch(Dispatchers.IO) {
Nip19.uriToRoute(str)?.let {
var returningNote: Note? = null
if (it.type == Nip19.Type.NOTE || it.type == Nip19.Type.EVENT || it.type == Nip19.Type.ADDRESS) {
LocalCache.checkGetOrCreateNote(it.hex)?.let { note ->
returningNote = note
}
}
onNote(LoadedBechLink(returningNote, it))
}
}
}
fun loadAndMarkAsRead(routeForLastRead: String, baseNoteCreatedAt: Long?, onIsNew: (Boolean) -> Unit) {
viewModelScope.launch(Dispatchers.IO) {
val lastTime = account.loadLastRead(routeForLastRead)
if (baseNoteCreatedAt != null) {
account.markAsRead(routeForLastRead, baseNoteCreatedAt)
onIsNew(baseNoteCreatedAt > lastTime)
} else {
onIsNew(false)
}
}
}
fun createChatRoomFor(user: User, then: (Int) -> Unit) {
viewModelScope.launch(Dispatchers.IO) {
val withKey = ChatroomKey(persistentSetOf(user.pubkeyHex))
account.userProfile().createChatroom(withKey)
then(withKey.hashCode())
}
}
class Factory(val account: Account) : ViewModelProvider.Factory {
override fun <AccountViewModel : ViewModel> create(modelClass: Class<AccountViewModel>): AccountViewModel {
return AccountViewModel(account) as AccountViewModel
}
}
}
@Immutable
data class LoadedBechLink(val baseNote: Note?, val nip19: Nip19.Return)