Improves multiple error dialogs appearing when zapping from the reactions bar in the feed.

This commit is contained in:
Vitor Pamplona 2025-03-14 13:57:33 -04:00
parent d76de0123a
commit fd538ebde1
46 changed files with 502 additions and 282 deletions

View File

@ -65,6 +65,10 @@
<option name="composableFile" value="true" /> <option name="composableFile" value="true" />
<option name="previewFile" value="true" /> <option name="previewFile" value="true" />
</inspection_tool> </inspection_tool>
<inspection_tool class="PreviewParameterProviderOnFirstParameter" enabled="true" level="ERROR" enabled_by_default="true">
<option name="composableFile" value="true" />
<option name="previewFile" value="true" />
</inspection_tool>
<inspection_tool class="PreviewPickerAnnotation" enabled="true" level="ERROR" enabled_by_default="true"> <inspection_tool class="PreviewPickerAnnotation" enabled="true" level="ERROR" enabled_by_default="true">
<option name="composableFile" value="true" /> <option name="composableFile" value="true" />
<option name="previewFile" value="true" /> <option name="previewFile" value="true" />

View File

@ -331,7 +331,7 @@ fun EditPostView(
it, it,
accountViewModel.account.settings.defaultFileServer, accountViewModel.account.settings.defaultFileServer,
onAdd = { alt, server, sensitiveContent, mediaQuality -> onAdd = { alt, server, sensitiveContent, mediaQuality ->
postViewModel.upload(alt, sensitiveContent, mediaQuality, false, server, accountViewModel::toast, context) postViewModel.upload(alt, sensitiveContent, mediaQuality, false, server, accountViewModel.toastManager::toast, context)
if (server.type != ServerType.NIP95) { if (server.type != ServerType.NIP95) {
accountViewModel.account.settings.changeDefaultFileServer(server) accountViewModel.account.settings.changeDefaultFileServer(server)
} }
@ -365,7 +365,7 @@ fun EditPostView(
postViewModel.wantsInvoice = false postViewModel.wantsInvoice = false
}, },
onClose = { postViewModel.wantsInvoice = false }, onClose = { postViewModel.wantsInvoice = false },
onError = { title, message -> accountViewModel.toast(title, message) }, onError = { title, message -> accountViewModel.toastManager.toast(title, message) },
) )
} }
} }

View File

@ -140,7 +140,7 @@ fun NewMediaView(
PostButton( PostButton(
onPost = { onPost = {
postViewModel.upload(context, relayList, onClose, accountViewModel::toast) postViewModel.upload(context, relayList, onClose, accountViewModel.toastManager::toast)
postViewModel.selectedServer?.let { postViewModel.selectedServer?.let {
if (it.type != ServerType.NIP95) { if (it.type != ServerType.NIP95) {
account.settings.changeDefaultFileServer(it) account.settings.changeDefaultFileServer(it)

View File

@ -198,7 +198,7 @@ fun NewUserMetadataScreen(
tint = MaterialTheme.colorScheme.placeholderText, tint = MaterialTheme.colorScheme.placeholderText,
modifier = Modifier.padding(start = 5.dp), modifier = Modifier.padding(start = 5.dp),
) { ) {
postViewModel.uploadForPicture(it, context, onError = accountViewModel::toast) postViewModel.uploadForPicture(it, context, onError = accountViewModel.toastManager::toast)
} }
}, },
singleLine = true, singleLine = true,
@ -223,7 +223,7 @@ fun NewUserMetadataScreen(
tint = MaterialTheme.colorScheme.placeholderText, tint = MaterialTheme.colorScheme.placeholderText,
modifier = Modifier.padding(start = 5.dp), modifier = Modifier.padding(start = 5.dp),
) { ) {
postViewModel.uploadForBanner(it, context, onError = accountViewModel::toast) postViewModel.uploadForBanner(it, context, onError = accountViewModel.toastManager::toast)
} }
}, },
singleLine = true, singleLine = true,

View File

@ -218,7 +218,7 @@ fun RelaySelectionDialog(
) )
} }
accountViewModel.toast( accountViewModel.toastManager.toast(
stringRes(context, R.string.unable_to_download_relay_document), stringRes(context, R.string.unable_to_download_relay_document),
msg, msg,
) )

View File

@ -103,7 +103,7 @@ fun BasicRelaySetupInfoDialog(
) )
} }
accountViewModel.toast( accountViewModel.toastManager.toast(
stringRes(context, R.string.unable_to_download_relay_document), stringRes(context, R.string.unable_to_download_relay_document),
msg, msg,
) )

View File

@ -280,7 +280,7 @@ fun LoadRelayInfo(
) )
} }
accountViewModel.toast( accountViewModel.toastManager.toast(
stringRes(context, R.string.unable_to_download_relay_document), stringRes(context, R.string.unable_to_download_relay_document),
msg, msg,
) )

View File

@ -105,7 +105,7 @@ fun Kind3RelaySetupInfoProposalDialog(
) )
} }
accountViewModel.toast( accountViewModel.toastManager.toast(
stringRes(context, R.string.unable_to_download_relay_document), stringRes(context, R.string.unable_to_download_relay_document),
msg, msg,
) )

View File

@ -64,7 +64,7 @@ fun RelayStatusRow(
.pointerInput(Unit) { .pointerInput(Unit) {
detectTapGestures( detectTapGestures(
onLongPress = { onLongPress = {
accountViewModel.toast( accountViewModel.toastManager.toast(
R.string.read_from_relay, R.string.read_from_relay,
R.string.read_from_relay_description, R.string.read_from_relay_description,
) )
@ -91,7 +91,7 @@ fun RelayStatusRow(
.pointerInput(Unit) { .pointerInput(Unit) {
detectTapGestures( detectTapGestures(
onLongPress = { onLongPress = {
accountViewModel.toast( accountViewModel.toastManager.toast(
R.string.write_to_relay, R.string.write_to_relay,
R.string.write_to_relay_description, R.string.write_to_relay_description,
) )
@ -118,7 +118,7 @@ fun RelayStatusRow(
.pointerInput(Unit) { .pointerInput(Unit) {
detectTapGestures( detectTapGestures(
onLongPress = { onLongPress = {
accountViewModel.toast( accountViewModel.toastManager.toast(
R.string.errors, R.string.errors,
R.string.errors_description, R.string.errors_description,
) )
@ -150,7 +150,7 @@ fun RelayStatusRow(
.pointerInput(Unit) { .pointerInput(Unit) {
detectTapGestures( detectTapGestures(
onLongPress = { onLongPress = {
accountViewModel.toast( accountViewModel.toastManager.toast(
R.string.spam, R.string.spam,
R.string.spam_description, R.string.spam_description,
) )

View File

@ -116,7 +116,7 @@ fun CashuPreview(
CashuPreviewNew( CashuPreviewNew(
it, it,
accountViewModel::meltCashu, accountViewModel::meltCashu,
accountViewModel::toast, accountViewModel.toastManager::toast,
) )
} }
} }

View File

@ -1235,10 +1235,10 @@ private fun saveMediaToGallery(
mimeType = mimeType, mimeType = mimeType,
localContext = localContext, localContext = localContext,
onSuccess = { onSuccess = {
accountViewModel.toast(R.string.video_saved_to_the_gallery, R.string.video_saved_to_the_gallery) accountViewModel.toastManager.toast(R.string.video_saved_to_the_gallery, R.string.video_saved_to_the_gallery)
}, },
onError = { onError = {
accountViewModel.toast(R.string.failed_to_save_the_video, null, it) accountViewModel.toastManager.toast(R.string.failed_to_save_the_video, null, it)
}, },
) )
} }

View File

@ -312,10 +312,10 @@ private fun saveMediaToGallery(
forceProxy = useTor, forceProxy = useTor,
localContext, localContext,
onSuccess = { onSuccess = {
accountViewModel.toast(success, success) accountViewModel.toastManager.toast(success, success)
}, },
onError = { onError = {
accountViewModel.toast(failure, null, it) accountViewModel.toastManager.toast(failure, null, it)
}, },
) )
} else if (content is MediaPreloadedContent) { } else if (content is MediaPreloadedContent) {
@ -325,10 +325,10 @@ private fun saveMediaToGallery(
content.mimeType, content.mimeType,
localContext, localContext,
onSuccess = { onSuccess = {
accountViewModel.toast(success, success) accountViewModel.toastManager.toast(success, success)
}, },
onError = { innerIt -> onError = { innerIt ->
accountViewModel.toast(failure, null, innerIt) accountViewModel.toastManager.toast(failure, null, innerIt)
}, },
) )
} }

View File

@ -726,7 +726,7 @@ fun ShareImageAction(
val n19 = Nip19Parser.uriToRoute(postNostrUri)?.entity as? NEvent val n19 = Nip19Parser.uriToRoute(postNostrUri)?.entity as? NEvent
if (n19 != null) { if (n19 != null) {
accountViewModel.addMediaToGallery(n19.hex, videoUri, n19.relay.getOrNull(0), blurhash, dim, hash, mimeType) // TODO Whole list or first? accountViewModel.addMediaToGallery(n19.hex, videoUri, n19.relay.getOrNull(0), blurhash, dim, hash, mimeType) // TODO Whole list or first?
accountViewModel.toast(R.string.media_added, R.string.media_added_to_profile_gallery) accountViewModel.toastManager.toast(R.string.media_added, R.string.media_added_to_profile_gallery)
} }
} }

View File

@ -18,20 +18,24 @@
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/ */
package com.vitorpamplona.amethyst.ui.components package com.vitorpamplona.amethyst.ui.components.toasts
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.vitorpamplona.amethyst.ui.actions.InformationDialog import com.vitorpamplona.amethyst.ui.actions.InformationDialog
import com.vitorpamplona.amethyst.ui.components.toasts.multiline.MultiErrorToastMsg
import com.vitorpamplona.amethyst.ui.components.toasts.multiline.MultiUserErrorMessageDialog
import com.vitorpamplona.amethyst.ui.navigation.INav
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
import com.vitorpamplona.amethyst.ui.screen.loggedIn.ResourceToastMsg
import com.vitorpamplona.amethyst.ui.screen.loggedIn.StringToastMsg
import com.vitorpamplona.amethyst.ui.screen.loggedIn.ThrowableToastMsg
import com.vitorpamplona.amethyst.ui.stringRes import com.vitorpamplona.amethyst.ui.stringRes
@Composable @Composable
fun DisplayErrorMessages(accountViewModel: AccountViewModel) { fun DisplayErrorMessages(
val openDialogMsg = accountViewModel.toasts.collectAsStateWithLifecycle(null) toastManager: ToastManager,
accountViewModel: AccountViewModel,
nav: INav,
) {
val openDialogMsg = toastManager.toasts.collectAsStateWithLifecycle(null)
openDialogMsg.value?.let { obj -> openDialogMsg.value?.let { obj ->
when (obj) { when (obj) {
@ -41,14 +45,14 @@ fun DisplayErrorMessages(accountViewModel: AccountViewModel) {
stringRes(obj.titleResId), stringRes(obj.titleResId),
stringRes(obj.resourceId, *obj.params), stringRes(obj.resourceId, *obj.params),
) { ) {
accountViewModel.clearToasts() toastManager.clearToasts()
} }
} else { } else {
InformationDialog( InformationDialog(
stringRes(obj.titleResId), stringRes(obj.titleResId),
stringRes(obj.resourceId), stringRes(obj.resourceId),
) { ) {
accountViewModel.clearToasts() toastManager.clearToasts()
} }
} }
@ -57,7 +61,7 @@ fun DisplayErrorMessages(accountViewModel: AccountViewModel) {
obj.title, obj.title,
obj.msg, obj.msg,
) { ) {
accountViewModel.clearToasts() toastManager.clearToasts()
} }
is ThrowableToastMsg -> is ThrowableToastMsg ->
@ -66,8 +70,10 @@ fun DisplayErrorMessages(accountViewModel: AccountViewModel) {
obj.msg, obj.msg,
obj.throwable, obj.throwable,
) { ) {
accountViewModel.clearToasts() toastManager.clearToasts()
} }
is MultiErrorToastMsg -> MultiUserErrorMessageDialog(obj, accountViewModel, nav)
} }
} }
} }

View File

@ -0,0 +1,30 @@
/**
* Copyright (c) 2024 Vitor Pamplona
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of
* this software and associated documentation files (the "Software"), to deal in
* the Software without restriction, including without limitation the rights to use,
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
* Software, and to permit persons to whom the Software is furnished to do so,
* subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
package com.vitorpamplona.amethyst.ui.components.toasts
import androidx.compose.runtime.Immutable
@Immutable
class ResourceToastMsg(
val titleResId: Int,
val resourceId: Int,
val params: Array<out String>? = null,
) : ToastMsg()

View File

@ -0,0 +1,29 @@
/**
* Copyright (c) 2024 Vitor Pamplona
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of
* this software and associated documentation files (the "Software"), to deal in
* the Software without restriction, including without limitation the rights to use,
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
* Software, and to permit persons to whom the Software is furnished to do so,
* subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
package com.vitorpamplona.amethyst.ui.components.toasts
import androidx.compose.runtime.Immutable
@Immutable
class StringToastMsg(
val title: String,
val msg: String,
) : ToastMsg()

View File

@ -0,0 +1,30 @@
/**
* Copyright (c) 2024 Vitor Pamplona
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of
* this software and associated documentation files (the "Software"), to deal in
* the Software without restriction, including without limitation the rights to use,
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
* Software, and to permit persons to whom the Software is furnished to do so,
* subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
package com.vitorpamplona.amethyst.ui.components.toasts
import androidx.compose.runtime.Immutable
@Immutable
class ThrowableToastMsg(
val titleResId: Int,
val msg: String? = null,
val throwable: Throwable,
) : ToastMsg()

View File

@ -0,0 +1,89 @@
/**
* Copyright (c) 2024 Vitor Pamplona
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of
* this software and associated documentation files (the "Software"), to deal in
* the Software without restriction, including without limitation the rights to use,
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
* Software, and to permit persons to whom the Software is furnished to do so,
* subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
package com.vitorpamplona.amethyst.ui.components.toasts
import com.vitorpamplona.amethyst.model.User
import com.vitorpamplona.amethyst.ui.components.toasts.multiline.MultiErrorToastMsg
import com.vitorpamplona.amethyst.ui.components.toasts.multiline.UserBasedErrorMessage
import kotlinx.coroutines.flow.MutableStateFlow
class ToastManager {
val toasts = MutableStateFlow<ToastMsg?>(null)
fun clearToasts() {
toasts.tryEmit(null)
}
fun toast(
title: String,
message: String,
) {
toasts.tryEmit(StringToastMsg(title, message))
}
fun toast(
titleResId: Int,
resourceId: Int,
) {
toasts.tryEmit(ResourceToastMsg(titleResId, resourceId))
}
fun toast(
titleResId: Int,
message: String?,
throwable: Throwable,
) {
toasts.tryEmit(ThrowableToastMsg(titleResId, message, throwable))
}
fun toast(
titleResId: Int,
resourceId: Int,
vararg params: String,
) {
toasts.tryEmit(ResourceToastMsg(titleResId, resourceId, params))
}
fun toast(
titleResId: Int,
message: String,
user: User?,
) {
val current = toasts.value
if (current is MultiErrorToastMsg && current.titleResId == titleResId) {
current.add(message, user)
} else {
toasts.tryEmit(MultiErrorToastMsg(titleResId).also { it.add(message, user) })
}
}
fun toast(
titleResId: Int,
data: UserBasedErrorMessage,
) {
val current = toasts.value
if (current is MultiErrorToastMsg && current.titleResId == titleResId) {
current.add(data)
} else {
toasts.tryEmit(MultiErrorToastMsg(titleResId).also { it.add(data) })
}
}
}

View File

@ -0,0 +1,26 @@
/**
* Copyright (c) 2024 Vitor Pamplona
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of
* this software and associated documentation files (the "Software"), to deal in
* the Software without restriction, including without limitation the rights to use,
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
* Software, and to permit persons to whom the Software is furnished to do so,
* subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
package com.vitorpamplona.amethyst.ui.components.toasts
import androidx.compose.runtime.Immutable
@Immutable
open class ToastMsg

View File

@ -18,11 +18,10 @@
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/ */
package com.vitorpamplona.amethyst.ui.note package com.vitorpamplona.amethyst.ui.components.toasts.multiline
import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.fillMaxWidth
@ -31,56 +30,44 @@ import androidx.compose.foundation.layout.width
import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.itemsIndexed import androidx.compose.foundation.lazy.itemsIndexed
import androidx.compose.foundation.text.selection.SelectionContainer import androidx.compose.foundation.text.selection.SelectionContainer
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.outlined.Done
import androidx.compose.material3.AlertDialog
import androidx.compose.material3.Button
import androidx.compose.material3.HorizontalDivider import androidx.compose.material3.HorizontalDivider
import androidx.compose.material3.Icon import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.Stable
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.painterResource
import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.Preview
import androidx.lifecycle.ViewModel
import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.lifecycle.compose.collectAsStateWithLifecycle
import androidx.lifecycle.viewmodel.compose.viewModel
import com.vitorpamplona.amethyst.R import com.vitorpamplona.amethyst.R
import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.amethyst.model.LocalCache
import com.vitorpamplona.amethyst.model.User import com.vitorpamplona.amethyst.model.User
import com.vitorpamplona.amethyst.ui.navigation.EmptyNav import com.vitorpamplona.amethyst.ui.navigation.EmptyNav
import com.vitorpamplona.amethyst.ui.navigation.INav import com.vitorpamplona.amethyst.ui.navigation.INav
import com.vitorpamplona.amethyst.ui.navigation.routeToMessage import com.vitorpamplona.amethyst.ui.navigation.routeToMessage
import com.vitorpamplona.amethyst.ui.note.UserPicture
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
import com.vitorpamplona.amethyst.ui.screen.loggedIn.mockAccountViewModel import com.vitorpamplona.amethyst.ui.screen.loggedIn.mockAccountViewModel
import com.vitorpamplona.amethyst.ui.stringRes import com.vitorpamplona.amethyst.ui.stringRes
import com.vitorpamplona.amethyst.ui.theme.DividerThickness import com.vitorpamplona.amethyst.ui.theme.DividerThickness
import com.vitorpamplona.amethyst.ui.theme.Size16dp
import com.vitorpamplona.amethyst.ui.theme.Size20Modifier import com.vitorpamplona.amethyst.ui.theme.Size20Modifier
import com.vitorpamplona.amethyst.ui.theme.Size30Modifier import com.vitorpamplona.amethyst.ui.theme.Size30Modifier
import com.vitorpamplona.amethyst.ui.theme.Size30dp import com.vitorpamplona.amethyst.ui.theme.Size30dp
import com.vitorpamplona.amethyst.ui.theme.Size40dp import com.vitorpamplona.amethyst.ui.theme.Size40dp
import com.vitorpamplona.amethyst.ui.theme.Size5dp import com.vitorpamplona.amethyst.ui.theme.Size5dp
import com.vitorpamplona.amethyst.ui.theme.StdHorzSpacer
import com.vitorpamplona.amethyst.ui.theme.StdVertSpacer import com.vitorpamplona.amethyst.ui.theme.StdVertSpacer
import com.vitorpamplona.amethyst.ui.theme.ThemeComparisonColumn import com.vitorpamplona.amethyst.ui.theme.ThemeComparisonColumn
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.runBlocking import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
@Composable @Composable
@Preview @Preview
fun MultiUserErrorMessageContentPreview() { fun ErrorListPreview() {
val accountViewModel = mockAccountViewModel() val accountViewModel = mockAccountViewModel()
val nav = EmptyNav
var user1: User? = null var user1: User? = null
var user2: User? = null var user2: User? = null
@ -88,107 +75,43 @@ fun MultiUserErrorMessageContentPreview() {
runBlocking { runBlocking {
withContext(Dispatchers.IO) { withContext(Dispatchers.IO) {
user1 = LocalCache.getOrCreateUser("aaabccaabbccaabbcc") user1 = LocalCache.getOrCreateUser("aaabccaabbccaabbccabdd")
user2 = LocalCache.getOrCreateUser("bbbccabbbccabbbcca") user2 = LocalCache.getOrCreateUser("bbbccabbbccabbbccaabdd")
user3 = LocalCache.getOrCreateUser("ccaadaccaadaccaada") user3 = LocalCache.getOrCreateUser("ccaadaccaadaccaadaabdd")
} }
} }
val model: UserBasedErrorMessageViewModel = viewModel() val model = MultiErrorToastMsg(R.string.error_dialog_zap_error)
model.add("Could not fetch invoice from https://minibits.cash/.well-known/lnurlp/victorieeman: There are too many unpaid invoices for this name.", user1) model.add("Could not fetch invoice from https://minibits.cash/.well-known/lnurlp/victorieeman: There are too many unpaid invoices for this name.", user1)
model.add("No Wallets found to pay a lightning invoice. Please install a Lightning wallet to use zaps.", user2) model.add("No Wallets found to pay a lightning invoice. Please install a Lightning wallet to use zaps.", user2)
model.add("Could not fetch invoice", user3) model.add("Could not fetch invoice", user3)
ThemeComparisonColumn { ThemeComparisonColumn {
MultiUserErrorMessageDialogInner( ErrorList(
title = "Couldn't not zap",
model = model, model = model,
accountViewModel = accountViewModel, accountViewModel = accountViewModel,
nav = nav, nav = EmptyNav,
) )
} }
} }
@Stable
class UserBasedErrorMessageViewModel : ViewModel() {
val errors = MutableStateFlow<List<UserBasedErrorMessage>>(emptyList())
val hasErrors = errors.map { it.isNotEmpty() }
fun add(
message: String,
user: User?,
) {
add(UserBasedErrorMessage(message, user))
}
fun add(newError: UserBasedErrorMessage) {
errors.update {
it + newError
}
}
fun clearErrors() {
errors.update {
emptyList()
}
}
}
class UserBasedErrorMessage(
val error: String,
val user: User?,
)
@Composable @Composable
fun MultiUserErrorMessageDialog( fun ErrorList(
title: String, model: MultiErrorToastMsg,
model: UserBasedErrorMessageViewModel,
accountViewModel: AccountViewModel, accountViewModel: AccountViewModel,
nav: INav, nav: INav,
) { ) {
val hasErrors by model.hasErrors.collectAsStateWithLifecycle(false) val errorState by model.errors.collectAsStateWithLifecycle()
if (hasErrors) { LazyColumn {
MultiUserErrorMessageDialogInner(title, model, accountViewModel, nav) itemsIndexed(errorState) { index, it ->
ErrorRow(it, accountViewModel, nav)
if (index < errorState.size - 1) {
HorizontalDivider(thickness = DividerThickness)
}
}
} }
} }
@Composable
fun MultiUserErrorMessageDialogInner(
title: String,
model: UserBasedErrorMessageViewModel,
accountViewModel: AccountViewModel,
nav: INav,
) {
AlertDialog(
onDismissRequest = model::clearErrors,
title = { Text(title) },
text = {
val errorState by model.errors.collectAsStateWithLifecycle(emptyList())
LazyColumn {
itemsIndexed(errorState) { index, it ->
ErrorRow(it, accountViewModel, nav)
if (index < errorState.size - 1) {
HorizontalDivider(thickness = DividerThickness)
}
}
}
},
confirmButton = {
Button(
onClick = model::clearErrors,
contentPadding = PaddingValues(horizontal = Size16dp),
) {
Icon(
imageVector = Icons.Outlined.Done,
contentDescription = null,
)
Spacer(StdHorzSpacer)
Text(stringRes(R.string.error_dialog_button_ok))
}
},
)
}
@Composable @Composable
fun ErrorRow( fun ErrorRow(
errorState: UserBasedErrorMessage, errorState: UserBasedErrorMessage,
@ -201,7 +124,7 @@ fun ErrorRow(
) { ) {
errorState.user?.let { errorState.user?.let {
Column(Modifier.width(Size40dp), horizontalAlignment = Alignment.Start) { Column(Modifier.width(Size40dp), horizontalAlignment = Alignment.Start) {
UserPicture(errorState.user, Size30dp, Modifier, accountViewModel, nav) UserPicture(it, Size30dp, Modifier, accountViewModel, nav)
Spacer(StdVertSpacer) Spacer(StdVertSpacer)
IconButton( IconButton(
modifier = Size30Modifier, modifier = Size30Modifier,

View File

@ -0,0 +1,52 @@
/**
* Copyright (c) 2024 Vitor Pamplona
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of
* this software and associated documentation files (the "Software"), to deal in
* the Software without restriction, including without limitation the rights to use,
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
* Software, and to permit persons to whom the Software is furnished to do so,
* subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
package com.vitorpamplona.amethyst.ui.components.toasts.multiline
import androidx.compose.runtime.Immutable
import com.vitorpamplona.amethyst.model.User
import com.vitorpamplona.amethyst.ui.components.toasts.ToastMsg
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.update
@Immutable
class MultiErrorToastMsg(
val titleResId: Int,
) : ToastMsg() {
val errors = MutableStateFlow<List<UserBasedErrorMessage>>(emptyList())
fun add(
message: String,
user: User?,
) {
add(UserBasedErrorMessage(message, user))
}
fun add(newError: UserBasedErrorMessage) {
errors.update {
it + newError
}
}
}
class UserBasedErrorMessage(
val error: String,
val user: User?,
)

View File

@ -0,0 +1,107 @@
/**
* Copyright (c) 2024 Vitor Pamplona
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of
* this software and associated documentation files (the "Software"), to deal in
* the Software without restriction, including without limitation the rights to use,
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
* Software, and to permit persons to whom the Software is furnished to do so,
* subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
package com.vitorpamplona.amethyst.ui.components.toasts.multiline
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Spacer
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.outlined.Done
import androidx.compose.material3.AlertDialog
import androidx.compose.material3.Button
import androidx.compose.material3.Icon
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.ui.tooling.preview.Preview
import com.vitorpamplona.amethyst.R
import com.vitorpamplona.amethyst.model.LocalCache
import com.vitorpamplona.amethyst.model.User
import com.vitorpamplona.amethyst.ui.navigation.EmptyNav
import com.vitorpamplona.amethyst.ui.navigation.INav
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
import com.vitorpamplona.amethyst.ui.screen.loggedIn.mockAccountViewModel
import com.vitorpamplona.amethyst.ui.stringRes
import com.vitorpamplona.amethyst.ui.theme.Size16dp
import com.vitorpamplona.amethyst.ui.theme.StdHorzSpacer
import com.vitorpamplona.amethyst.ui.theme.ThemeComparisonColumn
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.withContext
@Composable
@Preview
fun MultiUserErrorMessageContentPreview() {
val accountViewModel = mockAccountViewModel()
val nav = EmptyNav
var user1: User? = null
var user2: User? = null
var user3: User? = null
runBlocking {
withContext(Dispatchers.IO) {
user1 = LocalCache.getOrCreateUser("aaabccaabbccaabbccabdd")
user2 = LocalCache.getOrCreateUser("bbbccabbbccabbbccaabdd")
user3 = LocalCache.getOrCreateUser("ccaadaccaadaccaadaabdd")
}
}
val model = MultiErrorToastMsg(R.string.error_dialog_zap_error)
model.add("Could not fetch invoice from https://minibits.cash/.well-known/lnurlp/victorieeman: There are too many unpaid invoices for this name.", user1)
model.add("No Wallets found to pay a lightning invoice. Please install a Lightning wallet to use zaps.", user2)
model.add("Could not fetch invoice", user3)
ThemeComparisonColumn {
MultiUserErrorMessageDialog(
model = model,
accountViewModel = accountViewModel,
nav = nav,
)
}
}
@Composable
fun MultiUserErrorMessageDialog(
model: MultiErrorToastMsg,
accountViewModel: AccountViewModel,
nav: INav,
) {
AlertDialog(
onDismissRequest = accountViewModel.toastManager::clearToasts,
title = { Text(stringRes(model.titleResId)) },
text = {
ErrorList(model, accountViewModel, nav)
},
confirmButton = {
Button(
onClick = accountViewModel.toastManager::clearToasts,
contentPadding = PaddingValues(horizontal = Size16dp),
) {
Icon(
imageVector = Icons.Outlined.Done,
contentDescription = null,
)
Spacer(StdHorzSpacer)
Text(stringRes(R.string.error_dialog_button_ok))
}
},
)
}

View File

@ -51,8 +51,8 @@ import com.vitorpamplona.amethyst.R
import com.vitorpamplona.amethyst.ui.MainActivity import com.vitorpamplona.amethyst.ui.MainActivity
import com.vitorpamplona.amethyst.ui.actions.NewUserMetadataScreen import com.vitorpamplona.amethyst.ui.actions.NewUserMetadataScreen
import com.vitorpamplona.amethyst.ui.actions.relays.AllRelayListView import com.vitorpamplona.amethyst.ui.actions.relays.AllRelayListView
import com.vitorpamplona.amethyst.ui.components.DisplayErrorMessages
import com.vitorpamplona.amethyst.ui.components.DisplayNotifyMessages import com.vitorpamplona.amethyst.ui.components.DisplayNotifyMessages
import com.vitorpamplona.amethyst.ui.components.toasts.DisplayErrorMessages
import com.vitorpamplona.amethyst.ui.screen.AccountStateViewModel import com.vitorpamplona.amethyst.ui.screen.AccountStateViewModel
import com.vitorpamplona.amethyst.ui.screen.SharedPreferencesViewModel import com.vitorpamplona.amethyst.ui.screen.SharedPreferencesViewModel
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountSwitcherAndLeftDrawerLayout import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountSwitcherAndLeftDrawerLayout
@ -354,7 +354,7 @@ fun AppNavigation(
NavigateIfIntentRequested(nav, accountViewModel, accountStateViewModel) NavigateIfIntentRequested(nav, accountViewModel, accountStateViewModel)
DisplayErrorMessages(accountViewModel) DisplayErrorMessages(accountViewModel.toastManager, accountViewModel, nav)
DisplayNotifyMessages(accountViewModel, nav) DisplayNotifyMessages(accountViewModel, nav)
} }
@ -437,7 +437,7 @@ private fun NavigateIfIntentRequested(
actionableNextPage = null actionableNextPage = null
} else { } else {
accountViewModel.toast( accountViewModel.toastManager.toast(
R.string.invalid_nip19_uri, R.string.invalid_nip19_uri,
R.string.invalid_nip19_uri_description, R.string.invalid_nip19_uri_description,
intentNextPage, intentNextPage,
@ -492,7 +492,7 @@ private fun NavigateIfIntentRequested(
} else { } else {
scope.launch { scope.launch {
delay(1000) delay(1000)
accountViewModel.toast( accountViewModel.toastManager.toast(
R.string.invalid_nip19_uri, R.string.invalid_nip19_uri,
R.string.invalid_nip19_uri_description, R.string.invalid_nip19_uri_description,
uri, uri,

View File

@ -551,7 +551,7 @@ fun ListContent(
accountViewModel.setTorSettings(torSettings) accountViewModel.setTorSettings(torSettings)
}, },
onError = { onError = {
accountViewModel.toast( accountViewModel.toastManager.toast(
stringRes(context, R.string.could_not_connect_to_tor), stringRes(context, R.string.could_not_connect_to_tor),
it, it,
) )

View File

@ -76,11 +76,11 @@ import com.vitorpamplona.amethyst.model.Note
import com.vitorpamplona.amethyst.model.User import com.vitorpamplona.amethyst.model.User
import com.vitorpamplona.amethyst.service.ZapPaymentHandler import com.vitorpamplona.amethyst.service.ZapPaymentHandler
import com.vitorpamplona.amethyst.ui.components.TranslatableRichTextViewer import com.vitorpamplona.amethyst.ui.components.TranslatableRichTextViewer
import com.vitorpamplona.amethyst.ui.components.toasts.StringToastMsg
import com.vitorpamplona.amethyst.ui.navigation.EmptyNav import com.vitorpamplona.amethyst.ui.navigation.EmptyNav
import com.vitorpamplona.amethyst.ui.navigation.INav import com.vitorpamplona.amethyst.ui.navigation.INav
import com.vitorpamplona.amethyst.ui.navigation.routeToMessage import com.vitorpamplona.amethyst.ui.navigation.routeToMessage
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
import com.vitorpamplona.amethyst.ui.screen.loggedIn.StringToastMsg
import com.vitorpamplona.amethyst.ui.screen.loggedIn.mockAccountViewModel import com.vitorpamplona.amethyst.ui.screen.loggedIn.mockAccountViewModel
import com.vitorpamplona.amethyst.ui.screen.loggedIn.mockVitorAccountViewModel import com.vitorpamplona.amethyst.ui.screen.loggedIn.mockVitorAccountViewModel
import com.vitorpamplona.amethyst.ui.stringRes import com.vitorpamplona.amethyst.ui.stringRes
@ -530,23 +530,23 @@ fun ZapVote(
indication = ripple24dp, indication = ripple24dp,
onClick = { onClick = {
if (!accountViewModel.isWriteable()) { if (!accountViewModel.isWriteable()) {
accountViewModel.toast( accountViewModel.toastManager.toast(
R.string.read_only_user, R.string.read_only_user,
R.string.login_with_a_private_key_to_be_able_to_send_zaps, R.string.login_with_a_private_key_to_be_able_to_send_zaps,
) )
} else if (pollViewModel.isPollClosed()) { } else if (pollViewModel.isPollClosed()) {
accountViewModel.toast( accountViewModel.toastManager.toast(
R.string.poll_unable_to_vote, R.string.poll_unable_to_vote,
R.string.poll_is_closed_explainer, R.string.poll_is_closed_explainer,
) )
} else if (isLoggedUser) { } else if (isLoggedUser) {
accountViewModel.toast( accountViewModel.toastManager.toast(
R.string.poll_unable_to_vote, R.string.poll_unable_to_vote,
R.string.poll_author_no_vote, R.string.poll_author_no_vote,
) )
} else if (pollViewModel.isVoteAmountAtomic() && poolOption.zappedByLoggedIn.value) { } else if (pollViewModel.isVoteAmountAtomic() && poolOption.zappedByLoggedIn.value) {
// only allow one vote per option when min==max, i.e. atomic vote amount specified // only allow one vote per option when min==max, i.e. atomic vote amount specified
accountViewModel.toast( accountViewModel.toastManager.toast(
R.string.poll_unable_to_vote, R.string.poll_unable_to_vote,
R.string.one_vote_per_user_on_atomic_votes, R.string.one_vote_per_user_on_atomic_votes,
) )

View File

@ -99,12 +99,12 @@ import androidx.lifecycle.MediatorLiveData
import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.lifecycle.compose.collectAsStateWithLifecycle
import androidx.lifecycle.distinctUntilChanged import androidx.lifecycle.distinctUntilChanged
import androidx.lifecycle.map import androidx.lifecycle.map
import androidx.lifecycle.viewmodel.compose.viewModel
import com.vitorpamplona.amethyst.R import com.vitorpamplona.amethyst.R
import com.vitorpamplona.amethyst.commons.emojicoder.EmojiCoder import com.vitorpamplona.amethyst.commons.emojicoder.EmojiCoder
import com.vitorpamplona.amethyst.model.FeatureSetType import com.vitorpamplona.amethyst.model.FeatureSetType
import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.model.Note
import com.vitorpamplona.amethyst.model.User import com.vitorpamplona.amethyst.model.User
import com.vitorpamplona.amethyst.service.NostrUserProfileDataSource.user
import com.vitorpamplona.amethyst.service.ZapPaymentHandler import com.vitorpamplona.amethyst.service.ZapPaymentHandler
import com.vitorpamplona.amethyst.ui.actions.CrossfadeIfEnabled import com.vitorpamplona.amethyst.ui.actions.CrossfadeIfEnabled
import com.vitorpamplona.amethyst.ui.components.AnimatedBorderTextCornerRadius import com.vitorpamplona.amethyst.ui.components.AnimatedBorderTextCornerRadius
@ -662,7 +662,7 @@ fun ReplyReaction(
modifier = iconSizeModifier, modifier = iconSizeModifier,
onClick = { onClick = {
if (baseNote.isDraft()) { if (baseNote.isDraft()) {
accountViewModel.toast( accountViewModel.toastManager.toast(
R.string.draft_note, R.string.draft_note,
R.string.it_s_not_possible_to_reply_to_a_draft_note, R.string.it_s_not_possible_to_reply_to_a_draft_note,
) )
@ -670,7 +670,7 @@ fun ReplyReaction(
if (accountViewModel.isWriteable()) { if (accountViewModel.isWriteable()) {
onPress() onPress()
} else { } else {
accountViewModel.toast( accountViewModel.toastManager.toast(
R.string.read_only_user, R.string.read_only_user,
R.string.login_with_a_private_key_to_be_able_to_reply, R.string.login_with_a_private_key_to_be_able_to_reply,
) )
@ -986,7 +986,7 @@ private fun likeClick(
onWantsToSignReaction: () -> Unit, onWantsToSignReaction: () -> Unit,
) { ) {
if (baseNote.isDraft()) { if (baseNote.isDraft()) {
accountViewModel.toast( accountViewModel.toastManager.toast(
R.string.draft_note, R.string.draft_note,
R.string.it_s_not_possible_to_react_to_a_draft_note, R.string.it_s_not_possible_to_react_to_a_draft_note,
) )
@ -995,12 +995,12 @@ private fun likeClick(
val choices = accountViewModel.reactionChoices() val choices = accountViewModel.reactionChoices()
if (choices.isEmpty()) { if (choices.isEmpty()) {
accountViewModel.toast( accountViewModel.toastManager.toast(
R.string.no_reactions_setup, R.string.no_reactions_setup,
R.string.no_reaction_type_setup_long_press_to_change, R.string.no_reaction_type_setup_long_press_to_change,
) )
} else if (!accountViewModel.isWriteable()) { } else if (!accountViewModel.isWriteable()) {
accountViewModel.toast( accountViewModel.toastManager.toast(
R.string.read_only_user, R.string.read_only_user,
R.string.login_with_a_private_key_to_like_posts, R.string.login_with_a_private_key_to_like_posts,
) )
@ -1025,7 +1025,6 @@ fun ZapReaction(
var wantsToZap by remember { mutableStateOf(false) } var wantsToZap by remember { mutableStateOf(false) }
var wantsToChangeZapAmount by remember { mutableStateOf(false) } var wantsToChangeZapAmount by remember { mutableStateOf(false) }
var wantsToSetCustomZap by remember { mutableStateOf(false) } var wantsToSetCustomZap by remember { mutableStateOf(false) }
val errorViewModel: UserBasedErrorMessageViewModel = viewModel()
var wantsToPay by var wantsToPay by
remember(baseNote) { remember(baseNote) {
mutableStateOf<ImmutableList<ZapPaymentHandler.Payable>>( mutableStateOf<ImmutableList<ZapPaymentHandler.Payable>>(
@ -1060,7 +1059,7 @@ fun ZapReaction(
onError = { _, message, user -> onError = { _, message, user ->
scope.launch { scope.launch {
zappingProgress = 0f zappingProgress = 0f
errorViewModel.add(message, user) accountViewModel.toastManager.toast(R.string.error_dialog_zap_error, message, user)
} }
}, },
onPayViaIntent = { wantsToPay = it }, onPayViaIntent = { wantsToPay = it },
@ -1089,7 +1088,7 @@ fun ZapReaction(
onError = { _, message, user -> onError = { _, message, user ->
scope.launch { scope.launch {
zappingProgress = 0f zappingProgress = 0f
errorViewModel.add(message, user) accountViewModel.toastManager.toast(R.string.error_dialog_zap_error, message, user)
} }
}, },
onProgress = { scope.launch(Dispatchers.Main) { zappingProgress = it } }, onProgress = { scope.launch(Dispatchers.Main) { zappingProgress = it } },
@ -1097,13 +1096,6 @@ fun ZapReaction(
) )
} }
MultiUserErrorMessageDialog(
title = stringRes(id = R.string.error_dialog_zap_error),
model = errorViewModel,
accountViewModel = accountViewModel,
nav = nav,
)
if (wantsToChangeZapAmount) { if (wantsToChangeZapAmount) {
UpdateZapAmountDialog( UpdateZapAmountDialog(
onClose = { wantsToChangeZapAmount = false }, onClose = { wantsToChangeZapAmount = false },
@ -1120,12 +1112,12 @@ fun ZapReaction(
wantsToPay = persistentListOf() wantsToPay = persistentListOf()
scope.launch { scope.launch {
zappingProgress = 0f zappingProgress = 0f
errorViewModel.add(it) accountViewModel.toastManager.toast(R.string.error_dialog_zap_error, it)
} }
}, },
justShowError = { justShowError = {
scope.launch { scope.launch {
errorViewModel.add(it) accountViewModel.toastManager.toast(R.string.error_dialog_zap_error, it)
} }
}, },
) )
@ -1137,7 +1129,7 @@ fun ZapReaction(
onError = { _, message, user -> onError = { _, message, user ->
scope.launch { scope.launch {
zappingProgress = 0f zappingProgress = 0f
errorViewModel.add(message, user) accountViewModel.toastManager.toast(R.string.error_dialog_zap_error, message, user)
} }
}, },
onProgress = { scope.launch(Dispatchers.Main) { zappingProgress = it } }, onProgress = { scope.launch(Dispatchers.Main) { zappingProgress = it } },
@ -1191,7 +1183,7 @@ fun zapClick(
onPayViaIntent: (ImmutableList<ZapPaymentHandler.Payable>) -> Unit, onPayViaIntent: (ImmutableList<ZapPaymentHandler.Payable>) -> Unit,
) { ) {
if (baseNote.isDraft()) { if (baseNote.isDraft()) {
accountViewModel.toast( accountViewModel.toastManager.toast(
R.string.draft_note, R.string.draft_note,
R.string.it_s_not_possible_to_zap_to_a_draft_note, R.string.it_s_not_possible_to_zap_to_a_draft_note,
) )
@ -1201,12 +1193,12 @@ fun zapClick(
val choices = accountViewModel.zapAmountChoices() val choices = accountViewModel.zapAmountChoices()
if (choices.isEmpty()) { if (choices.isEmpty()) {
accountViewModel.toast( accountViewModel.toastManager.toast(
R.string.error_dialog_zap_error, R.string.error_dialog_zap_error,
R.string.no_zap_amount_setup_long_press_to_change, R.string.no_zap_amount_setup_long_press_to_change,
) )
} else if (!accountViewModel.isWriteable()) { } else if (!accountViewModel.isWriteable()) {
accountViewModel.toast( accountViewModel.toastManager.toast(
R.string.error_dialog_zap_error, R.string.error_dialog_zap_error,
R.string.login_with_a_private_key_to_be_able_to_send_zaps, R.string.login_with_a_private_key_to_be_able_to_send_zaps,
) )

View File

@ -169,7 +169,7 @@ fun RenderRelay(
openRelayDialog = true openRelayDialog = true
}, },
onError = { url, errorCode, exceptionMessage -> onError = { url, errorCode, exceptionMessage ->
accountViewModel.toast( accountViewModel.toastManager.toast(
R.string.unable_to_download_relay_document, R.string.unable_to_download_relay_document,
when (errorCode) { when (errorCode) {
Nip11Retriever.ErrorCode.FAIL_TO_ASSEMBLE_URL -> Nip11Retriever.ErrorCode.FAIL_TO_ASSEMBLE_URL ->

View File

@ -337,12 +337,12 @@ fun UpdateZapAmountContent(
postViewModel.updateNIP47(nip47uri) postViewModel.updateNIP47(nip47uri)
} catch (e: IllegalArgumentException) { } catch (e: IllegalArgumentException) {
if (e.message != null) { if (e.message != null) {
accountViewModel.toast( accountViewModel.toastManager.toast(
stringRes(context, R.string.error_parsing_nip47_title), stringRes(context, R.string.error_parsing_nip47_title),
stringRes(context, R.string.error_parsing_nip47, nip47uri, e.message!!), stringRes(context, R.string.error_parsing_nip47, nip47uri, e.message!!),
) )
} else { } else {
accountViewModel.toast( accountViewModel.toastManager.toast(
stringRes(context, R.string.error_parsing_nip47_title), stringRes(context, R.string.error_parsing_nip47_title),
stringRes(context, R.string.error_parsing_nip47_no_error, nip47uri), stringRes(context, R.string.error_parsing_nip47_no_error, nip47uri),
) )
@ -510,12 +510,12 @@ fun UpdateZapAmountContent(
postViewModel.updateNIP47(it) postViewModel.updateNIP47(it)
} catch (e: IllegalArgumentException) { } catch (e: IllegalArgumentException) {
if (e.message != null) { if (e.message != null) {
accountViewModel.toast( accountViewModel.toastManager.toast(
stringRes(context, R.string.error_parsing_nip47_title), stringRes(context, R.string.error_parsing_nip47_title),
stringRes(context, R.string.error_parsing_nip47, it, e.message!!), stringRes(context, R.string.error_parsing_nip47, it, e.message!!),
) )
} else { } else {
accountViewModel.toast( accountViewModel.toastManager.toast(
stringRes(context, R.string.error_parsing_nip47_title), stringRes(context, R.string.error_parsing_nip47_title),
stringRes(context, R.string.error_parsing_nip47_no_error, it), stringRes(context, R.string.error_parsing_nip47_no_error, it),
) )
@ -611,7 +611,7 @@ fun UpdateZapAmountContent(
context = context, context = context,
keyguardLauncher = keyguardLauncher, keyguardLauncher = keyguardLauncher,
onApproved = { showPassword = true }, onApproved = { showPassword = true },
onError = { title, message -> accountViewModel.toast(title, message) }, onError = { title, message -> accountViewModel.toastManager.toast(title, message) },
) )
} else { } else {
showPassword = false showPassword = false

View File

@ -66,6 +66,7 @@ import com.vitorpamplona.amethyst.model.Note
import com.vitorpamplona.amethyst.model.User import com.vitorpamplona.amethyst.model.User
import com.vitorpamplona.amethyst.service.ZapPaymentHandler import com.vitorpamplona.amethyst.service.ZapPaymentHandler
import com.vitorpamplona.amethyst.ui.components.SetDialogToEdgeToEdge import com.vitorpamplona.amethyst.ui.components.SetDialogToEdgeToEdge
import com.vitorpamplona.amethyst.ui.components.toasts.multiline.UserBasedErrorMessage
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
import com.vitorpamplona.amethyst.ui.screen.loggedIn.CloseButton import com.vitorpamplona.amethyst.ui.screen.loggedIn.CloseButton
import com.vitorpamplona.amethyst.ui.screen.loggedIn.TextSpinner import com.vitorpamplona.amethyst.ui.screen.loggedIn.TextSpinner

View File

@ -201,7 +201,7 @@ fun ShowFollowingOrUnfollowingButton(
if (isFollowing) { if (isFollowing) {
UnfollowButton { UnfollowButton {
if (!accountViewModel.isWriteable()) { if (!accountViewModel.isWriteable()) {
accountViewModel.toast( accountViewModel.toastManager.toast(
R.string.read_only_user, R.string.read_only_user,
R.string.login_with_a_private_key_to_be_able_to_unfollow, R.string.login_with_a_private_key_to_be_able_to_unfollow,
) )
@ -212,7 +212,7 @@ fun ShowFollowingOrUnfollowingButton(
} else { } else {
FollowButton { FollowButton {
if (!accountViewModel.isWriteable()) { if (!accountViewModel.isWriteable()) {
accountViewModel.toast( accountViewModel.toastManager.toast(
R.string.read_only_user, R.string.read_only_user,
R.string.login_with_a_private_key_to_be_able_to_follow, R.string.login_with_a_private_key_to_be_able_to_follow,
) )

View File

@ -75,7 +75,7 @@ fun DisplayOts(
val fullDateTime = val fullDateTime =
SimpleDateFormat.getDateTimeInstance().format(Date(unixtimestamp * 1000)) SimpleDateFormat.getDateTimeInstance().format(Date(unixtimestamp * 1000))
accountViewModel.toast( accountViewModel.toastManager.toast(
R.string.ots_info_title, R.string.ots_info_title,
R.string.ots_info_description, R.string.ots_info_description,
fullDateTime, fullDateTime,

View File

@ -58,7 +58,6 @@ import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp import androidx.compose.ui.unit.sp
import androidx.lifecycle.viewmodel.compose.viewModel
import com.vitorpamplona.amethyst.BuildConfig import com.vitorpamplona.amethyst.BuildConfig
import com.vitorpamplona.amethyst.R import com.vitorpamplona.amethyst.R
import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.amethyst.model.LocalCache
@ -72,10 +71,8 @@ import com.vitorpamplona.amethyst.ui.navigation.EmptyNav
import com.vitorpamplona.amethyst.ui.navigation.INav import com.vitorpamplona.amethyst.ui.navigation.INav
import com.vitorpamplona.amethyst.ui.navigation.routeFor import com.vitorpamplona.amethyst.ui.navigation.routeFor
import com.vitorpamplona.amethyst.ui.note.CloseIcon import com.vitorpamplona.amethyst.ui.note.CloseIcon
import com.vitorpamplona.amethyst.ui.note.MultiUserErrorMessageDialog
import com.vitorpamplona.amethyst.ui.note.ObserveZapIcon import com.vitorpamplona.amethyst.ui.note.ObserveZapIcon
import com.vitorpamplona.amethyst.ui.note.PayViaIntentDialog import com.vitorpamplona.amethyst.ui.note.PayViaIntentDialog
import com.vitorpamplona.amethyst.ui.note.UserBasedErrorMessageViewModel
import com.vitorpamplona.amethyst.ui.note.ZapAmountChoicePopup import com.vitorpamplona.amethyst.ui.note.ZapAmountChoicePopup
import com.vitorpamplona.amethyst.ui.note.ZapIcon import com.vitorpamplona.amethyst.ui.note.ZapIcon
import com.vitorpamplona.amethyst.ui.note.ZappedIcon import com.vitorpamplona.amethyst.ui.note.ZappedIcon
@ -293,7 +290,6 @@ fun ZapDonationButton(
nav: INav, nav: INav,
) { ) {
var wantsToZap by remember { mutableStateOf<ImmutableList<Long>?>(null) } var wantsToZap by remember { mutableStateOf<ImmutableList<Long>?>(null) }
val errorViewModel: UserBasedErrorMessageViewModel = viewModel()
var wantsToPay by var wantsToPay by
remember(baseNote) { remember(baseNote) {
mutableStateOf<ImmutableList<ZapPaymentHandler.Payable>>( mutableStateOf<ImmutableList<ZapPaymentHandler.Payable>>(
@ -320,7 +316,7 @@ fun ZapDonationButton(
onError = { _, message, toUser -> onError = { _, message, toUser ->
scope.launch { scope.launch {
zappingProgress = 0f zappingProgress = 0f
errorViewModel.add(message, toUser) accountViewModel.toastManager.toast(R.string.error_dialog_zap_error, message, toUser)
} }
}, },
onPayViaIntent = { wantsToPay = it }, onPayViaIntent = { wantsToPay = it },
@ -344,7 +340,7 @@ fun ZapDonationButton(
onError = { _, message, user -> onError = { _, message, user ->
scope.launch { scope.launch {
zappingProgress = 0f zappingProgress = 0f
errorViewModel.add(message, user) accountViewModel.toastManager.toast(R.string.error_dialog_zap_error, message, user)
} }
}, },
onProgress = { onProgress = {
@ -354,13 +350,6 @@ fun ZapDonationButton(
) )
} }
MultiUserErrorMessageDialog(
title = stringRes(id = R.string.error_dialog_zap_error),
model = errorViewModel,
accountViewModel = accountViewModel,
nav = nav,
)
if (wantsToPay.isNotEmpty()) { if (wantsToPay.isNotEmpty()) {
PayViaIntentDialog( PayViaIntentDialog(
payingInvoices = wantsToPay, payingInvoices = wantsToPay,
@ -370,12 +359,12 @@ fun ZapDonationButton(
wantsToPay = persistentListOf() wantsToPay = persistentListOf()
scope.launch { scope.launch {
zappingProgress = 0f zappingProgress = 0f
errorViewModel.add(it) accountViewModel.toastManager.toast(R.string.error_dialog_zap_error, it)
} }
}, },
justShowError = { justShowError = {
scope.launch { scope.launch {
errorViewModel.add(it) accountViewModel.toastManager.toast(R.string.error_dialog_zap_error, it)
} }
}, },
) )
@ -441,7 +430,7 @@ fun customZapClick(
onPayViaIntent: (ImmutableList<ZapPaymentHandler.Payable>) -> Unit, onPayViaIntent: (ImmutableList<ZapPaymentHandler.Payable>) -> Unit,
) { ) {
if (baseNote.isDraft()) { if (baseNote.isDraft()) {
accountViewModel.toast( accountViewModel.toastManager.toast(
R.string.draft_note, R.string.draft_note,
R.string.it_s_not_possible_to_zap_to_a_draft_note, R.string.it_s_not_possible_to_zap_to_a_draft_note,
) )
@ -451,12 +440,12 @@ fun customZapClick(
val choices = accountViewModel.zapAmountChoices() val choices = accountViewModel.zapAmountChoices()
if (choices.isEmpty()) { if (choices.isEmpty()) {
accountViewModel.toast( accountViewModel.toastManager.toast(
stringRes(context, R.string.error_dialog_zap_error), stringRes(context, R.string.error_dialog_zap_error),
stringRes(context, R.string.no_zap_amount_setup_long_press_to_change), stringRes(context, R.string.no_zap_amount_setup_long_press_to_change),
) )
} else if (!accountViewModel.isWriteable()) { } else if (!accountViewModel.isWriteable()) {
accountViewModel.toast( accountViewModel.toastManager.toast(
stringRes(context, R.string.error_dialog_zap_error), stringRes(context, R.string.error_dialog_zap_error),
stringRes(context, R.string.login_with_a_private_key_to_be_able_to_send_zaps), stringRes(context, R.string.login_with_a_private_key_to_be_able_to_send_zaps),
) )

View File

@ -221,7 +221,7 @@ fun DisplayFileList(
ContextCompat.startActivity(context, intent, null) ContextCompat.startActivity(context, intent, null)
} catch (e: Exception) { } catch (e: Exception) {
if (e is CancellationException) throw e if (e is CancellationException) throw e
accountViewModel.toast(R.string.torrent_failure, R.string.torrent_no_apps) accountViewModel.toastManager.toast(R.string.torrent_failure, R.string.torrent_no_apps)
} }
}, Modifier.size(Size30dp)) { }, Modifier.size(Size30dp)) {
DownloadForOfflineIcon(Size20dp, MaterialTheme.colorScheme.onBackground) DownloadForOfflineIcon(Size20dp, MaterialTheme.colorScheme.onBackground)

View File

@ -156,7 +156,7 @@ private fun ListenToExternalSignerIfNeeded(accountViewModel: AccountViewModel) {
contract = ActivityResultContracts.StartActivityForResult(), contract = ActivityResultContracts.StartActivityForResult(),
onResult = { result -> onResult = { result ->
if (result.resultCode != Activity.RESULT_OK) { if (result.resultCode != Activity.RESULT_OK) {
accountViewModel.toast( accountViewModel.toastManager.toast(
R.string.sign_request_rejected, R.string.sign_request_rejected,
R.string.sign_request_rejected_description, R.string.sign_request_rejected_description,
) )
@ -183,7 +183,7 @@ private fun ListenToExternalSignerIfNeeded(accountViewModel: AccountViewModel) {
} catch (e: Exception) { } catch (e: Exception) {
if (e is CancellationException) throw e if (e is CancellationException) throw e
Log.e("Signer", "Error opening Signer app", e) Log.e("Signer", "Error opening Signer app", e)
accountViewModel.toast( accountViewModel.toastManager.toast(
R.string.error_opening_external_signer, R.string.error_opening_external_signer,
R.string.error_opening_external_signer_description, R.string.error_opening_external_signer_description,
) )
@ -203,7 +203,7 @@ private fun ListenToExternalSignerIfNeeded(accountViewModel: AccountViewModel) {
} catch (e: Exception) { } catch (e: Exception) {
if (e is CancellationException) throw e if (e is CancellationException) throw e
Log.e("Signer", "Error opening Signer app", e) Log.e("Signer", "Error opening Signer app", e)
accountViewModel.toast( accountViewModel.toastManager.toast(
R.string.error_opening_external_signer, R.string.error_opening_external_signer,
R.string.error_opening_external_signer_description, R.string.error_opening_external_signer_description,
) )

View File

@ -321,7 +321,7 @@ private fun NSecCopyButton(accountViewModel: AccountViewModel) {
context = context, context = context,
keyguardLauncher = keyguardLauncher, keyguardLauncher = keyguardLauncher,
onApproved = { copyNSec(context, scope, accountViewModel.account, clipboardManager) }, onApproved = { copyNSec(context, scope, accountViewModel.account, clipboardManager) },
onError = { title, message -> accountViewModel.toast(title, message) }, onError = { title, message -> accountViewModel.toastManager.toast(title, message) },
) )
}, },
shape = ButtonBorder, shape = ButtonBorder,
@ -371,7 +371,7 @@ private fun EncryptNSecCopyButton(
context = context, context = context,
keyguardLauncher = keyguardLauncher, keyguardLauncher = keyguardLauncher,
onApproved = { encryptCopyNSec(password, context, scope, accountViewModel, clipboardManager) }, onApproved = { encryptCopyNSec(password, context, scope, accountViewModel, clipboardManager) },
onError = { title, message -> accountViewModel.toast(title, message) }, onError = { title, message -> accountViewModel.toastManager.toast(title, message) },
) )
}, },
shape = ButtonBorder, shape = ButtonBorder,
@ -496,7 +496,7 @@ private fun QrCodeButtonBase(
context = context, context = context,
keyguardLauncher = keyguardLauncher, keyguardLauncher = keyguardLauncher,
onApproved = { dialogOpen = true }, onApproved = { dialogOpen = true },
onError = { title, message -> accountViewModel.toast(title, message) }, onError = { title, message -> accountViewModel.toastManager.toast(title, message) },
) )
}, },
) { ) {

View File

@ -59,6 +59,7 @@ import com.vitorpamplona.amethyst.service.checkNotInMainThread
import com.vitorpamplona.amethyst.service.lnurl.LightningAddressResolver import com.vitorpamplona.amethyst.service.lnurl.LightningAddressResolver
import com.vitorpamplona.amethyst.ui.actions.Dao import com.vitorpamplona.amethyst.ui.actions.Dao
import com.vitorpamplona.amethyst.ui.components.UrlPreviewState import com.vitorpamplona.amethyst.ui.components.UrlPreviewState
import com.vitorpamplona.amethyst.ui.components.toasts.ToastManager
import com.vitorpamplona.amethyst.ui.feeds.FeedState import com.vitorpamplona.amethyst.ui.feeds.FeedState
import com.vitorpamplona.amethyst.ui.navigation.Route import com.vitorpamplona.amethyst.ui.navigation.Route
import com.vitorpamplona.amethyst.ui.note.ZapAmountCommentNotification import com.vitorpamplona.amethyst.ui.note.ZapAmountCommentNotification
@ -123,8 +124,6 @@ import kotlinx.coroutines.CancellationException
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.Job import kotlinx.coroutines.Job
import kotlinx.coroutines.channels.BufferOverflow
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.StateFlow
@ -137,25 +136,6 @@ import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
@Immutable open class ToastMsg
@Immutable class StringToastMsg(
val title: String,
val msg: String,
) : ToastMsg()
@Immutable class ResourceToastMsg(
val titleResId: Int,
val resourceId: Int,
val params: Array<out String>? = null,
) : ToastMsg()
@Immutable class ThrowableToastMsg(
val titleResId: Int,
val msg: String? = null,
val throwable: Throwable,
) : ToastMsg()
@Stable @Stable
class AccountViewModel( class AccountViewModel(
accountSettings: AccountSettings, accountSettings: AccountSettings,
@ -189,7 +169,7 @@ class AccountViewModel(
val dmRelays: StateFlow<ChatMessageRelayListEvent?> = observeByAuthor(ChatMessageRelayListEvent.KIND, account.signer.pubKey) val dmRelays: StateFlow<ChatMessageRelayListEvent?> = observeByAuthor(ChatMessageRelayListEvent.KIND, account.signer.pubKey)
val searchRelays: StateFlow<SearchRelayListEvent?> = observeByAuthor(SearchRelayListEvent.KIND, account.signer.pubKey) val searchRelays: StateFlow<SearchRelayListEvent?> = observeByAuthor(SearchRelayListEvent.KIND, account.signer.pubKey)
val toasts = MutableSharedFlow<ToastMsg?>(0, 3, onBufferOverflow = BufferOverflow.DROP_OLDEST) val toastManager = ToastManager()
val feedStates = AccountFeedContentStates(this) val feedStates = AccountFeedContentStates(this)
@ -269,40 +249,6 @@ class AccountViewModel(
Route.Notification to notificationHasNewItemsFlow, Route.Notification to notificationHasNewItemsFlow,
) )
fun clearToasts() {
viewModelScope.launch { toasts.emit(null) }
}
fun toast(
title: String,
message: String,
) {
viewModelScope.launch { toasts.emit(StringToastMsg(title, message)) }
}
fun toast(
titleResId: Int,
resourceId: Int,
) {
viewModelScope.launch { toasts.emit(ResourceToastMsg(titleResId, resourceId)) }
}
fun toast(
titleResId: Int,
message: String?,
throwable: Throwable,
) {
viewModelScope.launch { toasts.emit(ThrowableToastMsg(titleResId, message, throwable)) }
}
fun toast(
titleResId: Int,
resourceId: Int,
vararg params: String,
) {
viewModelScope.launch { toasts.emit(ResourceToastMsg(titleResId, resourceId, params)) }
}
fun isWriteable(): Boolean = account.isWriteable() fun isWriteable(): Boolean = account.isWriteable()
fun userProfile(): User = account.userProfile() fun userProfile(): User = account.userProfile()
@ -1406,7 +1352,7 @@ class AccountViewModel(
onMore: () -> Unit, onMore: () -> Unit,
) { ) {
if (baseNote.isDraft()) { if (baseNote.isDraft()) {
toast( toastManager.toast(
R.string.draft_note, R.string.draft_note,
R.string.it_s_not_possible_to_quote_to_a_draft_note, R.string.it_s_not_possible_to_quote_to_a_draft_note,
) )
@ -1420,7 +1366,7 @@ class AccountViewModel(
onMore() onMore()
} }
} else { } else {
toast( toastManager.toast(
R.string.read_only_user, R.string.read_only_user,
R.string.login_with_a_private_key_to_be_able_to_boost_posts, R.string.login_with_a_private_key_to_be_able_to_boost_posts,
) )
@ -1766,6 +1712,7 @@ fun mockAccountViewModel(): AccountViewModel {
KeyPair( KeyPair(
privKey = Hex.decode("0f761f8a5a481e26f06605a1d9b3e9eba7a107d351f43c43a57469b788274499"), privKey = Hex.decode("0f761f8a5a481e26f06605a1d9b3e9eba7a107d351f43c43a57469b788274499"),
pubKey = Hex.decode("989c3734c46abac7ce3ce229971581a5a6ee39cdd6aa7261a55823fa7f8c4799"), pubKey = Hex.decode("989c3734c46abac7ce3ce229971581a5a6ee39cdd6aa7261a55823fa7f8c4799"),
forceReplacePubkey = false,
), ),
), ),
sharedPreferencesViewModel.sharedPrefs, sharedPreferencesViewModel.sharedPrefs,

View File

@ -527,7 +527,7 @@ fun NewPostScreen(
it, it,
accountViewModel.account.settings.defaultFileServer, accountViewModel.account.settings.defaultFileServer,
onAdd = { alt, server, sensitiveContent, mediaQuality -> onAdd = { alt, server, sensitiveContent, mediaQuality ->
postViewModel.upload(alt, if (sensitiveContent) "" else null, mediaQuality, false, server, accountViewModel::toast, context) postViewModel.upload(alt, if (sensitiveContent) "" else null, mediaQuality, false, server, accountViewModel.toastManager::toast, context)
if (server.type != ServerType.NIP95) { if (server.type != ServerType.NIP95) {
accountViewModel.account.settings.changeDefaultFileServer(server) accountViewModel.account.settings.changeDefaultFileServer(server)
} }
@ -558,7 +558,7 @@ fun NewPostScreen(
postViewModel.wantsInvoice = false postViewModel.wantsInvoice = false
}, },
onClose = { postViewModel.wantsInvoice = false }, onClose = { postViewModel.wantsInvoice = false },
onError = { title, message -> accountViewModel.toast(title, message) }, onError = { title, message -> accountViewModel.toastManager.toast(title, message) },
) )
} }
} }

View File

@ -65,7 +65,7 @@ fun RoomChatFileUploadDialog(
}, },
upload = { upload = {
channelScreenModel.upload( channelScreenModel.upload(
onError = accountViewModel::toast, onError = accountViewModel.toastManager::toast,
context = context, context = context,
onceUploaded = onUpload, onceUploaded = onUpload,
) )

View File

@ -126,7 +126,7 @@ fun ChannelFileUploadDialog(
}, },
upload = { upload = {
channelScreenModel.upload( channelScreenModel.upload(
onError = accountViewModel::toast, onError = accountViewModel.toastManager::toast,
context = context, context = context,
onceUploaded = onUpload, onceUploaded = onUpload,
) )

View File

@ -70,11 +70,9 @@ import com.vitorpamplona.amethyst.ui.feeds.FeedEmpty
import com.vitorpamplona.amethyst.ui.feeds.RefresheableBox import com.vitorpamplona.amethyst.ui.feeds.RefresheableBox
import com.vitorpamplona.amethyst.ui.navigation.INav import com.vitorpamplona.amethyst.ui.navigation.INav
import com.vitorpamplona.amethyst.ui.note.DVMCard import com.vitorpamplona.amethyst.ui.note.DVMCard
import com.vitorpamplona.amethyst.ui.note.MultiUserErrorMessageDialog
import com.vitorpamplona.amethyst.ui.note.NoteAuthorPicture import com.vitorpamplona.amethyst.ui.note.NoteAuthorPicture
import com.vitorpamplona.amethyst.ui.note.ObserveZapIcon import com.vitorpamplona.amethyst.ui.note.ObserveZapIcon
import com.vitorpamplona.amethyst.ui.note.PayViaIntentDialog import com.vitorpamplona.amethyst.ui.note.PayViaIntentDialog
import com.vitorpamplona.amethyst.ui.note.UserBasedErrorMessageViewModel
import com.vitorpamplona.amethyst.ui.note.WatchNoteEvent import com.vitorpamplona.amethyst.ui.note.WatchNoteEvent
import com.vitorpamplona.amethyst.ui.note.ZapAmountChoicePopup import com.vitorpamplona.amethyst.ui.note.ZapAmountChoicePopup
import com.vitorpamplona.amethyst.ui.note.ZapIcon import com.vitorpamplona.amethyst.ui.note.ZapIcon
@ -442,7 +440,6 @@ fun ZapDVMButton(
val noteAuthor = baseNote.author ?: return val noteAuthor = baseNote.author ?: return
var wantsToZap by remember { mutableStateOf<List<Long>?>(null) } var wantsToZap by remember { mutableStateOf<List<Long>?>(null) }
val errorViewModel: UserBasedErrorMessageViewModel = viewModel()
var wantsToPay by var wantsToPay by
remember(baseNote) { remember(baseNote) {
mutableStateOf<ImmutableList<ZapPaymentHandler.Payable>>( mutableStateOf<ImmutableList<ZapPaymentHandler.Payable>>(
@ -472,7 +469,7 @@ fun ZapDVMButton(
onError = { _, message, toUser -> onError = { _, message, toUser ->
scope.launch { scope.launch {
zappingProgress = 0f zappingProgress = 0f
errorViewModel.add(message, toUser) accountViewModel.toastManager.toast(R.string.error_dialog_zap_error, message, toUser)
} }
}, },
onPayViaIntent = { wantsToPay = it }, onPayViaIntent = { wantsToPay = it },
@ -496,7 +493,7 @@ fun ZapDVMButton(
onError = { _, message, user -> onError = { _, message, user ->
scope.launch { scope.launch {
zappingProgress = 0f zappingProgress = 0f
errorViewModel.add(message, user) accountViewModel.toastManager.toast(R.string.error_dialog_zap_error, message, user)
} }
}, },
onProgress = { onProgress = {
@ -506,13 +503,6 @@ fun ZapDVMButton(
) )
} }
MultiUserErrorMessageDialog(
title = stringRes(id = R.string.error_dialog_zap_error),
model = errorViewModel,
accountViewModel,
nav,
)
if (wantsToPay.isNotEmpty()) { if (wantsToPay.isNotEmpty()) {
PayViaIntentDialog( PayViaIntentDialog(
payingInvoices = wantsToPay, payingInvoices = wantsToPay,
@ -522,12 +512,12 @@ fun ZapDVMButton(
wantsToPay = persistentListOf() wantsToPay = persistentListOf()
scope.launch { scope.launch {
zappingProgress = 0f zappingProgress = 0f
errorViewModel.add(it) accountViewModel.toastManager.toast(R.string.error_dialog_zap_error, it)
} }
}, },
justShowError = { justShowError = {
scope.launch { scope.launch {
errorViewModel.add(it) accountViewModel.toastManager.toast(R.string.error_dialog_zap_error, it)
} }
}, },
) )

View File

@ -208,7 +208,7 @@ fun GeoHashActionOptions(
if (isFollowingTag) { if (isFollowingTag) {
UnfollowButton { UnfollowButton {
if (!accountViewModel.isWriteable()) { if (!accountViewModel.isWriteable()) {
accountViewModel.toast( accountViewModel.toastManager.toast(
R.string.read_only_user, R.string.read_only_user,
R.string.login_with_a_private_key_to_be_able_to_unfollow, R.string.login_with_a_private_key_to_be_able_to_unfollow,
) )
@ -219,7 +219,7 @@ fun GeoHashActionOptions(
} else { } else {
FollowButton { FollowButton {
if (!accountViewModel.isWriteable()) { if (!accountViewModel.isWriteable()) {
accountViewModel.toast( accountViewModel.toastManager.toast(
R.string.read_only_user, R.string.read_only_user,
R.string.login_with_a_private_key_to_be_able_to_follow, R.string.login_with_a_private_key_to_be_able_to_follow,
) )

View File

@ -187,7 +187,7 @@ fun HashtagActionOptions(
if (isFollowingTag) { if (isFollowingTag) {
UnfollowButton { UnfollowButton {
if (!accountViewModel.isWriteable()) { if (!accountViewModel.isWriteable()) {
accountViewModel.toast( accountViewModel.toastManager.toast(
R.string.read_only_user, R.string.read_only_user,
R.string.login_with_a_private_key_to_be_able_to_unfollow, R.string.login_with_a_private_key_to_be_able_to_unfollow,
) )
@ -198,7 +198,7 @@ fun HashtagActionOptions(
} else { } else {
FollowButton { FollowButton {
if (!accountViewModel.isWriteable()) { if (!accountViewModel.isWriteable()) {
accountViewModel.toast( accountViewModel.toastManager.toast(
R.string.read_only_user, R.string.read_only_user,
R.string.login_with_a_private_key_to_be_able_to_follow, R.string.login_with_a_private_key_to_be_able_to_follow,
) )

View File

@ -56,7 +56,7 @@ fun DisplayFollowUnfollowButton(
if (isLoggedInFollowingUser) { if (isLoggedInFollowingUser) {
UnfollowButton { UnfollowButton {
if (!accountViewModel.isWriteable()) { if (!accountViewModel.isWriteable()) {
accountViewModel.toast( accountViewModel.toastManager.toast(
R.string.read_only_user, R.string.read_only_user,
R.string.login_with_a_private_key_to_be_able_to_unfollow, R.string.login_with_a_private_key_to_be_able_to_unfollow,
) )
@ -68,7 +68,7 @@ fun DisplayFollowUnfollowButton(
if (isUserFollowingLoggedIn) { if (isUserFollowingLoggedIn) {
FollowButton(R.string.follow_back) { FollowButton(R.string.follow_back) {
if (!accountViewModel.isWriteable()) { if (!accountViewModel.isWriteable()) {
accountViewModel.toast( accountViewModel.toastManager.toast(
R.string.read_only_user, R.string.read_only_user,
R.string.login_with_a_private_key_to_be_able_to_follow, R.string.login_with_a_private_key_to_be_able_to_follow,
) )
@ -79,7 +79,7 @@ fun DisplayFollowUnfollowButton(
} else { } else {
FollowButton(R.string.follow) { FollowButton(R.string.follow) {
if (!accountViewModel.isWriteable()) { if (!accountViewModel.isWriteable()) {
accountViewModel.toast( accountViewModel.toastManager.toast(
R.string.read_only_user, R.string.read_only_user,
R.string.login_with_a_private_key_to_be_able_to_follow, R.string.login_with_a_private_key_to_be_able_to_follow,
) )

View File

@ -130,7 +130,7 @@ fun DisplayLNAddress(
} }
}, },
onClose = { zapExpanded = false }, onClose = { zapExpanded = false },
onError = { title, message -> accountViewModel.toast(title, message) }, onError = { title, message -> accountViewModel.toastManager.toast(title, message) },
) )
} }
} }

View File

@ -331,7 +331,7 @@ fun MutedWordActionOptions(
if (isMutedWord == true) { if (isMutedWord == true) {
ShowWordButton { ShowWordButton {
if (!accountViewModel.isWriteable()) { if (!accountViewModel.isWriteable()) {
accountViewModel.toast( accountViewModel.toastManager.toast(
R.string.read_only_user, R.string.read_only_user,
R.string.login_with_a_private_key_to_be_able_to_show_word, R.string.login_with_a_private_key_to_be_able_to_show_word,
) )
@ -342,7 +342,7 @@ fun MutedWordActionOptions(
} else { } else {
HideWordButton { HideWordButton {
if (!accountViewModel.isWriteable()) { if (!accountViewModel.isWriteable()) {
accountViewModel.toast( accountViewModel.toastManager.toast(
R.string.read_only_user, R.string.read_only_user,
R.string.login_with_a_private_key_to_be_able_to_hide_word, R.string.login_with_a_private_key_to_be_able_to_hide_word,
) )
@ -393,7 +393,7 @@ private fun hideIfWritable(
currentWordToAdd: MutableState<String>, currentWordToAdd: MutableState<String>,
) { ) {
if (!accountViewModel.isWriteable()) { if (!accountViewModel.isWriteable()) {
accountViewModel.toast( accountViewModel.toastManager.toast(
R.string.read_only_user, R.string.read_only_user,
R.string.login_with_a_private_key_to_be_able_to_hide_word, R.string.login_with_a_private_key_to_be_able_to_hide_word,
) )

View File

@ -25,6 +25,7 @@ import com.vitorpamplona.quartz.nip01Core.core.toHexKey
class KeyPair( class KeyPair(
privKey: ByteArray? = null, privKey: ByteArray? = null,
pubKey: ByteArray? = null, pubKey: ByteArray? = null,
forceReplacePubkey: Boolean = true,
) { ) {
val privKey: ByteArray? val privKey: ByteArray?
val pubKey: ByteArray val pubKey: ByteArray
@ -44,7 +45,11 @@ class KeyPair(
} else { } else {
// as private key is provided, ignore the public key and set keys according to private key // as private key is provided, ignore the public key and set keys according to private key
this.privKey = privKey this.privKey = privKey
this.pubKey = Nip01.pubKeyCreate(privKey) if (pubKey == null || forceReplacePubkey) {
this.pubKey = Nip01.pubKeyCreate(privKey)
} else {
this.pubKey = pubKey
}
} }
} }