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
+4
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" />
@@ -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) },
)
}
}
@@ -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)
@@ -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,
@@ -218,7 +218,7 @@ fun RelaySelectionDialog(
)
}
accountViewModel.toast(
accountViewModel.toastManager.toast(
stringRes(context, R.string.unable_to_download_relay_document),
msg,
)
@@ -103,7 +103,7 @@ fun BasicRelaySetupInfoDialog(
)
}
accountViewModel.toast(
accountViewModel.toastManager.toast(
stringRes(context, R.string.unable_to_download_relay_document),
msg,
)
@@ -280,7 +280,7 @@ fun LoadRelayInfo(
)
}
accountViewModel.toast(
accountViewModel.toastManager.toast(
stringRes(context, R.string.unable_to_download_relay_document),
msg,
)
@@ -105,7 +105,7 @@ fun Kind3RelaySetupInfoProposalDialog(
)
}
accountViewModel.toast(
accountViewModel.toastManager.toast(
stringRes(context, R.string.unable_to_download_relay_document),
msg,
)
@@ -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,
)
@@ -116,7 +116,7 @@ fun CashuPreview(
CashuPreviewNew(
it,
accountViewModel::meltCashu,
accountViewModel::toast,
accountViewModel.toastManager::toast,
)
}
}
@@ -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)
},
)
}
@@ -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)
},
)
}
@@ -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)
}
}
@@ -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)
}
}
}
@@ -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()
@@ -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()
@@ -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()
@@ -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) })
}
}
}
@@ -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
@@ -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,107 +75,43 @@ 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)
val errorState by model.errors.collectAsStateWithLifecycle()
LazyColumn {
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
fun ErrorRow(
errorState: UserBasedErrorMessage,
@@ -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,
@@ -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?,
)
@@ -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))
}
},
)
}
@@ -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,
@@ -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,
)
@@ -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,
)
@@ -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,
)
@@ -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 ->
@@ -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
@@ -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
@@ -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,
)
@@ -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,
@@ -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),
)
@@ -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)
@@ -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,
)
@@ -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) },
)
},
) {
@@ -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,
@@ -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) },
)
}
}
@@ -65,7 +65,7 @@ fun RoomChatFileUploadDialog(
},
upload = {
channelScreenModel.upload(
onError = accountViewModel::toast,
onError = accountViewModel.toastManager::toast,
context = context,
onceUploaded = onUpload,
)
@@ -126,7 +126,7 @@ fun ChannelFileUploadDialog(
},
upload = {
channelScreenModel.upload(
onError = accountViewModel::toast,
onError = accountViewModel.toastManager::toast,
context = context,
onceUploaded = onUpload,
)
@@ -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)
}
},
)
@@ -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,
)
@@ -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,
)
@@ -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,
)
@@ -130,7 +130,7 @@ fun DisplayLNAddress(
}
},
onClose = { zapExpanded = false },
onError = { title, message -> accountViewModel.toast(title, message) },
onError = { title, message -> accountViewModel.toastManager.toast(title, message) },
)
}
}
@@ -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,
)
@@ -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
this.pubKey = Nip01.pubKeyCreate(privKey)
if (pubKey == null || forceReplacePubkey) {
this.pubKey = Nip01.pubKeyCreate(privKey)
} else {
this.pubKey = pubKey
}
}
}