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="previewFile" value="true" />
</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">
<option name="composableFile" value="true" />
<option name="previewFile" value="true" />

View File

@ -331,7 +331,7 @@ fun EditPostView(
it,
accountViewModel.account.settings.defaultFileServer,
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) {
accountViewModel.account.settings.changeDefaultFileServer(server)
}
@ -365,7 +365,7 @@ fun EditPostView(
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(
onPost = {
postViewModel.upload(context, relayList, onClose, accountViewModel::toast)
postViewModel.upload(context, relayList, onClose, accountViewModel.toastManager::toast)
postViewModel.selectedServer?.let {
if (it.type != ServerType.NIP95) {
account.settings.changeDefaultFileServer(it)

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1235,10 +1235,10 @@ private fun saveMediaToGallery(
mimeType = mimeType,
localContext = localContext,
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 = {
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,
localContext,
onSuccess = {
accountViewModel.toast(success, success)
accountViewModel.toastManager.toast(success, success)
},
onError = {
accountViewModel.toast(failure, null, it)
accountViewModel.toastManager.toast(failure, null, it)
},
)
} else if (content is MediaPreloadedContent) {
@ -325,10 +325,10 @@ private fun saveMediaToGallery(
content.mimeType,
localContext,
onSuccess = {
accountViewModel.toast(success, success)
accountViewModel.toastManager.toast(success, success)
},
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
if (n19 != null) {
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
* 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.lifecycle.compose.collectAsStateWithLifecycle
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.ResourceToastMsg
import com.vitorpamplona.amethyst.ui.screen.loggedIn.StringToastMsg
import com.vitorpamplona.amethyst.ui.screen.loggedIn.ThrowableToastMsg
import com.vitorpamplona.amethyst.ui.stringRes
@Composable
fun DisplayErrorMessages(accountViewModel: AccountViewModel) {
val openDialogMsg = accountViewModel.toasts.collectAsStateWithLifecycle(null)
fun DisplayErrorMessages(
toastManager: ToastManager,
accountViewModel: AccountViewModel,
nav: INav,
) {
val openDialogMsg = toastManager.toasts.collectAsStateWithLifecycle(null)
openDialogMsg.value?.let { obj ->
when (obj) {
@ -41,14 +45,14 @@ fun DisplayErrorMessages(accountViewModel: AccountViewModel) {
stringRes(obj.titleResId),
stringRes(obj.resourceId, *obj.params),
) {
accountViewModel.clearToasts()
toastManager.clearToasts()
}
} else {
InformationDialog(
stringRes(obj.titleResId),
stringRes(obj.resourceId),
) {
accountViewModel.clearToasts()
toastManager.clearToasts()
}
}
@ -57,7 +61,7 @@ fun DisplayErrorMessages(accountViewModel: AccountViewModel) {
obj.title,
obj.msg,
) {
accountViewModel.clearToasts()
toastManager.clearToasts()
}
is ThrowableToastMsg ->
@ -66,8 +70,10 @@ fun DisplayErrorMessages(accountViewModel: AccountViewModel) {
obj.msg,
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
* 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.Column
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
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.itemsIndexed
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.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.Stable
import androidx.compose.runtime.getValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.tooling.preview.Preview
import androidx.lifecycle.ViewModel
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import androidx.lifecycle.viewmodel.compose.viewModel
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.navigation.routeToMessage
import com.vitorpamplona.amethyst.ui.note.UserPicture
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.DividerThickness
import com.vitorpamplona.amethyst.ui.theme.Size16dp
import com.vitorpamplona.amethyst.ui.theme.Size20Modifier
import com.vitorpamplona.amethyst.ui.theme.Size30Modifier
import com.vitorpamplona.amethyst.ui.theme.Size30dp
import com.vitorpamplona.amethyst.ui.theme.Size40dp
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.ThemeComparisonColumn
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.withContext
@Composable
@Preview
fun MultiUserErrorMessageContentPreview() {
fun ErrorListPreview() {
val accountViewModel = mockAccountViewModel()
val nav = EmptyNav
var user1: User? = null
var user2: User? = null
@ -88,82 +75,33 @@ fun MultiUserErrorMessageContentPreview() {
runBlocking {
withContext(Dispatchers.IO) {
user1 = LocalCache.getOrCreateUser("aaabccaabbccaabbcc")
user2 = LocalCache.getOrCreateUser("bbbccabbbccabbbcca")
user3 = LocalCache.getOrCreateUser("ccaadaccaadaccaada")
user1 = LocalCache.getOrCreateUser("aaabccaabbccaabbccabdd")
user2 = LocalCache.getOrCreateUser("bbbccabbbccabbbccaabdd")
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("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 {
MultiUserErrorMessageDialogInner(
title = "Couldn't not zap",
ErrorList(
model = model,
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
fun MultiUserErrorMessageDialog(
title: String,
model: UserBasedErrorMessageViewModel,
fun ErrorList(
model: MultiErrorToastMsg,
accountViewModel: AccountViewModel,
nav: INav,
) {
val hasErrors by model.hasErrors.collectAsStateWithLifecycle(false)
if (hasErrors) {
MultiUserErrorMessageDialogInner(title, model, accountViewModel, nav)
}
}
@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())
val errorState by model.errors.collectAsStateWithLifecycle()
LazyColumn {
itemsIndexed(errorState) { index, it ->
ErrorRow(it, accountViewModel, nav)
@ -172,21 +110,6 @@ fun MultiUserErrorMessageDialogInner(
}
}
}
},
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
@ -201,7 +124,7 @@ fun ErrorRow(
) {
errorState.user?.let {
Column(Modifier.width(Size40dp), horizontalAlignment = Alignment.Start) {
UserPicture(errorState.user, Size30dp, Modifier, accountViewModel, nav)
UserPicture(it, Size30dp, Modifier, accountViewModel, nav)
Spacer(StdVertSpacer)
IconButton(
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.actions.NewUserMetadataScreen
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.toasts.DisplayErrorMessages
import com.vitorpamplona.amethyst.ui.screen.AccountStateViewModel
import com.vitorpamplona.amethyst.ui.screen.SharedPreferencesViewModel
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountSwitcherAndLeftDrawerLayout
@ -354,7 +354,7 @@ fun AppNavigation(
NavigateIfIntentRequested(nav, accountViewModel, accountStateViewModel)
DisplayErrorMessages(accountViewModel)
DisplayErrorMessages(accountViewModel.toastManager, accountViewModel, nav)
DisplayNotifyMessages(accountViewModel, nav)
}
@ -437,7 +437,7 @@ private fun NavigateIfIntentRequested(
actionableNextPage = null
} else {
accountViewModel.toast(
accountViewModel.toastManager.toast(
R.string.invalid_nip19_uri,
R.string.invalid_nip19_uri_description,
intentNextPage,
@ -492,7 +492,7 @@ private fun NavigateIfIntentRequested(
} else {
scope.launch {
delay(1000)
accountViewModel.toast(
accountViewModel.toastManager.toast(
R.string.invalid_nip19_uri,
R.string.invalid_nip19_uri_description,
uri,

View File

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

View File

@ -76,11 +76,11 @@ import com.vitorpamplona.amethyst.model.Note
import com.vitorpamplona.amethyst.model.User
import com.vitorpamplona.amethyst.service.ZapPaymentHandler
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.INav
import com.vitorpamplona.amethyst.ui.navigation.routeToMessage
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.mockVitorAccountViewModel
import com.vitorpamplona.amethyst.ui.stringRes
@ -530,23 +530,23 @@ fun ZapVote(
indication = ripple24dp,
onClick = {
if (!accountViewModel.isWriteable()) {
accountViewModel.toast(
accountViewModel.toastManager.toast(
R.string.read_only_user,
R.string.login_with_a_private_key_to_be_able_to_send_zaps,
)
} else if (pollViewModel.isPollClosed()) {
accountViewModel.toast(
accountViewModel.toastManager.toast(
R.string.poll_unable_to_vote,
R.string.poll_is_closed_explainer,
)
} else if (isLoggedUser) {
accountViewModel.toast(
accountViewModel.toastManager.toast(
R.string.poll_unable_to_vote,
R.string.poll_author_no_vote,
)
} else if (pollViewModel.isVoteAmountAtomic() && poolOption.zappedByLoggedIn.value) {
// 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.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.distinctUntilChanged
import androidx.lifecycle.map
import androidx.lifecycle.viewmodel.compose.viewModel
import com.vitorpamplona.amethyst.R
import com.vitorpamplona.amethyst.commons.emojicoder.EmojiCoder
import com.vitorpamplona.amethyst.model.FeatureSetType
import com.vitorpamplona.amethyst.model.Note
import com.vitorpamplona.amethyst.model.User
import com.vitorpamplona.amethyst.service.NostrUserProfileDataSource.user
import com.vitorpamplona.amethyst.service.ZapPaymentHandler
import com.vitorpamplona.amethyst.ui.actions.CrossfadeIfEnabled
import com.vitorpamplona.amethyst.ui.components.AnimatedBorderTextCornerRadius
@ -662,7 +662,7 @@ fun ReplyReaction(
modifier = iconSizeModifier,
onClick = {
if (baseNote.isDraft()) {
accountViewModel.toast(
accountViewModel.toastManager.toast(
R.string.draft_note,
R.string.it_s_not_possible_to_reply_to_a_draft_note,
)
@ -670,7 +670,7 @@ fun ReplyReaction(
if (accountViewModel.isWriteable()) {
onPress()
} else {
accountViewModel.toast(
accountViewModel.toastManager.toast(
R.string.read_only_user,
R.string.login_with_a_private_key_to_be_able_to_reply,
)
@ -986,7 +986,7 @@ private fun likeClick(
onWantsToSignReaction: () -> Unit,
) {
if (baseNote.isDraft()) {
accountViewModel.toast(
accountViewModel.toastManager.toast(
R.string.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()
if (choices.isEmpty()) {
accountViewModel.toast(
accountViewModel.toastManager.toast(
R.string.no_reactions_setup,
R.string.no_reaction_type_setup_long_press_to_change,
)
} else if (!accountViewModel.isWriteable()) {
accountViewModel.toast(
accountViewModel.toastManager.toast(
R.string.read_only_user,
R.string.login_with_a_private_key_to_like_posts,
)
@ -1025,7 +1025,6 @@ fun ZapReaction(
var wantsToZap by remember { mutableStateOf(false) }
var wantsToChangeZapAmount by remember { mutableStateOf(false) }
var wantsToSetCustomZap by remember { mutableStateOf(false) }
val errorViewModel: UserBasedErrorMessageViewModel = viewModel()
var wantsToPay by
remember(baseNote) {
mutableStateOf<ImmutableList<ZapPaymentHandler.Payable>>(
@ -1060,7 +1059,7 @@ fun ZapReaction(
onError = { _, message, user ->
scope.launch {
zappingProgress = 0f
errorViewModel.add(message, user)
accountViewModel.toastManager.toast(R.string.error_dialog_zap_error, message, user)
}
},
onPayViaIntent = { wantsToPay = it },
@ -1089,7 +1088,7 @@ fun ZapReaction(
onError = { _, message, user ->
scope.launch {
zappingProgress = 0f
errorViewModel.add(message, user)
accountViewModel.toastManager.toast(R.string.error_dialog_zap_error, message, user)
}
},
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) {
UpdateZapAmountDialog(
onClose = { wantsToChangeZapAmount = false },
@ -1120,12 +1112,12 @@ fun ZapReaction(
wantsToPay = persistentListOf()
scope.launch {
zappingProgress = 0f
errorViewModel.add(it)
accountViewModel.toastManager.toast(R.string.error_dialog_zap_error, it)
}
},
justShowError = {
scope.launch {
errorViewModel.add(it)
accountViewModel.toastManager.toast(R.string.error_dialog_zap_error, it)
}
},
)
@ -1137,7 +1129,7 @@ fun ZapReaction(
onError = { _, message, user ->
scope.launch {
zappingProgress = 0f
errorViewModel.add(message, user)
accountViewModel.toastManager.toast(R.string.error_dialog_zap_error, message, user)
}
},
onProgress = { scope.launch(Dispatchers.Main) { zappingProgress = it } },
@ -1191,7 +1183,7 @@ fun zapClick(
onPayViaIntent: (ImmutableList<ZapPaymentHandler.Payable>) -> Unit,
) {
if (baseNote.isDraft()) {
accountViewModel.toast(
accountViewModel.toastManager.toast(
R.string.draft_note,
R.string.it_s_not_possible_to_zap_to_a_draft_note,
)
@ -1201,12 +1193,12 @@ fun zapClick(
val choices = accountViewModel.zapAmountChoices()
if (choices.isEmpty()) {
accountViewModel.toast(
accountViewModel.toastManager.toast(
R.string.error_dialog_zap_error,
R.string.no_zap_amount_setup_long_press_to_change,
)
} else if (!accountViewModel.isWriteable()) {
accountViewModel.toast(
accountViewModel.toastManager.toast(
R.string.error_dialog_zap_error,
R.string.login_with_a_private_key_to_be_able_to_send_zaps,
)

View File

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

View File

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

View File

@ -66,6 +66,7 @@ import com.vitorpamplona.amethyst.model.Note
import com.vitorpamplona.amethyst.model.User
import com.vitorpamplona.amethyst.service.ZapPaymentHandler
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.CloseButton
import com.vitorpamplona.amethyst.ui.screen.loggedIn.TextSpinner

View File

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

View File

@ -75,7 +75,7 @@ fun DisplayOts(
val fullDateTime =
SimpleDateFormat.getDateTimeInstance().format(Date(unixtimestamp * 1000))
accountViewModel.toast(
accountViewModel.toastManager.toast(
R.string.ots_info_title,
R.string.ots_info_description,
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.sp
import androidx.lifecycle.viewmodel.compose.viewModel
import com.vitorpamplona.amethyst.BuildConfig
import com.vitorpamplona.amethyst.R
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.routeFor
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.PayViaIntentDialog
import com.vitorpamplona.amethyst.ui.note.UserBasedErrorMessageViewModel
import com.vitorpamplona.amethyst.ui.note.ZapAmountChoicePopup
import com.vitorpamplona.amethyst.ui.note.ZapIcon
import com.vitorpamplona.amethyst.ui.note.ZappedIcon
@ -293,7 +290,6 @@ fun ZapDonationButton(
nav: INav,
) {
var wantsToZap by remember { mutableStateOf<ImmutableList<Long>?>(null) }
val errorViewModel: UserBasedErrorMessageViewModel = viewModel()
var wantsToPay by
remember(baseNote) {
mutableStateOf<ImmutableList<ZapPaymentHandler.Payable>>(
@ -320,7 +316,7 @@ fun ZapDonationButton(
onError = { _, message, toUser ->
scope.launch {
zappingProgress = 0f
errorViewModel.add(message, toUser)
accountViewModel.toastManager.toast(R.string.error_dialog_zap_error, message, toUser)
}
},
onPayViaIntent = { wantsToPay = it },
@ -344,7 +340,7 @@ fun ZapDonationButton(
onError = { _, message, user ->
scope.launch {
zappingProgress = 0f
errorViewModel.add(message, user)
accountViewModel.toastManager.toast(R.string.error_dialog_zap_error, message, user)
}
},
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()) {
PayViaIntentDialog(
payingInvoices = wantsToPay,
@ -370,12 +359,12 @@ fun ZapDonationButton(
wantsToPay = persistentListOf()
scope.launch {
zappingProgress = 0f
errorViewModel.add(it)
accountViewModel.toastManager.toast(R.string.error_dialog_zap_error, it)
}
},
justShowError = {
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,
) {
if (baseNote.isDraft()) {
accountViewModel.toast(
accountViewModel.toastManager.toast(
R.string.draft_note,
R.string.it_s_not_possible_to_zap_to_a_draft_note,
)
@ -451,12 +440,12 @@ fun customZapClick(
val choices = accountViewModel.zapAmountChoices()
if (choices.isEmpty()) {
accountViewModel.toast(
accountViewModel.toastManager.toast(
stringRes(context, R.string.error_dialog_zap_error),
stringRes(context, R.string.no_zap_amount_setup_long_press_to_change),
)
} else if (!accountViewModel.isWriteable()) {
accountViewModel.toast(
accountViewModel.toastManager.toast(
stringRes(context, R.string.error_dialog_zap_error),
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)
} catch (e: Exception) {
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)) {
DownloadForOfflineIcon(Size20dp, MaterialTheme.colorScheme.onBackground)

View File

@ -156,7 +156,7 @@ private fun ListenToExternalSignerIfNeeded(accountViewModel: AccountViewModel) {
contract = ActivityResultContracts.StartActivityForResult(),
onResult = { result ->
if (result.resultCode != Activity.RESULT_OK) {
accountViewModel.toast(
accountViewModel.toastManager.toast(
R.string.sign_request_rejected,
R.string.sign_request_rejected_description,
)
@ -183,7 +183,7 @@ private fun ListenToExternalSignerIfNeeded(accountViewModel: AccountViewModel) {
} catch (e: Exception) {
if (e is CancellationException) throw 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_description,
)
@ -203,7 +203,7 @@ private fun ListenToExternalSignerIfNeeded(accountViewModel: AccountViewModel) {
} catch (e: Exception) {
if (e is CancellationException) throw 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_description,
)

View File

@ -321,7 +321,7 @@ private fun NSecCopyButton(accountViewModel: AccountViewModel) {
context = context,
keyguardLauncher = keyguardLauncher,
onApproved = { copyNSec(context, scope, accountViewModel.account, clipboardManager) },
onError = { title, message -> accountViewModel.toast(title, message) },
onError = { title, message -> accountViewModel.toastManager.toast(title, message) },
)
},
shape = ButtonBorder,
@ -371,7 +371,7 @@ private fun EncryptNSecCopyButton(
context = context,
keyguardLauncher = keyguardLauncher,
onApproved = { encryptCopyNSec(password, context, scope, accountViewModel, clipboardManager) },
onError = { title, message -> accountViewModel.toast(title, message) },
onError = { title, message -> accountViewModel.toastManager.toast(title, message) },
)
},
shape = ButtonBorder,
@ -496,7 +496,7 @@ private fun QrCodeButtonBase(
context = context,
keyguardLauncher = keyguardLauncher,
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.ui.actions.Dao
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.navigation.Route
import com.vitorpamplona.amethyst.ui.note.ZapAmountCommentNotification
@ -123,8 +124,6 @@ import kotlinx.coroutines.CancellationException
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.Job
import kotlinx.coroutines.channels.BufferOverflow
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.StateFlow
@ -137,25 +136,6 @@ import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.launch
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
class AccountViewModel(
accountSettings: AccountSettings,
@ -189,7 +169,7 @@ class AccountViewModel(
val dmRelays: StateFlow<ChatMessageRelayListEvent?> = observeByAuthor(ChatMessageRelayListEvent.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)
@ -269,40 +249,6 @@ class AccountViewModel(
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 userProfile(): User = account.userProfile()
@ -1406,7 +1352,7 @@ class AccountViewModel(
onMore: () -> Unit,
) {
if (baseNote.isDraft()) {
toast(
toastManager.toast(
R.string.draft_note,
R.string.it_s_not_possible_to_quote_to_a_draft_note,
)
@ -1420,7 +1366,7 @@ class AccountViewModel(
onMore()
}
} else {
toast(
toastManager.toast(
R.string.read_only_user,
R.string.login_with_a_private_key_to_be_able_to_boost_posts,
)
@ -1766,6 +1712,7 @@ fun mockAccountViewModel(): AccountViewModel {
KeyPair(
privKey = Hex.decode("0f761f8a5a481e26f06605a1d9b3e9eba7a107d351f43c43a57469b788274499"),
pubKey = Hex.decode("989c3734c46abac7ce3ce229971581a5a6ee39cdd6aa7261a55823fa7f8c4799"),
forceReplacePubkey = false,
),
),
sharedPreferencesViewModel.sharedPrefs,

View File

@ -527,7 +527,7 @@ fun NewPostScreen(
it,
accountViewModel.account.settings.defaultFileServer,
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) {
accountViewModel.account.settings.changeDefaultFileServer(server)
}
@ -558,7 +558,7 @@ fun NewPostScreen(
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 = {
channelScreenModel.upload(
onError = accountViewModel::toast,
onError = accountViewModel.toastManager::toast,
context = context,
onceUploaded = onUpload,
)

View File

@ -126,7 +126,7 @@ fun ChannelFileUploadDialog(
},
upload = {
channelScreenModel.upload(
onError = accountViewModel::toast,
onError = accountViewModel.toastManager::toast,
context = context,
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.navigation.INav
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.ObserveZapIcon
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.ZapAmountChoicePopup
import com.vitorpamplona.amethyst.ui.note.ZapIcon
@ -442,7 +440,6 @@ fun ZapDVMButton(
val noteAuthor = baseNote.author ?: return
var wantsToZap by remember { mutableStateOf<List<Long>?>(null) }
val errorViewModel: UserBasedErrorMessageViewModel = viewModel()
var wantsToPay by
remember(baseNote) {
mutableStateOf<ImmutableList<ZapPaymentHandler.Payable>>(
@ -472,7 +469,7 @@ fun ZapDVMButton(
onError = { _, message, toUser ->
scope.launch {
zappingProgress = 0f
errorViewModel.add(message, toUser)
accountViewModel.toastManager.toast(R.string.error_dialog_zap_error, message, toUser)
}
},
onPayViaIntent = { wantsToPay = it },
@ -496,7 +493,7 @@ fun ZapDVMButton(
onError = { _, message, user ->
scope.launch {
zappingProgress = 0f
errorViewModel.add(message, user)
accountViewModel.toastManager.toast(R.string.error_dialog_zap_error, message, user)
}
},
onProgress = {
@ -506,13 +503,6 @@ fun ZapDVMButton(
)
}
MultiUserErrorMessageDialog(
title = stringRes(id = R.string.error_dialog_zap_error),
model = errorViewModel,
accountViewModel,
nav,
)
if (wantsToPay.isNotEmpty()) {
PayViaIntentDialog(
payingInvoices = wantsToPay,
@ -522,12 +512,12 @@ fun ZapDVMButton(
wantsToPay = persistentListOf()
scope.launch {
zappingProgress = 0f
errorViewModel.add(it)
accountViewModel.toastManager.toast(R.string.error_dialog_zap_error, it)
}
},
justShowError = {
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) {
UnfollowButton {
if (!accountViewModel.isWriteable()) {
accountViewModel.toast(
accountViewModel.toastManager.toast(
R.string.read_only_user,
R.string.login_with_a_private_key_to_be_able_to_unfollow,
)
@ -219,7 +219,7 @@ fun GeoHashActionOptions(
} else {
FollowButton {
if (!accountViewModel.isWriteable()) {
accountViewModel.toast(
accountViewModel.toastManager.toast(
R.string.read_only_user,
R.string.login_with_a_private_key_to_be_able_to_follow,
)

View File

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

View File

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

View File

@ -130,7 +130,7 @@ fun DisplayLNAddress(
}
},
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) {
ShowWordButton {
if (!accountViewModel.isWriteable()) {
accountViewModel.toast(
accountViewModel.toastManager.toast(
R.string.read_only_user,
R.string.login_with_a_private_key_to_be_able_to_show_word,
)
@ -342,7 +342,7 @@ fun MutedWordActionOptions(
} else {
HideWordButton {
if (!accountViewModel.isWriteable()) {
accountViewModel.toast(
accountViewModel.toastManager.toast(
R.string.read_only_user,
R.string.login_with_a_private_key_to_be_able_to_hide_word,
)
@ -393,7 +393,7 @@ private fun hideIfWritable(
currentWordToAdd: MutableState<String>,
) {
if (!accountViewModel.isWriteable()) {
accountViewModel.toast(
accountViewModel.toastManager.toast(
R.string.read_only_user,
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(
privKey: ByteArray? = null,
pubKey: ByteArray? = null,
forceReplacePubkey: Boolean = true,
) {
val privKey: ByteArray?
val pubKey: ByteArray
@ -44,7 +45,11 @@ class KeyPair(
} else {
// as private key is provided, ignore the public key and set keys according to private key
this.privKey = privKey
if (pubKey == null || forceReplacePubkey) {
this.pubKey = Nip01.pubKeyCreate(privKey)
} else {
this.pubKey = pubKey
}
}
}