mirror of
https://github.com/vitorpamplona/amethyst.git
synced 2026-08-10 08:27:04 +00:00
Merge branch 'vitorpamplona:main' into profiles-list-management
This commit is contained in:
Generated
+6
@@ -0,0 +1,6 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="AndroidProjectSystem">
|
||||
<option name="providerId" value="com.android.tools.idea.GradleProjectSystem" />
|
||||
</component>
|
||||
</project>
|
||||
+4
@@ -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" />
|
||||
|
||||
@@ -142,6 +142,7 @@ class Amethyst : Application() {
|
||||
RECEIVER_EXPORTED,
|
||||
)
|
||||
} else {
|
||||
@Suppress("UnspecifiedRegisterReceiverFlag")
|
||||
registerReceiver(
|
||||
pokeyReceiver,
|
||||
IntentFilter(PokeyReceiver.POKEY_ACTION),
|
||||
|
||||
@@ -150,6 +150,7 @@ import com.vitorpamplona.quartz.nip51Lists.GeneralListEvent
|
||||
import com.vitorpamplona.quartz.nip51Lists.MuteListEvent
|
||||
import com.vitorpamplona.quartz.nip51Lists.PeopleListEvent
|
||||
import com.vitorpamplona.quartz.nip56Reports.ReportEvent
|
||||
import com.vitorpamplona.quartz.nip56Reports.ReportType
|
||||
import com.vitorpamplona.quartz.nip57Zaps.LnZapEvent
|
||||
import com.vitorpamplona.quartz.nip57Zaps.LnZapRequestEvent
|
||||
import com.vitorpamplona.quartz.nip57Zaps.splits.ZapSplitSetup
|
||||
@@ -1618,18 +1619,18 @@ class Account(
|
||||
|
||||
suspend fun report(
|
||||
note: Note,
|
||||
type: ReportEvent.ReportType,
|
||||
type: ReportType,
|
||||
content: String = "",
|
||||
) {
|
||||
if (!isWriteable()) return
|
||||
|
||||
if (note.hasReacted(userProfile(), "⚠️")) {
|
||||
// has already liked this note
|
||||
if (note.hasReport(userProfile(), type)) {
|
||||
// has already reported this note
|
||||
return
|
||||
}
|
||||
|
||||
note.event?.let {
|
||||
ReportEvent.create(it, type, signer, content) {
|
||||
signer.sign(ReportEvent.build(it, type)) {
|
||||
Amethyst.instance.client.send(it)
|
||||
LocalCache.justConsume(it, null)
|
||||
}
|
||||
@@ -1638,7 +1639,7 @@ class Account(
|
||||
|
||||
suspend fun report(
|
||||
user: User,
|
||||
type: ReportEvent.ReportType,
|
||||
type: ReportType,
|
||||
) {
|
||||
if (!isWriteable()) return
|
||||
|
||||
@@ -1647,7 +1648,8 @@ class Account(
|
||||
return
|
||||
}
|
||||
|
||||
ReportEvent.create(user.pubkeyHex, type, signer) {
|
||||
val template = ReportEvent.build(user.pubkeyHex, type)
|
||||
signer.sign(template) {
|
||||
Amethyst.instance.client.send(it)
|
||||
LocalCache.justConsume(it, null)
|
||||
}
|
||||
|
||||
@@ -695,8 +695,8 @@ object LocalCache {
|
||||
event.originalPost().mapNotNull { checkGetOrCreateNote(it) } +
|
||||
event.taggedAddresses().map { getOrCreateAddressableNote(it) }
|
||||
is ReportEvent ->
|
||||
event.reportedPost().mapNotNull { checkGetOrCreateNote(it.key) } +
|
||||
event.taggedAddresses().map { getOrCreateAddressableNote(it) }
|
||||
event.reportedPost().mapNotNull { checkGetOrCreateNote(it.eventId) } +
|
||||
event.reportedAddresses().map { getOrCreateAddressableNote(it.address) }
|
||||
is ChannelMessageEvent ->
|
||||
event
|
||||
.tagsWithoutCitations()
|
||||
@@ -1342,7 +1342,7 @@ object LocalCache {
|
||||
// Already processed this event.
|
||||
if (note.event != null) return
|
||||
|
||||
val mentions = event.reportedAuthor().mapNotNull { checkGetOrCreateUser(it.key) }
|
||||
val mentions = event.reportedAuthor().mapNotNull { checkGetOrCreateUser(it.pubkey) }
|
||||
val repliesTo = computeReplyTo(event)
|
||||
|
||||
note.loadEvent(event, author, repliesTo)
|
||||
@@ -2231,7 +2231,7 @@ object LocalCache {
|
||||
}
|
||||
if (noteEvent is ReportEvent) {
|
||||
noteEvent.reportedAuthor().mapNotNull {
|
||||
val author = getUserIfExists(it.key)
|
||||
val author = getUserIfExists(it.pubkey)
|
||||
author?.removeReport(note)
|
||||
author?.clearEOSE()
|
||||
}
|
||||
|
||||
@@ -0,0 +1,39 @@
|
||||
/**
|
||||
* 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.model
|
||||
|
||||
import android.util.LruCache
|
||||
|
||||
object MediaAspectRatioCache {
|
||||
val mediaAspectRatioCacheByUrl = LruCache<String, Float>(1000)
|
||||
|
||||
fun get(url: String) = mediaAspectRatioCacheByUrl.get(url)
|
||||
|
||||
fun add(
|
||||
url: String,
|
||||
width: Int,
|
||||
height: Int,
|
||||
) {
|
||||
if (height > 1) {
|
||||
mediaAspectRatioCacheByUrl.put(url, width.toFloat() / height.toFloat())
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -68,6 +68,8 @@ import com.vitorpamplona.quartz.nip47WalletConnect.LnZapPaymentResponseEvent
|
||||
import com.vitorpamplona.quartz.nip47WalletConnect.PayInvoiceSuccessResponse
|
||||
import com.vitorpamplona.quartz.nip53LiveActivities.chat.LiveActivitiesChatMessageEvent
|
||||
import com.vitorpamplona.quartz.nip53LiveActivities.streaming.LiveActivitiesEvent
|
||||
import com.vitorpamplona.quartz.nip56Reports.ReportEvent
|
||||
import com.vitorpamplona.quartz.nip56Reports.ReportType
|
||||
import com.vitorpamplona.quartz.nip57Zaps.LnZapEvent
|
||||
import com.vitorpamplona.quartz.nip57Zaps.LnZapRequestEvent
|
||||
import com.vitorpamplona.quartz.nip59Giftwrap.WrappedEvent
|
||||
@@ -717,6 +719,15 @@ open class Note(
|
||||
)
|
||||
}
|
||||
|
||||
fun hasReport(
|
||||
loggedIn: User,
|
||||
type: ReportType,
|
||||
): Boolean =
|
||||
reports[loggedIn]?.firstOrNull {
|
||||
it.event is ReportEvent &&
|
||||
(it.event as ReportEvent).reportedAuthor().any { it.type == type }
|
||||
} != null
|
||||
|
||||
fun hasPledgeBy(user: User): Boolean =
|
||||
replies
|
||||
.filter { it.event?.hasAdditionalReward() ?: false }
|
||||
|
||||
@@ -46,6 +46,7 @@ import com.vitorpamplona.quartz.nip19Bech32.entities.NProfile
|
||||
import com.vitorpamplona.quartz.nip19Bech32.toNpub
|
||||
import com.vitorpamplona.quartz.nip51Lists.BookmarkListEvent
|
||||
import com.vitorpamplona.quartz.nip56Reports.ReportEvent
|
||||
import com.vitorpamplona.quartz.nip56Reports.ReportType
|
||||
import com.vitorpamplona.quartz.nip57Zaps.LnZapEvent
|
||||
import com.vitorpamplona.quartz.nip65RelayList.AdvertisedRelayListEvent
|
||||
import com.vitorpamplona.quartz.utils.DualCase
|
||||
@@ -363,11 +364,11 @@ class User(
|
||||
|
||||
fun hasReport(
|
||||
loggedIn: User,
|
||||
type: ReportEvent.ReportType,
|
||||
type: ReportType,
|
||||
): Boolean =
|
||||
reports[loggedIn]?.firstOrNull {
|
||||
it.event is ReportEvent &&
|
||||
(it.event as ReportEvent).reportedAuthor().any { it.reportType == type }
|
||||
(it.event as ReportEvent).reportedAuthor().any { it.type == type }
|
||||
} != null
|
||||
|
||||
fun containsAny(hiddenWordsCase: List<DualCase>): Boolean {
|
||||
|
||||
@@ -41,6 +41,12 @@ object CachedRichTextParser {
|
||||
return result
|
||||
}
|
||||
|
||||
fun cachedText(
|
||||
content: String,
|
||||
tags: ImmutableListOfLists<String>,
|
||||
callbackUri: String? = null,
|
||||
): RichTextViewerState? = richTextCache[hashCodeCache(content, tags, callbackUri)]
|
||||
|
||||
fun parseText(
|
||||
content: String,
|
||||
tags: ImmutableListOfLists<String>,
|
||||
|
||||
+20
-3
@@ -21,6 +21,7 @@
|
||||
package com.vitorpamplona.amethyst.service.lnurl
|
||||
|
||||
import android.content.Context
|
||||
import android.util.Log
|
||||
import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper
|
||||
import com.vitorpamplona.amethyst.BuildConfig
|
||||
import com.vitorpamplona.amethyst.R
|
||||
@@ -166,12 +167,24 @@ class LightningAddressResolver {
|
||||
response: Response,
|
||||
context: Context,
|
||||
): String {
|
||||
val body = response.body.string()
|
||||
|
||||
val errorMessage =
|
||||
runCatching {
|
||||
jacksonObjectMapper().readTree(response.body.string())
|
||||
jacksonObjectMapper().readTree(body)
|
||||
}.getOrNull()?.let { tree ->
|
||||
val status = tree.get("status")?.asText()
|
||||
val message = tree.get("message")?.asText()
|
||||
val errorNode = tree.get("error")
|
||||
val messageNode = tree.get("message")
|
||||
val statusNode = tree.get("status")
|
||||
|
||||
if (tree.get("error").isBoolean && messageNode != null) {
|
||||
if (errorNode.asBoolean() == true) {
|
||||
return messageNode.asText()
|
||||
}
|
||||
}
|
||||
|
||||
val status = statusNode?.asText()
|
||||
val message = messageNode?.asText()
|
||||
|
||||
if (status == "error" && message != null) {
|
||||
message
|
||||
@@ -180,6 +193,10 @@ class LightningAddressResolver {
|
||||
}
|
||||
}
|
||||
|
||||
if (errorMessage == null) {
|
||||
Log.d("LightningAddressResolver", "Error parsing LNResponse: $body")
|
||||
}
|
||||
|
||||
return errorMessage
|
||||
?: HttpStatusMessages.resourceIdFor(response.code)?.let { stringRes(context, it) }
|
||||
?: response.message.ifBlank { null }
|
||||
|
||||
+6
@@ -30,9 +30,11 @@ import androidx.media3.common.Player
|
||||
import androidx.media3.common.Player.PositionInfo
|
||||
import androidx.media3.common.Player.STATE_IDLE
|
||||
import androidx.media3.common.Player.STATE_READY
|
||||
import androidx.media3.common.VideoSize
|
||||
import androidx.media3.common.util.UnstableApi
|
||||
import androidx.media3.exoplayer.ExoPlayer
|
||||
import androidx.media3.session.MediaSession
|
||||
import com.vitorpamplona.amethyst.model.MediaAspectRatioCache
|
||||
import com.vitorpamplona.amethyst.ui.MainActivity
|
||||
import kotlinx.coroutines.DelicateCoroutinesApi
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
@@ -113,6 +115,10 @@ class MultiPlayerPlaybackManager(
|
||||
// avoids saving positions for live streams otherwise caching goes crazy
|
||||
val mustCachePositions = !uri.contains(".m3u8", true)
|
||||
|
||||
override fun onVideoSizeChanged(videoSize: VideoSize) {
|
||||
MediaAspectRatioCache.add(uri, videoSize.width, videoSize.height)
|
||||
}
|
||||
|
||||
override fun onIsPlayingChanged(isPlaying: Boolean) {
|
||||
if (isPlaying) {
|
||||
player.setWakeMode(C.WAKE_MODE_NETWORK)
|
||||
|
||||
@@ -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)
|
||||
|
||||
+2
-2
@@ -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,
|
||||
|
||||
+1
-1
@@ -218,7 +218,7 @@ fun RelaySelectionDialog(
|
||||
)
|
||||
}
|
||||
|
||||
accountViewModel.toast(
|
||||
accountViewModel.toastManager.toast(
|
||||
stringRes(context, R.string.unable_to_download_relay_document),
|
||||
msg,
|
||||
)
|
||||
|
||||
+1
-1
@@ -103,7 +103,7 @@ fun BasicRelaySetupInfoDialog(
|
||||
)
|
||||
}
|
||||
|
||||
accountViewModel.toast(
|
||||
accountViewModel.toastManager.toast(
|
||||
stringRes(context, R.string.unable_to_download_relay_document),
|
||||
msg,
|
||||
)
|
||||
|
||||
+1
-1
@@ -280,7 +280,7 @@ fun LoadRelayInfo(
|
||||
)
|
||||
}
|
||||
|
||||
accountViewModel.toast(
|
||||
accountViewModel.toastManager.toast(
|
||||
stringRes(context, R.string.unable_to_download_relay_document),
|
||||
msg,
|
||||
)
|
||||
|
||||
+1
-1
@@ -105,7 +105,7 @@ fun Kind3RelaySetupInfoProposalDialog(
|
||||
)
|
||||
}
|
||||
|
||||
accountViewModel.toast(
|
||||
accountViewModel.toastManager.toast(
|
||||
stringRes(context, R.string.unable_to_download_relay_document),
|
||||
msg,
|
||||
)
|
||||
|
||||
+4
-4
@@ -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,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -59,6 +59,7 @@ val DefaultAnimationColors =
|
||||
@Composable
|
||||
fun LoadingAnimation(
|
||||
indicatorSize: Dp = 20.dp,
|
||||
circleWidth: Dp = 4.dp,
|
||||
circleColors: ImmutableList<Color> = DefaultAnimationColors,
|
||||
animationDuration: Int = 1000,
|
||||
) {
|
||||
@@ -86,7 +87,7 @@ fun LoadingAnimation(
|
||||
.size(size = indicatorSize)
|
||||
.rotate(degrees = rotateAnimation)
|
||||
.border(
|
||||
width = 4.dp,
|
||||
width = circleWidth,
|
||||
brush = Brush.sweepGradient(circleColors),
|
||||
shape = CircleShape,
|
||||
),
|
||||
|
||||
@@ -530,20 +530,22 @@ fun DisplaySecretEmoji(
|
||||
) {
|
||||
if (canPreview && quotesLeft > 0) {
|
||||
var secretContent by remember {
|
||||
mutableStateOf<RichTextViewerState?>(null)
|
||||
mutableStateOf(CachedRichTextParser.cachedText(EmojiCoder.decode(segment.segmentText), state.tags))
|
||||
}
|
||||
|
||||
var showPopup by remember {
|
||||
mutableStateOf(false)
|
||||
}
|
||||
|
||||
LaunchedEffect(segment) {
|
||||
launch(Dispatchers.Default) {
|
||||
secretContent =
|
||||
CachedRichTextParser.parseText(
|
||||
EmojiCoder.decode(segment.segmentText),
|
||||
state.tags,
|
||||
)
|
||||
if (secretContent == null) {
|
||||
LaunchedEffect(segment) {
|
||||
launch(Dispatchers.Default) {
|
||||
secretContent =
|
||||
CachedRichTextParser.parseText(
|
||||
EmojiCoder.decode(segment.segmentText),
|
||||
state.tags,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -98,6 +98,7 @@ import com.linc.audiowaveform.infiniteLinearGradient
|
||||
import com.vitorpamplona.amethyst.R
|
||||
import com.vitorpamplona.amethyst.commons.compose.GenericBaseCache
|
||||
import com.vitorpamplona.amethyst.commons.compose.produceCachedState
|
||||
import com.vitorpamplona.amethyst.model.MediaAspectRatioCache
|
||||
import com.vitorpamplona.amethyst.service.okhttp.HttpClientManager
|
||||
import com.vitorpamplona.amethyst.service.playback.PlaybackClientController
|
||||
import com.vitorpamplona.amethyst.ui.actions.MediaSaverToDisk
|
||||
@@ -261,7 +262,8 @@ fun VideoView(
|
||||
}
|
||||
|
||||
if (blurhash == null) {
|
||||
val ratio = dimensions?.aspectRatio()
|
||||
val ratio = dimensions?.aspectRatio() ?: MediaAspectRatioCache.get(videoUri)
|
||||
|
||||
val modifier =
|
||||
if (ratio != null && automaticallyStartPlayback.value) {
|
||||
Modifier.aspectRatio(ratio)
|
||||
@@ -295,7 +297,7 @@ fun VideoView(
|
||||
}
|
||||
}
|
||||
} else {
|
||||
val ratio = dimensions?.aspectRatio()
|
||||
val ratio = dimensions?.aspectRatio() ?: MediaAspectRatioCache.get(videoUri)
|
||||
|
||||
val modifier =
|
||||
if (ratio != null) {
|
||||
@@ -1233,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)
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
+4
-4
@@ -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)
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
+34
-14
@@ -43,6 +43,7 @@ import androidx.compose.material3.adaptive.currentWindowAdaptiveInfo
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.MutableState
|
||||
import androidx.compose.runtime.SideEffect
|
||||
import androidx.compose.runtime.collectAsState
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
@@ -77,6 +78,7 @@ import com.vitorpamplona.amethyst.commons.richtext.MediaPreloadedContent
|
||||
import com.vitorpamplona.amethyst.commons.richtext.MediaUrlContent
|
||||
import com.vitorpamplona.amethyst.commons.richtext.MediaUrlImage
|
||||
import com.vitorpamplona.amethyst.commons.richtext.MediaUrlVideo
|
||||
import com.vitorpamplona.amethyst.model.MediaAspectRatioCache
|
||||
import com.vitorpamplona.amethyst.service.Blurhash
|
||||
import com.vitorpamplona.amethyst.ui.actions.CrossfadeIfEnabled
|
||||
import com.vitorpamplona.amethyst.ui.actions.InformationDialog
|
||||
@@ -87,10 +89,13 @@ import com.vitorpamplona.amethyst.ui.note.DownloadForOfflineIcon
|
||||
import com.vitorpamplona.amethyst.ui.note.HashCheckFailedIcon
|
||||
import com.vitorpamplona.amethyst.ui.note.HashCheckIcon
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.profile.gallery.UrlImageView
|
||||
import com.vitorpamplona.amethyst.ui.stringRes
|
||||
import com.vitorpamplona.amethyst.ui.theme.Size20dp
|
||||
import com.vitorpamplona.amethyst.ui.theme.Size24dp
|
||||
import com.vitorpamplona.amethyst.ui.theme.Size30dp
|
||||
import com.vitorpamplona.amethyst.ui.theme.Size40dp
|
||||
import com.vitorpamplona.amethyst.ui.theme.Size6dp
|
||||
import com.vitorpamplona.amethyst.ui.theme.Size75dp
|
||||
import com.vitorpamplona.amethyst.ui.theme.hashVerifierMark
|
||||
import com.vitorpamplona.amethyst.ui.theme.imageModifier
|
||||
@@ -132,7 +137,6 @@ fun ZoomableContentView(
|
||||
.fillMaxWidth()
|
||||
.clickable { dialogOpen = true }
|
||||
val loadedImageModifier = if (roundedCorner) MaterialTheme.colorScheme.imageModifier else Modifier.fillMaxWidth()
|
||||
|
||||
UrlImageView(content, contentScale, mainImageModifier, loadedImageModifier, controllerVisible, accountViewModel = accountViewModel)
|
||||
}
|
||||
}
|
||||
@@ -235,7 +239,7 @@ fun LocalImageView(
|
||||
)
|
||||
}
|
||||
|
||||
val ratio = remember(content) { aspectRatio(content.dim) }
|
||||
val ratio = remember(content) { content.dim?.aspectRatio() ?: MediaAspectRatioCache.get(content.localFile.toString()) }
|
||||
CrossfadeIfEnabled(targetState = showImage.value, contentAlignment = Alignment.Center, accountViewModel = accountViewModel) {
|
||||
if (it) {
|
||||
SubcomposeAsyncImage(
|
||||
@@ -265,8 +269,14 @@ fun LocalImageView(
|
||||
)
|
||||
}
|
||||
} else {
|
||||
WaitAndDisplay {
|
||||
DisplayUrlWithLoadingSymbol(content)
|
||||
if (ratio != null) {
|
||||
Box(loadedImageModifier.aspectRatio(ratio), contentAlignment = Alignment.Center) {
|
||||
LoadingAnimation(Size40dp, Size6dp)
|
||||
}
|
||||
} else {
|
||||
WaitAndDisplay {
|
||||
DisplayUrlWithLoadingSymbol(content)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -276,6 +286,11 @@ fun LocalImageView(
|
||||
is AsyncImagePainter.State.Success -> {
|
||||
SubcomposeAsyncImageContent(loadedImageModifier)
|
||||
|
||||
SideEffect {
|
||||
val drawable = (state as AsyncImagePainter.State.Success).result.image
|
||||
MediaAspectRatioCache.add(content.localFile.toString(), drawable.width, drawable.height)
|
||||
}
|
||||
|
||||
content.isVerified?.let {
|
||||
AnimatedVisibility(
|
||||
visible = controllerVisible.value,
|
||||
@@ -328,7 +343,7 @@ fun UrlImageView(
|
||||
accountViewModel: AccountViewModel,
|
||||
alwayShowImage: Boolean = false,
|
||||
) {
|
||||
val ratio = content.dim?.aspectRatio()
|
||||
val ratio = content.dim?.aspectRatio() ?: MediaAspectRatioCache.get(content.url)
|
||||
|
||||
val showImage =
|
||||
remember {
|
||||
@@ -373,8 +388,14 @@ fun UrlImageView(
|
||||
)
|
||||
}
|
||||
} else {
|
||||
WaitAndDisplay {
|
||||
DisplayUrlWithLoadingSymbol(content)
|
||||
if (ratio != null) {
|
||||
Box(loadedImageModifier.aspectRatio(ratio), contentAlignment = Alignment.Center) {
|
||||
LoadingAnimation(Size40dp, Size6dp)
|
||||
}
|
||||
} else {
|
||||
WaitAndDisplay {
|
||||
DisplayUrlWithLoadingSymbol(content)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -385,6 +406,11 @@ fun UrlImageView(
|
||||
SubcomposeAsyncImageContent(loadedImageModifier)
|
||||
|
||||
ShowHashAnimated(content, controllerVisible, Modifier.align(Alignment.TopEnd))
|
||||
|
||||
SideEffect {
|
||||
val drawable = (state as AsyncImagePainter.State.Success).result.image
|
||||
MediaAspectRatioCache.add(content.url, drawable.width, drawable.height)
|
||||
}
|
||||
}
|
||||
else -> {}
|
||||
}
|
||||
@@ -519,12 +545,6 @@ fun ShowHash(content: MediaUrlContent) {
|
||||
verifiedHash?.let { HashVerificationSymbol(it) }
|
||||
}
|
||||
|
||||
fun aspectRatio(dim: DimensionTag?): Float? {
|
||||
if (dim == null) return null
|
||||
|
||||
return dim.width.toFloat() / dim.height.toFloat()
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun WaitAndDisplay(
|
||||
content:
|
||||
@@ -706,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)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+16
-10
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
+30
@@ -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()
|
||||
+29
@@ -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()
|
||||
+30
@@ -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()
|
||||
+89
@@ -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
|
||||
+20
-97
@@ -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,
|
||||
+52
@@ -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?,
|
||||
)
|
||||
+107
@@ -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))
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
@@ -31,7 +31,7 @@ import com.vitorpamplona.quartz.utils.TimeUtils
|
||||
open class DiscoverNIP89FeedFilter(
|
||||
val account: Account,
|
||||
) : AdditiveFeedFilter<Note>() {
|
||||
val lastAnnounced = 90 * 24 * 60 * 60 // 90 Days ago
|
||||
val lastAnnounced = 365 * 24 * 60 * 60 // 365 Days ago
|
||||
// TODO better than announced would be last active, as this requires the DVM provider to regularly update the NIP89 announcement
|
||||
|
||||
override fun feedKey(): String = account.userProfile().pubkeyHex + "-" + followList()
|
||||
@@ -43,8 +43,6 @@ open class DiscoverNIP89FeedFilter(
|
||||
followList() == MuteListEvent.blockListFor(account.userProfile().pubkeyHex)
|
||||
|
||||
override fun feed(): List<Note> {
|
||||
val filterParams = buildFilterParams(account)
|
||||
|
||||
val notes =
|
||||
LocalCache.addressables.filterIntoSet { _, it ->
|
||||
acceptDVM(it)
|
||||
|
||||
@@ -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
|
||||
@@ -363,7 +363,7 @@ fun AppNavigation(
|
||||
|
||||
NavigateIfIntentRequested(nav, accountViewModel, accountStateViewModel)
|
||||
|
||||
DisplayErrorMessages(accountViewModel)
|
||||
DisplayErrorMessages(accountViewModel.toastManager, accountViewModel, nav)
|
||||
DisplayNotifyMessages(accountViewModel, nav)
|
||||
}
|
||||
|
||||
@@ -446,7 +446,7 @@ private fun NavigateIfIntentRequested(
|
||||
|
||||
actionableNextPage = null
|
||||
} else {
|
||||
accountViewModel.toast(
|
||||
accountViewModel.toastManager.toast(
|
||||
R.string.invalid_nip19_uri,
|
||||
R.string.invalid_nip19_uri_description,
|
||||
intentNextPage,
|
||||
@@ -501,7 +501,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,
|
||||
|
||||
@@ -559,7 +559,7 @@ fun ListContent(
|
||||
accountViewModel.setTorSettings(torSettings)
|
||||
},
|
||||
onError = {
|
||||
accountViewModel.toast(
|
||||
accountViewModel.toastManager.toast(
|
||||
stringRes(context, R.string.could_not_connect_to_tor),
|
||||
it,
|
||||
)
|
||||
|
||||
@@ -64,7 +64,6 @@ import androidx.compose.ui.window.Popup
|
||||
import androidx.compose.ui.window.PopupProperties
|
||||
import com.vitorpamplona.amethyst.R
|
||||
import com.vitorpamplona.amethyst.commons.emojicoder.EmojiCoder
|
||||
import com.vitorpamplona.amethyst.commons.richtext.RichTextViewerState
|
||||
import com.vitorpamplona.amethyst.model.FeatureSetType
|
||||
import com.vitorpamplona.amethyst.model.Note
|
||||
import com.vitorpamplona.amethyst.model.NoteState
|
||||
@@ -251,21 +250,23 @@ fun DisplaySecretEmojiAsReaction(
|
||||
accountViewModel: AccountViewModel,
|
||||
nav: INav,
|
||||
) {
|
||||
var secretContent by remember {
|
||||
mutableStateOf<RichTextViewerState?>(null)
|
||||
var secretContent by remember(reaction) {
|
||||
mutableStateOf(CachedRichTextParser.cachedText(EmojiCoder.decode(reaction), EmptyTagList))
|
||||
}
|
||||
|
||||
var showPopup by remember {
|
||||
mutableStateOf(false)
|
||||
}
|
||||
|
||||
LaunchedEffect(reaction) {
|
||||
launch(Dispatchers.Default) {
|
||||
secretContent =
|
||||
CachedRichTextParser.parseText(
|
||||
EmojiCoder.decode(reaction),
|
||||
EmptyTagList,
|
||||
)
|
||||
if (secretContent == null) {
|
||||
LaunchedEffect(reaction) {
|
||||
launch(Dispatchers.Default) {
|
||||
secretContent =
|
||||
CachedRichTextParser.parseText(
|
||||
EmojiCoder.decode(reaction),
|
||||
EmptyTagList,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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 ->
|
||||
|
||||
+3
-2
@@ -53,6 +53,7 @@ import com.vitorpamplona.amethyst.ui.screen.loggedIn.profile.gallery.UrlImageVie
|
||||
import com.vitorpamplona.amethyst.ui.stringRes
|
||||
import com.vitorpamplona.amethyst.ui.theme.DividerThickness
|
||||
import com.vitorpamplona.amethyst.ui.theme.Size10dp
|
||||
import com.vitorpamplona.amethyst.ui.theme.Size40Modifier
|
||||
import com.vitorpamplona.quartz.nip01Core.tags.addressables.taggedAddresses
|
||||
import com.vitorpamplona.quartz.nip30CustomEmoji.selection.EmojiPackSelectionEvent
|
||||
import kotlinx.collections.immutable.toImmutableList
|
||||
@@ -108,11 +109,11 @@ fun ShowEmojiSuggestionList(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = spacedBy(Size10dp),
|
||||
) {
|
||||
Box(Modifier.size(40.dp)) {
|
||||
Box(Size40Modifier) {
|
||||
UrlImageView(it.url, accountViewModel)
|
||||
}
|
||||
Text(it.code, fontWeight = FontWeight.Bold, modifier = Modifier.weight(1f))
|
||||
Box(Modifier.size(40.dp), contentAlignment = Alignment.Center) {
|
||||
Box(Size40Modifier, contentAlignment = Alignment.Center) {
|
||||
IconButton(
|
||||
onClick = {
|
||||
onFullSize(it)
|
||||
|
||||
@@ -53,7 +53,6 @@ import androidx.compose.material3.Surface
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TopAppBar
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.MutableState
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.livedata.observeAsState
|
||||
@@ -79,13 +78,10 @@ import androidx.lifecycle.viewModelScope
|
||||
import androidx.lifecycle.viewmodel.compose.viewModel
|
||||
import com.vitorpamplona.amethyst.R
|
||||
import com.vitorpamplona.amethyst.commons.emojicoder.EmojiCoder
|
||||
import com.vitorpamplona.amethyst.commons.richtext.RichTextViewerState
|
||||
import com.vitorpamplona.amethyst.model.Account
|
||||
import com.vitorpamplona.amethyst.model.AddressableNote
|
||||
import com.vitorpamplona.amethyst.service.CachedRichTextParser
|
||||
import com.vitorpamplona.amethyst.service.firstFullChar
|
||||
import com.vitorpamplona.amethyst.ui.components.AnimatedBorderTextCornerRadius
|
||||
import com.vitorpamplona.amethyst.ui.components.CoreSecretMessage
|
||||
import com.vitorpamplona.amethyst.ui.components.InLineIconRenderer
|
||||
import com.vitorpamplona.amethyst.ui.components.SetDialogToEdgeToEdge
|
||||
import com.vitorpamplona.amethyst.ui.navigation.INav
|
||||
@@ -380,53 +376,6 @@ private fun RenderReactionOption(
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun DisplaySecretEmoji(
|
||||
text: String,
|
||||
state: RichTextViewerState,
|
||||
callbackUri: String?,
|
||||
canPreview: Boolean,
|
||||
quotesLeft: Int,
|
||||
backgroundColor: MutableState<Color>,
|
||||
accountViewModel: AccountViewModel,
|
||||
nav: INav,
|
||||
) {
|
||||
if (canPreview && quotesLeft > 0) {
|
||||
var secretContent by remember {
|
||||
mutableStateOf<RichTextViewerState?>(null)
|
||||
}
|
||||
|
||||
var showPopup by remember {
|
||||
mutableStateOf(false)
|
||||
}
|
||||
|
||||
LaunchedEffect(text) {
|
||||
launch(Dispatchers.Default) {
|
||||
secretContent =
|
||||
CachedRichTextParser.parseText(
|
||||
EmojiCoder.decode(text),
|
||||
state.tags,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
val localSecretContent = secretContent
|
||||
|
||||
AnimatedBorderTextCornerRadius(
|
||||
text,
|
||||
Modifier.clickable {
|
||||
showPopup = !showPopup
|
||||
},
|
||||
)
|
||||
|
||||
if (localSecretContent != null && showPopup) {
|
||||
CoreSecretMessage(localSecretContent, callbackUri, quotesLeft, backgroundColor, accountViewModel, nav)
|
||||
}
|
||||
} else {
|
||||
Text(text)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun EmojiSelector(
|
||||
accountViewModel: AccountViewModel,
|
||||
|
||||
@@ -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),
|
||||
)
|
||||
@@ -592,7 +592,7 @@ fun UpdateZapAmountContent(
|
||||
onValueChange = { postViewModel.walletConnectSecret = it },
|
||||
keyboardOptions =
|
||||
KeyboardOptions(
|
||||
autoCorrect = false,
|
||||
autoCorrectEnabled = false,
|
||||
keyboardType = KeyboardType.Password,
|
||||
imeAction = ImeAction.Go,
|
||||
),
|
||||
@@ -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,
|
||||
|
||||
+7
-18
@@ -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),
|
||||
)
|
||||
|
||||
@@ -36,6 +36,7 @@ import com.vitorpamplona.amethyst.ui.stringRes
|
||||
import com.vitorpamplona.amethyst.ui.theme.replyModifier
|
||||
import com.vitorpamplona.quartz.nip02FollowList.EmptyTagList
|
||||
import com.vitorpamplona.quartz.nip56Reports.ReportEvent
|
||||
import com.vitorpamplona.quartz.nip56Reports.ReportType
|
||||
|
||||
@Composable
|
||||
fun RenderReport(
|
||||
@@ -52,16 +53,18 @@ fun RenderReport(
|
||||
val reportType =
|
||||
base
|
||||
.map {
|
||||
when (it.reportType) {
|
||||
ReportEvent.ReportType.EXPLICIT -> stringRes(R.string.explicit_content)
|
||||
ReportEvent.ReportType.NUDITY -> stringRes(R.string.nudity)
|
||||
ReportEvent.ReportType.PROFANITY -> stringRes(R.string.profanity_hateful_speech)
|
||||
ReportEvent.ReportType.SPAM -> stringRes(R.string.spam)
|
||||
ReportEvent.ReportType.IMPERSONATION -> stringRes(R.string.impersonation)
|
||||
ReportEvent.ReportType.ILLEGAL -> stringRes(R.string.illegal_behavior)
|
||||
ReportEvent.ReportType.MALWARE -> stringRes(R.string.malware)
|
||||
ReportEvent.ReportType.MOD -> stringRes(R.string.mod)
|
||||
ReportEvent.ReportType.OTHER -> stringRes(R.string.other)
|
||||
when (it.type) {
|
||||
ReportType.EXPLICIT -> stringRes(R.string.explicit_content)
|
||||
ReportType.NUDITY -> stringRes(R.string.nudity)
|
||||
ReportType.PROFANITY -> stringRes(R.string.profanity_hateful_speech)
|
||||
ReportType.SPAM -> stringRes(R.string.spam)
|
||||
ReportType.IMPERSONATION -> stringRes(R.string.impersonation)
|
||||
ReportType.ILLEGAL -> stringRes(R.string.illegal_behavior)
|
||||
ReportType.MALWARE -> stringRes(R.string.malware)
|
||||
ReportType.MOD -> stringRes(R.string.mod)
|
||||
ReportType.OTHER -> stringRes(R.string.other)
|
||||
ReportType.HARASSMENT -> stringRes(R.string.harassment)
|
||||
null -> stringRes(R.string.other)
|
||||
}
|
||||
}.toSet()
|
||||
.joinToString(", ")
|
||||
|
||||
@@ -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,
|
||||
)
|
||||
|
||||
+3
-3
@@ -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) },
|
||||
)
|
||||
},
|
||||
) {
|
||||
|
||||
+8
-61
@@ -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
|
||||
@@ -102,7 +103,7 @@ import com.vitorpamplona.quartz.nip37Drafts.DraftEvent
|
||||
import com.vitorpamplona.quartz.nip47WalletConnect.Response
|
||||
import com.vitorpamplona.quartz.nip50Search.SearchRelayListEvent
|
||||
import com.vitorpamplona.quartz.nip51Lists.GeneralListEvent
|
||||
import com.vitorpamplona.quartz.nip56Reports.ReportEvent
|
||||
import com.vitorpamplona.quartz.nip56Reports.ReportType
|
||||
import com.vitorpamplona.quartz.nip57Zaps.LnZapEvent
|
||||
import com.vitorpamplona.quartz.nip57Zaps.LnZapRequestEvent
|
||||
import com.vitorpamplona.quartz.nip57Zaps.zapraiser.zapraiserAmount
|
||||
@@ -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()
|
||||
@@ -741,7 +687,7 @@ class AccountViewModel(
|
||||
|
||||
fun report(
|
||||
note: Note,
|
||||
type: ReportEvent.ReportType,
|
||||
type: ReportType,
|
||||
content: String = "",
|
||||
) {
|
||||
viewModelScope.launch(Dispatchers.IO) { account.report(note, type, content) }
|
||||
@@ -749,7 +695,7 @@ class AccountViewModel(
|
||||
|
||||
fun report(
|
||||
user: User,
|
||||
type: ReportEvent.ReportType,
|
||||
type: ReportType,
|
||||
) {
|
||||
viewModelScope.launch(Dispatchers.IO) {
|
||||
account.report(user, type)
|
||||
@@ -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,
|
||||
|
||||
+2
-2
@@ -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) },
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
+8
-8
@@ -63,7 +63,7 @@ import com.vitorpamplona.amethyst.ui.note.ArrowBackIcon
|
||||
import com.vitorpamplona.amethyst.ui.stringRes
|
||||
import com.vitorpamplona.amethyst.ui.theme.DividerThickness
|
||||
import com.vitorpamplona.amethyst.ui.theme.WarningColor
|
||||
import com.vitorpamplona.quartz.nip56Reports.ReportEvent
|
||||
import com.vitorpamplona.quartz.nip56Reports.ReportType
|
||||
import kotlinx.collections.immutable.toImmutableList
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@@ -75,13 +75,13 @@ fun ReportNoteDialog(
|
||||
) {
|
||||
val reportTypes =
|
||||
listOf(
|
||||
Pair(ReportEvent.ReportType.SPAM, stringRes(R.string.report_dialog_spam)),
|
||||
Pair(ReportEvent.ReportType.PROFANITY, stringRes(R.string.report_dialog_profanity)),
|
||||
Pair(ReportEvent.ReportType.IMPERSONATION, stringRes(R.string.report_dialog_impersonation)),
|
||||
Pair(ReportEvent.ReportType.NUDITY, stringRes(R.string.report_dialog_nudity)),
|
||||
Pair(ReportEvent.ReportType.ILLEGAL, stringRes(R.string.report_dialog_illegal)),
|
||||
Pair(ReportEvent.ReportType.MALWARE, stringRes(R.string.report_malware)),
|
||||
Pair(ReportEvent.ReportType.MOD, stringRes(R.string.report_mod)),
|
||||
Pair(ReportType.SPAM, stringRes(R.string.report_dialog_spam)),
|
||||
Pair(ReportType.PROFANITY, stringRes(R.string.report_dialog_profanity)),
|
||||
Pair(ReportType.IMPERSONATION, stringRes(R.string.report_dialog_impersonation)),
|
||||
Pair(ReportType.NUDITY, stringRes(R.string.report_dialog_nudity)),
|
||||
Pair(ReportType.ILLEGAL, stringRes(R.string.report_dialog_illegal)),
|
||||
Pair(ReportType.MALWARE, stringRes(R.string.report_malware)),
|
||||
Pair(ReportType.MOD, stringRes(R.string.report_mod)),
|
||||
)
|
||||
|
||||
val reasonOptions = remember { reportTypes.map { TitleExplainer(it.second) }.toImmutableList() }
|
||||
|
||||
+1
-1
@@ -65,7 +65,7 @@ fun RoomChatFileUploadDialog(
|
||||
},
|
||||
upload = {
|
||||
channelScreenModel.upload(
|
||||
onError = accountViewModel::toast,
|
||||
onError = accountViewModel.toastManager::toast,
|
||||
context = context,
|
||||
onceUploaded = onUpload,
|
||||
)
|
||||
|
||||
+1
-1
@@ -126,7 +126,7 @@ fun ChannelFileUploadDialog(
|
||||
},
|
||||
upload = {
|
||||
channelScreenModel.upload(
|
||||
onError = accountViewModel::toast,
|
||||
onError = accountViewModel.toastManager::toast,
|
||||
context = context,
|
||||
onceUploaded = onUpload,
|
||||
)
|
||||
|
||||
+2
-2
@@ -47,12 +47,12 @@ import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.vitorpamplona.amethyst.R
|
||||
import com.vitorpamplona.amethyst.ui.feeds.FeedContentState
|
||||
import com.vitorpamplona.amethyst.ui.navigation.INav
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
||||
import com.vitorpamplona.amethyst.ui.stringRes
|
||||
import com.vitorpamplona.amethyst.ui.theme.Size40dp
|
||||
import com.vitorpamplona.amethyst.ui.theme.TabRowHeight
|
||||
import com.vitorpamplona.amethyst.ui.theme.placeholderText
|
||||
import kotlinx.coroutines.launch
|
||||
@@ -93,7 +93,7 @@ fun MessagesTabHeader(
|
||||
IconButton(
|
||||
modifier =
|
||||
Modifier
|
||||
.size(40.dp)
|
||||
.size(Size40dp)
|
||||
.align(Alignment.CenterEnd),
|
||||
onClick = { moreActionsExpanded = true },
|
||||
) {
|
||||
|
||||
+4
-14
@@ -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)
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
+2
-2
@@ -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,
|
||||
)
|
||||
|
||||
+2
-2
@@ -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,
|
||||
)
|
||||
|
||||
+3
-4
@@ -56,12 +56,11 @@ import com.vitorpamplona.amethyst.ui.actions.CrossfadeIfEnabled
|
||||
import com.vitorpamplona.amethyst.ui.components.AutoNonlazyGrid
|
||||
import com.vitorpamplona.amethyst.ui.components.ClickableUrl
|
||||
import com.vitorpamplona.amethyst.ui.components.DisplayBlurHash
|
||||
import com.vitorpamplona.amethyst.ui.components.DisplayUrlWithLoadingSymbol
|
||||
import com.vitorpamplona.amethyst.ui.components.GetMediaItem
|
||||
import com.vitorpamplona.amethyst.ui.components.GetVideoController
|
||||
import com.vitorpamplona.amethyst.ui.components.ImageUrlWithDownloadButton
|
||||
import com.vitorpamplona.amethyst.ui.components.LoadingAnimation
|
||||
import com.vitorpamplona.amethyst.ui.components.SensitivityWarning
|
||||
import com.vitorpamplona.amethyst.ui.components.WaitAndDisplay
|
||||
import com.vitorpamplona.amethyst.ui.navigation.INav
|
||||
import com.vitorpamplona.amethyst.ui.note.DownloadForOfflineIcon
|
||||
import com.vitorpamplona.amethyst.ui.note.WatchAuthor
|
||||
@@ -217,8 +216,8 @@ fun UrlImageView(
|
||||
defaultModifier,
|
||||
)
|
||||
} else {
|
||||
WaitAndDisplay {
|
||||
DisplayUrlWithLoadingSymbol(content)
|
||||
Box(defaultModifier, contentAlignment = Alignment.Center) {
|
||||
LoadingAnimation()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+3
-3
@@ -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,
|
||||
)
|
||||
|
||||
+1
-1
@@ -130,7 +130,7 @@ fun DisplayLNAddress(
|
||||
}
|
||||
},
|
||||
onClose = { zapExpanded = false },
|
||||
onError = { title, message -> accountViewModel.toast(title, message) },
|
||||
onError = { title, message -> accountViewModel.toastManager.toast(title, message) },
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
+7
-7
@@ -36,7 +36,7 @@ import com.vitorpamplona.amethyst.ui.note.externalLinkForUser
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
||||
import com.vitorpamplona.amethyst.ui.stringRes
|
||||
import com.vitorpamplona.amethyst.ui.theme.DividerThickness
|
||||
import com.vitorpamplona.quartz.nip56Reports.ReportEvent
|
||||
import com.vitorpamplona.quartz.nip56Reports.ReportType
|
||||
|
||||
@Composable
|
||||
fun UserProfileDropDownMenu(
|
||||
@@ -108,42 +108,42 @@ fun UserProfileDropDownMenu(
|
||||
DropdownMenuItem(
|
||||
text = { Text(stringRes(id = R.string.report_spam_scam)) },
|
||||
onClick = {
|
||||
accountViewModel.report(user, ReportEvent.ReportType.SPAM)
|
||||
accountViewModel.report(user, ReportType.SPAM)
|
||||
onDismiss()
|
||||
},
|
||||
)
|
||||
DropdownMenuItem(
|
||||
text = { Text(stringRes(R.string.report_hateful_speech)) },
|
||||
onClick = {
|
||||
accountViewModel.report(user, ReportEvent.ReportType.PROFANITY)
|
||||
accountViewModel.report(user, ReportType.PROFANITY)
|
||||
onDismiss()
|
||||
},
|
||||
)
|
||||
DropdownMenuItem(
|
||||
text = { Text(stringRes(id = R.string.report_impersonation)) },
|
||||
onClick = {
|
||||
accountViewModel.report(user, ReportEvent.ReportType.IMPERSONATION)
|
||||
accountViewModel.report(user, ReportType.IMPERSONATION)
|
||||
onDismiss()
|
||||
},
|
||||
)
|
||||
DropdownMenuItem(
|
||||
text = { Text(stringRes(R.string.report_nudity_porn)) },
|
||||
onClick = {
|
||||
accountViewModel.report(user, ReportEvent.ReportType.NUDITY)
|
||||
accountViewModel.report(user, ReportType.NUDITY)
|
||||
onDismiss()
|
||||
},
|
||||
)
|
||||
DropdownMenuItem(
|
||||
text = { Text(stringRes(id = R.string.report_illegal_behaviour)) },
|
||||
onClick = {
|
||||
accountViewModel.report(user, ReportEvent.ReportType.ILLEGAL)
|
||||
accountViewModel.report(user, ReportType.ILLEGAL)
|
||||
onDismiss()
|
||||
},
|
||||
)
|
||||
DropdownMenuItem(
|
||||
text = { Text(stringRes(id = R.string.report_malware)) },
|
||||
onClick = {
|
||||
accountViewModel.report(user, ReportEvent.ReportType.MALWARE)
|
||||
accountViewModel.report(user, ReportType.MALWARE)
|
||||
onDismiss()
|
||||
},
|
||||
)
|
||||
|
||||
+3
-3
@@ -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,
|
||||
)
|
||||
|
||||
+12
-9
@@ -99,9 +99,12 @@ import com.vitorpamplona.amethyst.ui.screen.AccountStateViewModel
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.qrcode.SimpleQrCodeScanner
|
||||
import com.vitorpamplona.amethyst.ui.stringRes
|
||||
import com.vitorpamplona.amethyst.ui.theme.DoubleHorzSpacer
|
||||
import com.vitorpamplona.amethyst.ui.theme.Size0dp
|
||||
import com.vitorpamplona.amethyst.ui.theme.Size10dp
|
||||
import com.vitorpamplona.amethyst.ui.theme.Size20dp
|
||||
import com.vitorpamplona.amethyst.ui.theme.Size35dp
|
||||
import com.vitorpamplona.amethyst.ui.theme.Size40dp
|
||||
import com.vitorpamplona.amethyst.ui.theme.Size50dp
|
||||
import com.vitorpamplona.amethyst.ui.theme.ThemeComparisonRow
|
||||
import com.vitorpamplona.amethyst.ui.theme.placeholderText
|
||||
import com.vitorpamplona.amethyst.ui.tor.TorSettings
|
||||
@@ -200,7 +203,7 @@ fun LoginPage(
|
||||
contentScale = ContentScale.Inside,
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.height(40.dp))
|
||||
Spacer(modifier = Modifier.height(Size40dp))
|
||||
|
||||
KeyTextField(
|
||||
value = key.value,
|
||||
@@ -334,9 +337,9 @@ fun LoginPage(
|
||||
}
|
||||
}
|
||||
|
||||
Spacer(modifier = Modifier.height(10.dp))
|
||||
Spacer(modifier = Modifier.height(Size10dp))
|
||||
|
||||
Box(modifier = Modifier.padding(40.dp, 0.dp, 40.dp, 0.dp)) {
|
||||
Box(modifier = Modifier.padding(Size40dp, 0.dp, Size40dp, 0.dp)) {
|
||||
LoginButton(
|
||||
enabled = acceptedTerms.value,
|
||||
processingLogin = processingLogin,
|
||||
@@ -371,7 +374,7 @@ fun LoginPage(
|
||||
}
|
||||
|
||||
if (PackageUtils.isExternalSignerInstalled(context)) {
|
||||
Box(modifier = Modifier.padding(40.dp, 20.dp, 40.dp, 0.dp)) {
|
||||
Box(modifier = Modifier.padding(Size40dp, Size20dp, Size40dp, Size0dp)) {
|
||||
LoginWithAmberButton(
|
||||
enabled = acceptedTerms.value,
|
||||
onClick = {
|
||||
@@ -551,7 +554,7 @@ fun KeyTextField(
|
||||
onValueChange = { onValueChange(it, false) },
|
||||
keyboardOptions =
|
||||
KeyboardOptions(
|
||||
autoCorrect = false,
|
||||
autoCorrectEnabled = false,
|
||||
keyboardType = KeyboardType.Password,
|
||||
imeAction = ImeAction.Go,
|
||||
),
|
||||
@@ -777,11 +780,11 @@ fun LoginWithAmberButton(
|
||||
enabled = enabled,
|
||||
onClick = onClick,
|
||||
shape = RoundedCornerShape(Size35dp),
|
||||
modifier = Modifier.height(50.dp),
|
||||
modifier = Modifier.height(Size50dp),
|
||||
) {
|
||||
Text(
|
||||
text = stringRes(R.string.login_with_external_signer),
|
||||
modifier = Modifier.padding(horizontal = 40.dp),
|
||||
modifier = Modifier.padding(horizontal = Size40dp),
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -796,9 +799,9 @@ fun LoginButton(
|
||||
enabled = enabled,
|
||||
onClick = onClick,
|
||||
shape = RoundedCornerShape(Size35dp),
|
||||
modifier = Modifier.height(50.dp),
|
||||
modifier = Modifier.height(Size50dp),
|
||||
) {
|
||||
Row(modifier = Modifier.padding(horizontal = 40.dp)) {
|
||||
Row(modifier = Modifier.padding(horizontal = Size40dp)) {
|
||||
if (processingLogin) {
|
||||
LoadingAnimation()
|
||||
Spacer(modifier = DoubleHorzSpacer)
|
||||
|
||||
@@ -86,6 +86,7 @@ val TopBarSize = 50.dp - 64.dp
|
||||
|
||||
val Size0dp = 0.dp
|
||||
val Size5dp = 5.dp
|
||||
val Size6dp = 6.dp
|
||||
val Size10dp = 10.dp
|
||||
val Size12dp = 12.dp
|
||||
val Size13dp = 13.dp
|
||||
|
||||
@@ -21,6 +21,7 @@
|
||||
<string name="impersonation">Impersonation</string>
|
||||
<string name="illegal_behavior">Illegal Behavior</string>
|
||||
<string name="other">Other</string>
|
||||
<string name="harassment">Harassment</string>
|
||||
<string name="unknown">Unknown</string>
|
||||
<string name="relay_icon">Relay Icon</string>
|
||||
<string name="unknown_author">Unknown Author</string>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
[versions]
|
||||
accompanistAdaptive = "0.37.2"
|
||||
activityCompose = "1.10.1"
|
||||
agp = "8.8.2"
|
||||
agp = "8.9.0"
|
||||
android-compileSdk = "35"
|
||||
android-minSdk = "26"
|
||||
android-targetSdk = "35"
|
||||
@@ -13,10 +13,10 @@ benchmark = "1.3.3"
|
||||
benchmarkJunit4 = "1.3.3"
|
||||
biometricKtx = "1.2.0-alpha05"
|
||||
coil = "3.1.0"
|
||||
composeBom = "2025.02.00"
|
||||
composeBom = "2025.03.00"
|
||||
coreKtx = "1.15.0"
|
||||
espressoCore = "3.6.1"
|
||||
firebaseBom = "33.9.0"
|
||||
firebaseBom = "33.10.0"
|
||||
fragmentKtx = "1.8.6"
|
||||
gms = "4.4.2"
|
||||
jacksonModuleKotlin = "2.18.2"
|
||||
@@ -33,9 +33,9 @@ lifecycleRuntimeKtx = "2.8.7"
|
||||
lightcompressor = "1.3.2"
|
||||
markdown = "077a2cde64"
|
||||
media3 = "1.5.1"
|
||||
mockk = "1.13.16"
|
||||
mockk = "1.13.17"
|
||||
kotlinx-coroutines-test = "1.10.1"
|
||||
navigationCompose = "2.8.8"
|
||||
navigationCompose = "2.8.9"
|
||||
okhttp = "5.0.0-alpha.14"
|
||||
runner = "1.6.2"
|
||||
rfc3986 = "0.1.2"
|
||||
@@ -46,9 +46,9 @@ torAndroid = "0.4.8.12"
|
||||
translate = "17.0.3"
|
||||
unifiedpush = "2.3.1"
|
||||
urlDetector = "0.1.23"
|
||||
vico-charts = "2.0.1"
|
||||
vico-charts = "2.0.3"
|
||||
zelory = "3.0.1"
|
||||
zoomable = "2.1.0"
|
||||
zoomable = "2.3.0"
|
||||
zxing = "3.5.3"
|
||||
zxingAndroidEmbedded = "4.3.0"
|
||||
windowCoreAndroid = "1.3.0"
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
#Wed Jan 04 09:23:50 EST 2023
|
||||
distributionBase=GRADLE_USER_HOME
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-8.10.2-bin.zip
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-8.11.1-bin.zip
|
||||
distributionPath=wrapper/dists
|
||||
zipStorePath=wrapper/dists
|
||||
zipStoreBase=GRADLE_USER_HOME
|
||||
|
||||
+6
-2
@@ -20,14 +20,18 @@
|
||||
*/
|
||||
package com.vitorpamplona.quartz.experimental.audio.header.tags
|
||||
|
||||
import com.vitorpamplona.quartz.nip01Core.core.has
|
||||
import com.vitorpamplona.quartz.utils.ensure
|
||||
|
||||
class DownloadUrlTag {
|
||||
companion object {
|
||||
const val TAG_NAME = "download_url"
|
||||
const val TAG_SIZE = 2
|
||||
|
||||
@JvmStatic
|
||||
fun parse(tag: Array<String>): String? {
|
||||
if (tag.size < TAG_SIZE || tag[0] != TAG_NAME) return null
|
||||
ensure(tag.has(1)) { return null }
|
||||
ensure(tag[0] == TAG_NAME) { return null }
|
||||
ensure(tag[1].isNotEmpty()) { return null }
|
||||
return tag[1]
|
||||
}
|
||||
|
||||
|
||||
+6
-2
@@ -20,14 +20,18 @@
|
||||
*/
|
||||
package com.vitorpamplona.quartz.experimental.audio.header.tags
|
||||
|
||||
import com.vitorpamplona.quartz.nip01Core.core.has
|
||||
import com.vitorpamplona.quartz.utils.ensure
|
||||
|
||||
class StreamUrlTag {
|
||||
companion object {
|
||||
const val TAG_NAME = "stream_url"
|
||||
const val TAG_SIZE = 2
|
||||
|
||||
@JvmStatic
|
||||
fun parse(tag: Array<String>): String? {
|
||||
if (tag.size < TAG_SIZE || tag[0] != TAG_NAME) return null
|
||||
ensure(tag.has(1)) { return null }
|
||||
ensure(tag[0] == TAG_NAME) { return null }
|
||||
ensure(tag[1].isNotEmpty()) { return null }
|
||||
return tag[1]
|
||||
}
|
||||
|
||||
|
||||
+5
-3
@@ -21,7 +21,9 @@
|
||||
package com.vitorpamplona.quartz.experimental.audio.header.tags
|
||||
|
||||
import com.fasterxml.jackson.module.kotlin.readValue
|
||||
import com.vitorpamplona.quartz.nip01Core.core.has
|
||||
import com.vitorpamplona.quartz.nip01Core.jackson.EventMapper
|
||||
import com.vitorpamplona.quartz.utils.ensure
|
||||
|
||||
class WaveformTag(
|
||||
val wave: List<Int>,
|
||||
@@ -30,12 +32,12 @@ class WaveformTag(
|
||||
|
||||
companion object {
|
||||
const val TAG_NAME = "waveform"
|
||||
const val TAG_SIZE = 2
|
||||
|
||||
@JvmStatic
|
||||
fun parse(tag: Array<String>): WaveformTag? {
|
||||
if (tag.size < TAG_SIZE || tag[0] != TAG_NAME) return null
|
||||
if (tag[1].isEmpty()) return null
|
||||
ensure(tag.has(1)) { return null }
|
||||
ensure(tag[0] == TAG_NAME) { return null }
|
||||
ensure(tag[1].isNotEmpty()) { return null }
|
||||
val wave = runCatching { EventMapper.mapper.readValue<List<Int>>(tag[1]) }.getOrNull()
|
||||
if (wave.isNullOrEmpty()) return null
|
||||
return WaveformTag(wave)
|
||||
|
||||
+6
-2
@@ -20,14 +20,18 @@
|
||||
*/
|
||||
package com.vitorpamplona.quartz.experimental.audio.track.tags
|
||||
|
||||
import com.vitorpamplona.quartz.nip01Core.core.has
|
||||
import com.vitorpamplona.quartz.utils.ensure
|
||||
|
||||
class CoverTag {
|
||||
companion object {
|
||||
const val TAG_NAME = "cover"
|
||||
const val TAG_SIZE = 2
|
||||
|
||||
@JvmStatic
|
||||
fun parse(tag: Array<String>): String? {
|
||||
if (tag.size < TAG_SIZE || tag[0] != TAG_NAME) return null
|
||||
ensure(tag.has(1)) { return null }
|
||||
ensure(tag[0] == TAG_NAME) { return null }
|
||||
ensure(tag[1].isNotEmpty()) { return null }
|
||||
return tag[1]
|
||||
}
|
||||
|
||||
|
||||
+6
-2
@@ -20,14 +20,18 @@
|
||||
*/
|
||||
package com.vitorpamplona.quartz.experimental.audio.track.tags
|
||||
|
||||
import com.vitorpamplona.quartz.nip01Core.core.has
|
||||
import com.vitorpamplona.quartz.utils.ensure
|
||||
|
||||
class MediaTag {
|
||||
companion object {
|
||||
const val TAG_NAME = "media"
|
||||
const val TAG_SIZE = 2
|
||||
|
||||
@JvmStatic
|
||||
fun parse(tag: Array<String>): String? {
|
||||
if (tag.size < TAG_SIZE || tag[0] != TAG_NAME) return null
|
||||
ensure(tag.has(1)) { return null }
|
||||
ensure(tag[0] == TAG_NAME) { return null }
|
||||
ensure(tag[1].isNotEmpty()) { return null }
|
||||
return tag[1]
|
||||
}
|
||||
|
||||
|
||||
+9
-7
@@ -23,9 +23,10 @@ package com.vitorpamplona.quartz.experimental.audio.track.tags
|
||||
import androidx.compose.runtime.Immutable
|
||||
import com.vitorpamplona.quartz.nip01Core.core.HexKey
|
||||
import com.vitorpamplona.quartz.nip01Core.core.Tag
|
||||
import com.vitorpamplona.quartz.nip01Core.core.isNotName
|
||||
import com.vitorpamplona.quartz.nip01Core.core.has
|
||||
import com.vitorpamplona.quartz.nip01Core.tags.people.PubKeyReferenceTag
|
||||
import com.vitorpamplona.quartz.utils.arrayOfNotNull
|
||||
import com.vitorpamplona.quartz.utils.ensure
|
||||
|
||||
@Immutable
|
||||
data class ParticipantTag(
|
||||
@@ -36,26 +37,27 @@ data class ParticipantTag(
|
||||
|
||||
companion object {
|
||||
const val TAG_NAME = "p"
|
||||
const val TAG_SIZE = 2
|
||||
|
||||
@JvmStatic
|
||||
fun parse(tag: Tag): ParticipantTag? {
|
||||
if (tag.isNotName(TAG_NAME, TAG_SIZE)) return null
|
||||
if (tag[1].length != 64) return null
|
||||
ensure(tag.has(1)) { return null }
|
||||
ensure(tag[0] == TAG_NAME) { return null }
|
||||
ensure(tag[1].length == 64) { return null }
|
||||
return ParticipantTag(tag[1], tag.getOrNull(2))
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
fun parseKey(tag: Tag): String? {
|
||||
if (tag.isNotName(TAG_NAME, TAG_SIZE)) return null
|
||||
if (tag[1].length != 64) return null
|
||||
ensure(tag.has(1)) { return null }
|
||||
ensure(tag[0] == TAG_NAME) { return null }
|
||||
ensure(tag[1].length == 64) { return null }
|
||||
return tag[1]
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
fun assemble(
|
||||
pubkey: HexKey,
|
||||
relayHint: String?,
|
||||
relayHint: String? = null,
|
||||
) = arrayOfNotNull(TAG_NAME, pubkey, relayHint)
|
||||
}
|
||||
}
|
||||
|
||||
+6
-2
@@ -20,14 +20,18 @@
|
||||
*/
|
||||
package com.vitorpamplona.quartz.experimental.audio.track.tags
|
||||
|
||||
import com.vitorpamplona.quartz.nip01Core.core.has
|
||||
import com.vitorpamplona.quartz.utils.ensure
|
||||
|
||||
class PriceTag {
|
||||
companion object {
|
||||
const val TAG_NAME = "price"
|
||||
const val TAG_SIZE = 2
|
||||
|
||||
@JvmStatic
|
||||
fun parse(tag: Array<String>): String? {
|
||||
if (tag.size < TAG_SIZE || tag[0] != TAG_NAME) return null
|
||||
ensure(tag.has(1)) { return null }
|
||||
ensure(tag[0] == TAG_NAME) { return null }
|
||||
ensure(tag[1].isNotEmpty()) { return null }
|
||||
return tag[1]
|
||||
}
|
||||
|
||||
|
||||
+6
-2
@@ -20,14 +20,18 @@
|
||||
*/
|
||||
package com.vitorpamplona.quartz.experimental.audio.track.tags
|
||||
|
||||
import com.vitorpamplona.quartz.nip01Core.core.has
|
||||
import com.vitorpamplona.quartz.utils.ensure
|
||||
|
||||
class TypeTag {
|
||||
companion object {
|
||||
const val TAG_NAME = "c"
|
||||
const val TAG_SIZE = 2
|
||||
|
||||
@JvmStatic
|
||||
fun parse(tag: Array<String>): String? {
|
||||
if (tag.size < TAG_SIZE || tag[0] != TAG_NAME) return null
|
||||
ensure(tag.has(1)) { return null }
|
||||
ensure(tag[0] == TAG_NAME) { return null }
|
||||
ensure(tag[1].isNotEmpty()) { return null }
|
||||
return tag[1]
|
||||
}
|
||||
|
||||
|
||||
@@ -20,16 +20,19 @@
|
||||
*/
|
||||
package com.vitorpamplona.quartz.experimental.bounties
|
||||
|
||||
import com.vitorpamplona.quartz.nip01Core.core.has
|
||||
import com.vitorpamplona.quartz.utils.ensure
|
||||
import java.math.BigDecimal
|
||||
|
||||
class RewardTag {
|
||||
companion object {
|
||||
const val TAG_NAME = "reward"
|
||||
const val TAG_SIZE = 2
|
||||
|
||||
@JvmStatic
|
||||
fun parse(tag: Array<String>): BigDecimal? {
|
||||
if (tag.size < TAG_SIZE || tag[0] != TAG_NAME) return null
|
||||
ensure(tag.has(1)) { return null }
|
||||
ensure(tag[0] == TAG_NAME) { return null }
|
||||
ensure(tag[1].isNotEmpty()) { return null }
|
||||
|
||||
return runCatching { BigDecimal(tag[1]) }.getOrNull()
|
||||
}
|
||||
|
||||
+112
@@ -0,0 +1,112 @@
|
||||
/**
|
||||
* 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.quartz.experimental.decoupling
|
||||
|
||||
import com.vitorpamplona.quartz.experimental.decoupling.setup.EncryptionKeyListEvent
|
||||
import com.vitorpamplona.quartz.nip01Core.core.HexKey
|
||||
import com.vitorpamplona.quartz.nip01Core.core.hexToByteArray
|
||||
import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner
|
||||
import com.vitorpamplona.quartz.nip44Encryption.Nip44
|
||||
|
||||
class DecoupledCipher {
|
||||
fun innerEncrypt(
|
||||
content: String,
|
||||
privKey: ByteArray,
|
||||
toPublicKey: HexKey,
|
||||
) = Nip44
|
||||
.encrypt(content, privKey, toPublicKey.hexToByteArray())
|
||||
.encodePayload()
|
||||
|
||||
fun innerDecrypt(
|
||||
ciphertext: String,
|
||||
privKey: ByteArray,
|
||||
fromPublicKey: HexKey,
|
||||
) = Nip44.decrypt(
|
||||
payload = ciphertext,
|
||||
privateKey = privKey,
|
||||
pubKey = fromPublicKey.hexToByteArray(),
|
||||
)
|
||||
|
||||
fun encrypt(
|
||||
decryptedContent: String,
|
||||
toPublicKey: HexKey,
|
||||
fromKeyList: EncryptionKeyListEvent,
|
||||
toKeyList: EncryptionKeyListEvent,
|
||||
signer: NostrSigner,
|
||||
onReady: (String) -> Unit,
|
||||
) {
|
||||
val toKeys = toKeyList.keys()
|
||||
val sendToKey = if (toKeys.isEmpty()) toKeyList.pubKey else toKeys.random().pubkey
|
||||
|
||||
val fromKeys = fromKeyList.keys()
|
||||
|
||||
// uses the main key
|
||||
if (fromKeys.isEmpty()) {
|
||||
signer.nip44Encrypt(decryptedContent, sendToKey, onReady)
|
||||
} else {
|
||||
val keyToUse = fromKeys.random()
|
||||
|
||||
EncryptionKeyCache.getOrLoad(
|
||||
deriveFromPubKey = signer.pubKey,
|
||||
nonce = keyToUse.nonce,
|
||||
load = { onLoaded ->
|
||||
signer.deriveKey(keyToUse.nonce) { newPrivKey ->
|
||||
onLoaded(newPrivKey.hexToByteArray())
|
||||
}
|
||||
},
|
||||
) { derivedPrivKey ->
|
||||
onReady(innerEncrypt(decryptedContent, derivedPrivKey, sendToKey))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun decrypt(
|
||||
encryptedContent: String,
|
||||
fromPublicKey: HexKey,
|
||||
toPublicKey: HexKey,
|
||||
fromKeyList: EncryptionKeyListEvent,
|
||||
toEncryptedKeyList: EncryptionKeyListEvent,
|
||||
signer: NostrSigner,
|
||||
onReady: (String) -> Unit,
|
||||
) {
|
||||
val fromKeys = fromKeyList.keys()
|
||||
val sentFromKey = if (fromKeys.isEmpty()) fromKeyList.pubKey else fromKeys.random().pubkey
|
||||
|
||||
val keyToUse = toEncryptedKeyList.keys().firstOrNull { it.pubkey == toPublicKey }
|
||||
|
||||
// uses the main key
|
||||
if (signer.pubKey == toPublicKey) {
|
||||
signer.nip44Decrypt(encryptedContent, sentFromKey, onReady)
|
||||
} else if (keyToUse != null) {
|
||||
EncryptionKeyCache.getOrLoad(
|
||||
deriveFromPubKey = signer.pubKey,
|
||||
nonce = keyToUse.nonce,
|
||||
load = { onLoaded ->
|
||||
signer.deriveKey(keyToUse.nonce) { newPrivKey ->
|
||||
onLoaded(newPrivKey.hexToByteArray())
|
||||
}
|
||||
},
|
||||
) { derivedPrivKey ->
|
||||
innerDecrypt(encryptedContent, derivedPrivKey, sentFromKey)?.let { onReady(it) }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+68
@@ -0,0 +1,68 @@
|
||||
/**
|
||||
* 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.quartz.experimental.decoupling
|
||||
|
||||
import android.util.LruCache
|
||||
import com.vitorpamplona.quartz.nip01Core.core.HexKey
|
||||
|
||||
object EncryptionKeyCache {
|
||||
private val sharedNonceKeyCache = LruCache<EncryptionKeyCacheKey, ByteArray>(50)
|
||||
|
||||
private fun idx(
|
||||
key: HexKey,
|
||||
nonce: HexKey,
|
||||
) = EncryptionKeyCacheKey(key, nonce)
|
||||
|
||||
fun put(
|
||||
deriveFromPubKey: HexKey,
|
||||
nonce: HexKey,
|
||||
privKey: ByteArray,
|
||||
) = sharedNonceKeyCache.put(idx(deriveFromPubKey, nonce), privKey)
|
||||
|
||||
fun get(
|
||||
deriveFromPubKey: HexKey,
|
||||
nonce: HexKey,
|
||||
) = sharedNonceKeyCache.get(idx(deriveFromPubKey, nonce))
|
||||
|
||||
inline fun getOrLoad(
|
||||
deriveFromPubKey: HexKey,
|
||||
nonce: HexKey,
|
||||
load: (onLoaded: (privKey: ByteArray) -> Unit) -> Unit,
|
||||
crossinline whenReady: (privKey: ByteArray) -> Unit,
|
||||
) {
|
||||
val cachedPrivKey = get(deriveFromPubKey, nonce)
|
||||
if (cachedPrivKey != null) {
|
||||
whenReady(cachedPrivKey)
|
||||
return
|
||||
}
|
||||
|
||||
load { newPrivKey ->
|
||||
put(deriveFromPubKey, nonce, newPrivKey)
|
||||
whenReady(newPrivKey)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// keeps a cache per signed-in user
|
||||
private data class EncryptionKeyCacheKey(
|
||||
val pubkey: HexKey,
|
||||
val nonce: HexKey,
|
||||
)
|
||||
+46
@@ -0,0 +1,46 @@
|
||||
/**
|
||||
* 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.quartz.experimental.decoupling
|
||||
|
||||
import com.vitorpamplona.quartz.nip01Core.crypto.KeyPair
|
||||
import com.vitorpamplona.quartz.nip44Encryption.crypto.Hkdf
|
||||
import com.vitorpamplona.quartz.utils.sha256.sha256
|
||||
|
||||
class EncryptionKeyDerivation {
|
||||
companion object {
|
||||
private val saltPrefix = "nip44kd".toByteArray(Charsets.UTF_8)
|
||||
|
||||
fun derivePrivateKey(
|
||||
privKey: ByteArray,
|
||||
nonce: ByteArray?,
|
||||
): ByteArray {
|
||||
if (nonce == null) {
|
||||
return privKey
|
||||
}
|
||||
return sha256(Hkdf().extract(privKey, saltPrefix + nonce))
|
||||
}
|
||||
|
||||
fun deriveKeyPair(
|
||||
privKey: ByteArray,
|
||||
nonce: ByteArray?,
|
||||
): KeyPair = KeyPair(privKey = derivePrivateKey(privKey, nonce))
|
||||
}
|
||||
}
|
||||
+80
@@ -0,0 +1,80 @@
|
||||
/**
|
||||
* 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.quartz.experimental.decoupling.setup
|
||||
|
||||
import androidx.compose.runtime.Immutable
|
||||
import com.vitorpamplona.quartz.experimental.decoupling.setup.tags.KeyTag
|
||||
import com.vitorpamplona.quartz.nip01Core.core.BaseAddressableEvent
|
||||
import com.vitorpamplona.quartz.nip01Core.core.HexKey
|
||||
import com.vitorpamplona.quartz.nip01Core.core.TagArrayBuilder
|
||||
import com.vitorpamplona.quartz.nip01Core.signers.eventTemplate
|
||||
import com.vitorpamplona.quartz.nip01Core.signers.eventUpdate
|
||||
import com.vitorpamplona.quartz.nip31Alts.alt
|
||||
import com.vitorpamplona.quartz.utils.TimeUtils
|
||||
|
||||
@Immutable
|
||||
class EncryptionKeyListEvent(
|
||||
id: HexKey,
|
||||
pubKey: HexKey,
|
||||
createdAt: Long,
|
||||
tags: Array<Array<String>>,
|
||||
content: String,
|
||||
sig: HexKey,
|
||||
) : BaseAddressableEvent(id, pubKey, createdAt, KIND, tags, content, sig) {
|
||||
fun keys() = tags.mapNotNull(KeyTag::parse)
|
||||
|
||||
companion object {
|
||||
const val KIND = 10044
|
||||
const val ALT = "Encryption keys"
|
||||
|
||||
fun add(
|
||||
key: KeyTag,
|
||||
currentEvent: EncryptionKeyListEvent,
|
||||
createdAt: Long = TimeUtils.now(),
|
||||
initializer: TagArrayBuilder<EncryptionKeyListEvent>.() -> Unit = {},
|
||||
) = eventUpdate(currentEvent, createdAt) {
|
||||
alt(ALT)
|
||||
key(key)
|
||||
initializer()
|
||||
}
|
||||
|
||||
fun remove(
|
||||
key: KeyTag,
|
||||
currentEvent: EncryptionKeyListEvent,
|
||||
createdAt: Long = TimeUtils.now(),
|
||||
initializer: TagArrayBuilder<EncryptionKeyListEvent>.() -> Unit = {},
|
||||
) = eventUpdate(currentEvent, createdAt) {
|
||||
alt(ALT)
|
||||
removeIf(KeyTag::isSameKey, key.toTagArray())
|
||||
initializer()
|
||||
}
|
||||
|
||||
fun build(
|
||||
keys: List<KeyTag>,
|
||||
createdAt: Long = TimeUtils.now(),
|
||||
initializer: TagArrayBuilder<EncryptionKeyListEvent>.() -> Unit = {},
|
||||
) = eventTemplate(KIND, "", createdAt) {
|
||||
alt(ALT)
|
||||
keys(keys)
|
||||
initializer()
|
||||
}
|
||||
}
|
||||
}
|
||||
+34
@@ -0,0 +1,34 @@
|
||||
/**
|
||||
* 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.quartz.experimental.decoupling.setup
|
||||
|
||||
import com.vitorpamplona.quartz.experimental.decoupling.setup.tags.KeyTag
|
||||
import com.vitorpamplona.quartz.nip01Core.core.HexKey
|
||||
import com.vitorpamplona.quartz.nip01Core.core.TagArrayBuilder
|
||||
|
||||
fun TagArrayBuilder<EncryptionKeyListEvent>.key(
|
||||
key: HexKey,
|
||||
nonce: HexKey,
|
||||
) = add(KeyTag.assemble(key, nonce))
|
||||
|
||||
fun TagArrayBuilder<EncryptionKeyListEvent>.key(key: KeyTag) = add(key.toTagArray())
|
||||
|
||||
fun TagArrayBuilder<EncryptionKeyListEvent>.keys(keys: List<KeyTag>) = addAll(keys.map { it.toTagArray() })
|
||||
+62
@@ -0,0 +1,62 @@
|
||||
/**
|
||||
* 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.quartz.experimental.decoupling.setup.tags
|
||||
|
||||
import com.vitorpamplona.quartz.nip01Core.core.HexKey
|
||||
import com.vitorpamplona.quartz.nip01Core.core.has
|
||||
import com.vitorpamplona.quartz.utils.ensure
|
||||
|
||||
class KeyTag(
|
||||
val pubkey: HexKey,
|
||||
val nonce: HexKey,
|
||||
) {
|
||||
fun toTagArray() = assemble(pubkey, nonce)
|
||||
|
||||
companion object {
|
||||
const val TAG_NAME = "n"
|
||||
|
||||
fun isSameKey(
|
||||
tag1: Array<String>,
|
||||
tag2: Array<String>,
|
||||
): Boolean {
|
||||
ensure(tag1.has(1)) { return false }
|
||||
ensure(tag2.has(1)) { return false }
|
||||
ensure(tag1[0] == tag2[0]) { return false }
|
||||
ensure(tag1[1] == tag2[1]) { return false }
|
||||
return true
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
fun parse(tag: Array<String>): KeyTag? {
|
||||
ensure(tag.has(2)) { return null }
|
||||
ensure(tag[0] == TAG_NAME) { return null }
|
||||
ensure(tag[1].isNotEmpty()) { return null }
|
||||
ensure(tag[2].isNotEmpty()) { return null }
|
||||
return KeyTag(tag[1], tag[2])
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
fun assemble(
|
||||
key: HexKey,
|
||||
nonce: HexKey,
|
||||
) = arrayOf(TAG_NAME, key, nonce)
|
||||
}
|
||||
}
|
||||
+2
@@ -47,6 +47,8 @@ class PrivateOutboxRelayListEvent(
|
||||
) : BaseReplaceableEvent(id, pubKey, createdAt, KIND, tags, content, sig) {
|
||||
@Transient private var privateTagsCache: Array<Array<String>>? = null
|
||||
|
||||
override fun isContentEncoded() = true
|
||||
|
||||
override fun countMemory(): Long =
|
||||
super.countMemory() +
|
||||
pointerSizeInBytes + (privateTagsCache?.sumOf { pointerSizeInBytes + it.sumOf { pointerSizeInBytes + it.bytesUsedInMemory() } } ?: 0)
|
||||
|
||||
@@ -24,7 +24,7 @@ import com.vitorpamplona.quartz.nip10Notes.tags.MarkedETag
|
||||
import com.vitorpamplona.quartz.nip10Notes.tags.MarkedETag.MARKER
|
||||
|
||||
fun MarkedETag.Companion.parseFork(tag: Array<String>): MarkedETag? {
|
||||
if (tag.size < TAG_SIZE || tag[0] != TAG_NAME) return null
|
||||
if (tag.size < 4 || tag[0] != "e") return null
|
||||
if (tag[ORDER_MARKER] != MARKER.FORK.code) return null
|
||||
// ["e", id hex, relay hint, marker, pubkey]
|
||||
return MarkedETag(
|
||||
|
||||
+6
-2
@@ -20,6 +20,9 @@
|
||||
*/
|
||||
package com.vitorpamplona.quartz.experimental.interactiveStories.tags
|
||||
|
||||
import com.vitorpamplona.quartz.nip01Core.core.has
|
||||
import com.vitorpamplona.quartz.utils.ensure
|
||||
|
||||
class ReadStatusTag {
|
||||
enum class STATUS(
|
||||
val value: String,
|
||||
@@ -34,11 +37,12 @@ class ReadStatusTag {
|
||||
|
||||
companion object {
|
||||
const val TAG_NAME = "status"
|
||||
const val TAG_SIZE = 2
|
||||
|
||||
@JvmStatic
|
||||
fun parse(tag: Array<String>): String? {
|
||||
if (tag.size < TAG_SIZE || tag[0] != TAG_NAME) return null
|
||||
ensure(tag.has(1)) { return null }
|
||||
ensure(tag[0] == TAG_NAME) { return null }
|
||||
ensure(tag[1].isNotEmpty()) { return null }
|
||||
return tag[1]
|
||||
}
|
||||
|
||||
|
||||
-1
@@ -60,7 +60,6 @@ data class RootSceneTag(
|
||||
|
||||
companion object {
|
||||
const val TAG_NAME = "A"
|
||||
const val TAG_SIZE = 2
|
||||
|
||||
fun assembleATagId(
|
||||
kind: Int,
|
||||
|
||||
-1
@@ -35,7 +35,6 @@ class StoryOptionTag(
|
||||
|
||||
companion object {
|
||||
const val TAG_NAME = "option"
|
||||
const val TAG_SIZE = 3
|
||||
|
||||
@JvmStatic
|
||||
fun parse(tag: Array<String>): StoryOptionTag? {
|
||||
|
||||
@@ -20,14 +20,18 @@
|
||||
*/
|
||||
package com.vitorpamplona.quartz.experimental.nns.tags
|
||||
|
||||
import com.vitorpamplona.quartz.nip01Core.core.has
|
||||
import com.vitorpamplona.quartz.utils.ensure
|
||||
|
||||
class IPv4Tag {
|
||||
companion object {
|
||||
const val TAG_NAME = "ip4"
|
||||
const val TAG_SIZE = 2
|
||||
|
||||
@JvmStatic
|
||||
fun parse(tag: Array<String>): String? {
|
||||
if (tag.size < TAG_SIZE || tag[0] != TAG_NAME) return null
|
||||
ensure(tag.has(1)) { return null }
|
||||
ensure(tag[0] == TAG_NAME) { return null }
|
||||
ensure(tag[1].isNotEmpty()) { return null }
|
||||
return tag[1]
|
||||
}
|
||||
|
||||
|
||||
@@ -20,14 +20,18 @@
|
||||
*/
|
||||
package com.vitorpamplona.quartz.experimental.nns.tags
|
||||
|
||||
import com.vitorpamplona.quartz.nip01Core.core.has
|
||||
import com.vitorpamplona.quartz.utils.ensure
|
||||
|
||||
class IPv6Tag {
|
||||
companion object {
|
||||
const val TAG_NAME = "ip6"
|
||||
const val TAG_SIZE = 2
|
||||
|
||||
@JvmStatic
|
||||
fun parse(tag: Array<String>): String? {
|
||||
if (tag.size < TAG_SIZE || tag[0] != TAG_NAME) return null
|
||||
ensure(tag.has(1)) { return null }
|
||||
ensure(tag[0] == TAG_NAME) { return null }
|
||||
ensure(tag[1].isNotEmpty()) { return null }
|
||||
return tag[1]
|
||||
}
|
||||
|
||||
|
||||
@@ -20,14 +20,18 @@
|
||||
*/
|
||||
package com.vitorpamplona.quartz.experimental.nns.tags
|
||||
|
||||
import com.vitorpamplona.quartz.nip01Core.core.has
|
||||
import com.vitorpamplona.quartz.utils.ensure
|
||||
|
||||
class VersionTag {
|
||||
companion object {
|
||||
const val TAG_NAME = "version"
|
||||
const val TAG_SIZE = 2
|
||||
|
||||
@JvmStatic
|
||||
fun parse(tag: Array<String>): String? {
|
||||
if (tag.size < TAG_SIZE || tag[0] != TAG_NAME) return null
|
||||
ensure(tag.has(1)) { return null }
|
||||
ensure(tag[0] == TAG_NAME) { return null }
|
||||
ensure(tag[1].isNotEmpty()) { return null }
|
||||
return tag[1]
|
||||
}
|
||||
|
||||
|
||||
+6
-2
@@ -20,14 +20,18 @@
|
||||
*/
|
||||
package com.vitorpamplona.quartz.experimental.relationshipStatus.tags
|
||||
|
||||
import com.vitorpamplona.quartz.nip01Core.core.has
|
||||
import com.vitorpamplona.quartz.utils.ensure
|
||||
|
||||
class PetnameTag {
|
||||
companion object {
|
||||
const val TAG_NAME = "petname"
|
||||
const val TAG_SIZE = 2
|
||||
|
||||
@JvmStatic
|
||||
fun parse(tag: Array<String>): String? {
|
||||
if (tag.size < TAG_SIZE || tag[0] != TAG_NAME) return null
|
||||
ensure(tag.has(1)) { return null }
|
||||
ensure(tag[0] == TAG_NAME) { return null }
|
||||
ensure(tag[1].isNotEmpty()) { return null }
|
||||
return tag[1]
|
||||
}
|
||||
|
||||
|
||||
+6
-2
@@ -20,14 +20,18 @@
|
||||
*/
|
||||
package com.vitorpamplona.quartz.experimental.relationshipStatus.tags
|
||||
|
||||
import com.vitorpamplona.quartz.nip01Core.core.has
|
||||
import com.vitorpamplona.quartz.utils.ensure
|
||||
|
||||
class RankTag {
|
||||
companion object {
|
||||
const val TAG_NAME = "rank"
|
||||
const val TAG_SIZE = 2
|
||||
|
||||
@JvmStatic
|
||||
fun parse(tag: Array<String>): Int? {
|
||||
if (tag.size < TAG_SIZE || tag[0] != TAG_NAME) return null
|
||||
ensure(tag.has(1)) { return null }
|
||||
ensure(tag[0] == TAG_NAME) { return null }
|
||||
ensure(tag[1].isNotEmpty()) { return null }
|
||||
return tag[1].toIntOrNull()
|
||||
}
|
||||
|
||||
|
||||
+6
-2
@@ -20,14 +20,18 @@
|
||||
*/
|
||||
package com.vitorpamplona.quartz.experimental.relationshipStatus.tags
|
||||
|
||||
import com.vitorpamplona.quartz.nip01Core.core.has
|
||||
import com.vitorpamplona.quartz.utils.ensure
|
||||
|
||||
class SummaryTag {
|
||||
companion object {
|
||||
const val TAG_NAME = "summary"
|
||||
const val TAG_SIZE = 2
|
||||
|
||||
@JvmStatic
|
||||
fun parse(tag: Array<String>): String? {
|
||||
if (tag.size < TAG_SIZE || tag[0] != TAG_NAME) return null
|
||||
ensure(tag.has(1)) { return null }
|
||||
ensure(tag[0] == TAG_NAME) { return null }
|
||||
ensure(tag[1].isNotEmpty()) { return null }
|
||||
return tag[1]
|
||||
}
|
||||
|
||||
|
||||
+6
-2
@@ -20,14 +20,18 @@
|
||||
*/
|
||||
package com.vitorpamplona.quartz.experimental.zapPolls.tags
|
||||
|
||||
import com.vitorpamplona.quartz.nip01Core.core.has
|
||||
import com.vitorpamplona.quartz.utils.ensure
|
||||
|
||||
class ClosedAtTag {
|
||||
companion object {
|
||||
const val TAG_NAME = "closed_at"
|
||||
const val TAG_SIZE = 2
|
||||
|
||||
@JvmStatic
|
||||
fun parse(tag: Array<String>): Long? {
|
||||
if (tag.size < TAG_SIZE || tag[0] != TAG_NAME) return null
|
||||
ensure(tag.has(1)) { return null }
|
||||
ensure(tag[0] == TAG_NAME) { return null }
|
||||
ensure(tag[1].isNotEmpty()) { return null }
|
||||
return tag[1].toLongOrNull()
|
||||
}
|
||||
|
||||
|
||||
+5
-2
@@ -20,16 +20,19 @@
|
||||
*/
|
||||
package com.vitorpamplona.quartz.experimental.zapPolls.tags
|
||||
|
||||
import com.vitorpamplona.quartz.nip01Core.core.has
|
||||
import com.vitorpamplona.quartz.utils.ensure
|
||||
import kotlin.math.round
|
||||
|
||||
class ConsensusThresholdTag {
|
||||
companion object {
|
||||
const val TAG_NAME = "consensus_threshold"
|
||||
const val TAG_SIZE = 2
|
||||
|
||||
@JvmStatic
|
||||
fun parse(tag: Array<String>): Double? {
|
||||
if (tag.size < TAG_SIZE || tag[0] != TAG_NAME) return null
|
||||
ensure(tag.has(1)) { return null }
|
||||
ensure(tag[0] == TAG_NAME) { return null }
|
||||
ensure(tag[1].isNotEmpty()) { return null }
|
||||
return tag[1].toLongOrNull()?.toDouble()?.div(100)
|
||||
}
|
||||
|
||||
|
||||
+6
-2
@@ -20,14 +20,18 @@
|
||||
*/
|
||||
package com.vitorpamplona.quartz.experimental.zapPolls.tags
|
||||
|
||||
import com.vitorpamplona.quartz.nip01Core.core.has
|
||||
import com.vitorpamplona.quartz.utils.ensure
|
||||
|
||||
class MaximumTag {
|
||||
companion object {
|
||||
const val TAG_NAME = "value_maximum"
|
||||
const val TAG_SIZE = 2
|
||||
|
||||
@JvmStatic
|
||||
fun parse(tag: Array<String>): Long? {
|
||||
if (tag.size < TAG_SIZE || tag[0] != TAG_NAME) return null
|
||||
ensure(tag.has(1)) { return null }
|
||||
ensure(tag[0] == TAG_NAME) { return null }
|
||||
ensure(tag[1].isNotEmpty()) { return null }
|
||||
return tag[1].toLongOrNull()
|
||||
}
|
||||
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user