diff --git a/app/src/main/java/com/vitorpamplona/amethyst/model/LocalCache.kt b/app/src/main/java/com/vitorpamplona/amethyst/model/LocalCache.kt index 83638986e..f366531a5 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/model/LocalCache.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/model/LocalCache.kt @@ -63,6 +63,7 @@ import com.vitorpamplona.quartz.events.DeletionEvent import com.vitorpamplona.quartz.events.EmojiPackEvent import com.vitorpamplona.quartz.events.EmojiPackSelectionEvent import com.vitorpamplona.quartz.events.Event +import com.vitorpamplona.quartz.events.FhirResourceEvent import com.vitorpamplona.quartz.events.FileHeaderEvent import com.vitorpamplona.quartz.events.FileServersEvent import com.vitorpamplona.quartz.events.FileStorageEvent @@ -1363,6 +1364,26 @@ object LocalCache { refreshObservers(note) } + fun consume( + event: FhirResourceEvent, + relay: Relay?, + ) { + val note = getOrCreateNote(event.id) + val author = getOrCreateUser(event.pubKey) + + if (relay != null) { + author.addRelayBeingUsed(relay, event.createdAt) + note.addRelay(relay) + } + + // Already processed this event. + if (note.event != null) return + + note.loadEvent(event, author, emptyList()) + + refreshObservers(note) + } + fun consume( event: HighlightEvent, relay: Relay?, @@ -1981,6 +2002,7 @@ object LocalCache { is EmojiPackEvent -> consume(event, relay) is EmojiPackSelectionEvent -> consume(event, relay) is SealedGossipEvent -> consume(event, relay) + is FhirResourceEvent -> consume(event, relay) is FileHeaderEvent -> consume(event, relay) is FileServersEvent -> consume(event, relay) is FileStorageEvent -> consume(event, relay) diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/note/MiniFhir.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/MiniFhir.kt new file mode 100644 index 000000000..b782a1c19 --- /dev/null +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/MiniFhir.kt @@ -0,0 +1,123 @@ +/** + * 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.note + +import com.fasterxml.jackson.annotation.JsonSubTypes +import com.fasterxml.jackson.annotation.JsonTypeInfo + +@JsonTypeInfo( + use = JsonTypeInfo.Id.NAME, + include = JsonTypeInfo.As.PROPERTY, + property = "resourceType", +) +@JsonSubTypes( + JsonSubTypes.Type(value = Practitioner::class, name = "Practitioner"), + JsonSubTypes.Type(value = Patient::class, name = "Patient"), + JsonSubTypes.Type(value = Bundle::class, name = "Bundle"), + JsonSubTypes.Type(value = VisionPrescription::class, name = "VisionPrescription"), +) +open class Resource( + var resourceType: String? = null, + var id: String = "", +) + +class Practitioner( + resourceType: String? = null, + id: String = "", + var active: Boolean? = null, + var name: ArrayList = arrayListOf(), + var gender: String? = null, +) : Resource(resourceType, id) + +class Patient( + resourceType: String? = null, + id: String = "", + var active: Boolean? = null, + var name: ArrayList = arrayListOf(), + var gender: String? = null, +) : Resource(resourceType, id) + +class HumanName( + var use: String? = null, + var family: String? = null, + var given: ArrayList = arrayListOf(), +) { + fun assembleName(): String { + return given.joinToString(" ") + " " + family + } +} + +class Bundle( + resourceType: String? = null, + id: String = "", + var type: String? = null, + var created: String? = null, + var entry: List = arrayListOf(), +) : Resource(resourceType, id) + +class VisionPrescription( + resourceType: String? = null, + id: String = "", + var status: String? = null, + var created: String? = null, + var patient: Reference? = Reference(), + var encounter: Reference? = Reference(), + var dateWritten: String? = null, + var prescriber: Reference? = Reference(), + var lensSpecification: List = arrayListOf(), +) : Resource(resourceType, id) + +class LensSpecification( + var eye: String? = null, + var sphere: Double? = null, + var cylinder: Double? = null, + var axis: Double? = null, + var add: Double? = null, + var prism: List = arrayListOf(), + // contact lenses + var power: Double? = null, + var backCurve: Double? = null, + var diameter: Double? = null, + var color: String? = null, + var brand: String? = null, + var note: String? = null, +) + +class Prism( + var amount: Double? = null, + var base: String? = null, +) + +class Reference( + var reference: String? = null, +) + +fun findReferenceInDb( + it: String, + db: Map, +): Resource? { + val parts = it.split("/") + return if (parts.size == 2) { + db.get(parts[1].removePrefix("#")) + } else { + db.get(it.removePrefix("#")) + } +} diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/note/NoteCompose.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/NoteCompose.kt index 7725b9ead..fbab305e1 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/note/NoteCompose.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/NoteCompose.kt @@ -34,6 +34,7 @@ import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.ExperimentalLayoutApi import androidx.compose.foundation.layout.FlowRow +import androidx.compose.foundation.layout.IntrinsicSize import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxSize @@ -53,11 +54,13 @@ import androidx.compose.material3.IconButton import androidx.compose.material3.LocalTextStyle import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text +import androidx.compose.material3.VerticalDivider import androidx.compose.material3.darkColorScheme import androidx.compose.material3.lightColorScheme import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.MutableState +import androidx.compose.runtime.State import androidx.compose.runtime.derivedStateOf import androidx.compose.runtime.getValue import androidx.compose.runtime.livedata.observeAsState @@ -66,6 +69,7 @@ import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment +import androidx.compose.ui.Alignment.Companion.Center import androidx.compose.ui.Alignment.Companion.CenterVertically import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip @@ -81,9 +85,11 @@ import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.AnnotatedString import androidx.compose.ui.text.buildAnnotatedString +import androidx.compose.ui.text.capitalize import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import androidx.core.graphics.drawable.toBitmap @@ -94,6 +100,8 @@ import androidx.lifecycle.map import coil.compose.AsyncImage import coil.compose.AsyncImagePainter import coil.request.SuccessResult +import com.fasterxml.jackson.databind.DeserializationFeature +import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper import com.fonfon.kgeohash.GeoHash import com.fonfon.kgeohash.toGeoHash import com.vitorpamplona.amethyst.R @@ -125,6 +133,7 @@ import com.vitorpamplona.amethyst.ui.components.VideoView import com.vitorpamplona.amethyst.ui.components.ZoomableContentView import com.vitorpamplona.amethyst.ui.components.ZoomableImageDialog import com.vitorpamplona.amethyst.ui.components.measureSpaceWidth +import com.vitorpamplona.amethyst.ui.components.mockAccountViewModel import com.vitorpamplona.amethyst.ui.elements.AddButton import com.vitorpamplona.amethyst.ui.elements.DisplayFollowingCommunityInPost import com.vitorpamplona.amethyst.ui.elements.DisplayFollowingHashtagsInPost @@ -206,6 +215,8 @@ import com.vitorpamplona.quartz.events.EmojiPackEvent import com.vitorpamplona.quartz.events.EmojiPackSelectionEvent import com.vitorpamplona.quartz.events.EmojiUrl import com.vitorpamplona.quartz.events.EmptyTagList +import com.vitorpamplona.quartz.events.Event +import com.vitorpamplona.quartz.events.FhirResourceEvent import com.vitorpamplona.quartz.events.FileHeaderEvent import com.vitorpamplona.quartz.events.FileStorageHeaderEvent import com.vitorpamplona.quartz.events.GenericRepostEvent @@ -239,8 +250,11 @@ import kotlinx.collections.immutable.persistentListOf import kotlinx.collections.immutable.toImmutableList import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext import java.io.File import java.net.URL +import java.text.DecimalFormat +import java.text.NumberFormat import java.text.SimpleDateFormat import java.util.Date import java.util.Locale @@ -1176,6 +1190,9 @@ private fun RenderNoteRow( is BadgeAwardEvent -> { RenderBadgeAward(baseNote, backgroundColor, accountViewModel, nav) } + is FhirResourceEvent -> { + RenderFhirResource(baseNote, accountViewModel, nav) + } is PeopleListEvent -> { DisplayPeopleList(baseNote, backgroundColor, accountViewModel, nav) } @@ -4346,3 +4363,223 @@ fun CreateImageHeader( } } } + +@Preview +@Composable +fun RenderEyeGlassesPrescriptionPreview() { + val accountViewModel = mockAccountViewModel() + val nav: (String) -> Unit = {} + + val prescriptionEvent = Event.fromJson("{\"id\":\"0c15d2bc6f7dcc42fa4426d35d30d09840c9afa5b46d100415006e41d6471416\",\"pubkey\":\"bcd4715cc34f98dce7b52fddaf1d826e5ce0263479b7e110a5bd3c3789486ca8\",\"created_at\":1709074097,\"kind\":82,\"tags\":[],\"content\":\"{\\\"resourceType\\\":\\\"Bundle\\\",\\\"id\\\":\\\"bundle-vision-test\\\",\\\"type\\\":\\\"document\\\",\\\"entry\\\":[{\\\"resourceType\\\":\\\"Practitioner\\\",\\\"id\\\":\\\"2\\\",\\\"active\\\":true,\\\"name\\\":[{\\\"use\\\":\\\"official\\\",\\\"family\\\":\\\"Careful\\\",\\\"given\\\":[\\\"Adam\\\"]}],\\\"gender\\\":\\\"male\\\"},{\\\"resourceType\\\":\\\"Patient\\\",\\\"id\\\":\\\"1\\\",\\\"active\\\":true,\\\"name\\\":[{\\\"use\\\":\\\"official\\\",\\\"family\\\":\\\"Duck\\\",\\\"given\\\":[\\\"Donald\\\"]}],\\\"gender\\\":\\\"male\\\"},{\\\"resourceType\\\":\\\"VisionPrescription\\\",\\\"status\\\":\\\"active\\\",\\\"created\\\":\\\"2014-06-15\\\",\\\"patient\\\":{\\\"reference\\\":\\\"#1\\\"},\\\"dateWritten\\\":\\\"2014-06-15\\\",\\\"prescriber\\\":{\\\"reference\\\":\\\"#2\\\"},\\\"lensSpecification\\\":[{\\\"eye\\\":\\\"right\\\",\\\"sphere\\\":-2,\\\"prism\\\":[{\\\"amount\\\":0.5,\\\"base\\\":\\\"down\\\"}],\\\"add\\\":2},{\\\"eye\\\":\\\"left\\\",\\\"sphere\\\":-1,\\\"cylinder\\\":-0.5,\\\"axis\\\":180,\\\"prism\\\":[{\\\"amount\\\":0.5,\\\"base\\\":\\\"up\\\"}],\\\"add\\\":2}]}]}\",\"sig\":\"dc58f6109111ca06920c0c711aeaf8e2ee84975afa60d939828d4e01e2edea738f735fb5b1fcadf6d5496e36ac429abf7020a55fd1e4ed215738afc8d07cb950\"}") as FhirResourceEvent + + RenderFhirResource(prescriptionEvent, accountViewModel, nav) +} + +@Composable +fun RenderFhirResource( + baseNote: Note, + accountViewModel: AccountViewModel, + nav: (String) -> Unit, +) { + val event = baseNote.event as? FhirResourceEvent ?: return + + RenderFhirResource(event, accountViewModel, nav) +} + +@Composable +fun RenderFhirResource( + event: FhirResourceEvent, + accountViewModel: AccountViewModel, + nav: (String) -> Unit, +) { + var baseResource: Resource? by remember(event) { + mutableStateOf(null) + } + + LaunchedEffect(key1 = event) { + withContext(Dispatchers.IO) { + val mapper = + jacksonObjectMapper().configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false) + + try { + baseResource = mapper.readValue(event.content, Resource::class.java) + } catch (e: Exception) { + Log.e("RenderEyeGlassesPrescription", "Parser error", e) + } + } + } + + baseResource?.let { resource -> + when (resource) { + is Bundle -> { + val db = resource.entry.associate { it.id to it } + val vision = resource.entry.filterIsInstance(VisionPrescription::class.java) + + vision.firstOrNull()?.let { + RenderEyeGlassesPrescription(it, db, accountViewModel, nav) + } + } + is VisionPrescription -> { + val db = mapOf(resource.id to resource) + RenderEyeGlassesPrescription(resource, db, accountViewModel, nav) + } + else -> { + } + } + } +} + +@Composable +fun RenderEyeGlassesPrescription( + visionPrescription: VisionPrescription, + db: Map, + accountViewModel: AccountViewModel, + nav: (String) -> Unit, +) { + Column( + modifier = + Modifier + .fillMaxWidth() + .padding(Size10dp), + ) { + val rightEye = visionPrescription.lensSpecification.firstOrNull { it.eye == "right" } + val leftEye = visionPrescription.lensSpecification.firstOrNull { it.eye == "left" } + + Text( + "Eyeglasses Prescription", + modifier = Modifier.padding(4.dp).fillMaxWidth(), + textAlign = TextAlign.Center, + ) + + Spacer(StdVertSpacer) + + visionPrescription.patient?.reference?.let { + val patient = findReferenceInDb(it, db) as? Patient + + patient?.name?.firstOrNull()?.assembleName()?.let { + Text( + text = "Patient: $it", + modifier = Modifier.padding(4.dp).fillMaxWidth(), + ) + } + } + visionPrescription.status?.let { + Text( + text = "Status: ${it.capitalize()}", + modifier = Modifier.padding(4.dp).fillMaxWidth(), + ) + } + + Spacer(DoubleVertSpacer) + + RenderEyeGlassesPrescriptionHeaderRow() + HorizontalDivider(thickness = DividerThickness) + + rightEye?.let { + RenderEyeGlassesPrescriptionRow(data = it) + HorizontalDivider(thickness = DividerThickness) + } + + leftEye?.let { + RenderEyeGlassesPrescriptionRow(data = it) + HorizontalDivider(thickness = DividerThickness) + } + + Spacer(DoubleVertSpacer) + + visionPrescription.prescriber?.reference?.let { + val practitioner = findReferenceInDb(it, db) as? Practitioner + + practitioner?.name?.firstOrNull()?.assembleName()?.let { + Text( + text = "Signed by: $it", + modifier = Modifier.padding(4.dp).fillMaxWidth(), + textAlign = TextAlign.Right, + ) + } + } + } +} + +@Composable +fun RenderEyeGlassesPrescriptionHeaderRow() { + Row( + modifier = Modifier.fillMaxWidth().height(IntrinsicSize.Min), + horizontalArrangement = Arrangement.SpaceBetween, + ) { + Text( + text = "Eye", + modifier = Modifier.padding(4.dp).weight(1f), + ) + VerticalDivider(thickness = DividerThickness) + Text( + text = "Sph", + textAlign = TextAlign.Right, + modifier = Modifier.padding(4.dp).weight(1f), + ) + Text( + text = "Cyl", + textAlign = TextAlign.Right, + modifier = Modifier.padding(4.dp).weight(1f), + ) + Text( + text = "Axis", + textAlign = TextAlign.Right, + modifier = Modifier.padding(4.dp).weight(1f), + ) + VerticalDivider(thickness = DividerThickness) + Text( + text = "Add", + textAlign = TextAlign.Right, + modifier = Modifier.padding(4.dp).weight(1f), + ) + } +} + +@Composable +fun RenderEyeGlassesPrescriptionRow(data: LensSpecification) { + Row( + modifier = Modifier.fillMaxWidth().height(IntrinsicSize.Min), + horizontalArrangement = Arrangement.Center, + verticalAlignment = Alignment.CenterVertically, + ) { + val numberFormat = DecimalFormat("##.00") + val integerFormat = DecimalFormat("###") + + Text( + text = data.eye?.capitalize() ?: "Unknown", + modifier = Modifier.padding(4.dp).weight(1f), + ) + VerticalDivider(thickness = DividerThickness) + Text( + text = formatOrBlank(data.sphere, numberFormat), + textAlign = TextAlign.Right, + modifier = Modifier.padding(4.dp).weight(1f), + ) + Text( + text = formatOrBlank(data.cylinder, numberFormat), + textAlign = TextAlign.Right, + modifier = Modifier.padding(4.dp).weight(1f), + ) + Text( + text = formatOrBlank(data.axis, integerFormat), + textAlign = TextAlign.Right, + modifier = Modifier.padding(4.dp).weight(1f), + ) + VerticalDivider(thickness = DividerThickness) + Text( + text = formatOrBlank(data.add, numberFormat), + textAlign = TextAlign.Right, + modifier = Modifier.padding(4.dp).weight(1f), + ) + } +} + +fun formatOrBlank( + amount: Double?, + numberFormat: NumberFormat, +): String { + if (amount == null) return "" + if (Math.abs(amount) < 0.01) return "" + return numberFormat.format(amount) +} diff --git a/commons/src/main/java/com/vitorpamplona/amethyst/commons/RichTextParser.kt b/commons/src/main/java/com/vitorpamplona/amethyst/commons/RichTextParser.kt index 3532ba941..bb205711b 100644 --- a/commons/src/main/java/com/vitorpamplona/amethyst/commons/RichTextParser.kt +++ b/commons/src/main/java/com/vitorpamplona/amethyst/commons/RichTextParser.kt @@ -319,8 +319,8 @@ class RichTextParser() { Pattern.compile("#([^\\s!@#\$%^&*()=+./,\\[{\\]};:'\"?><]+)(.*)", Pattern.CASE_INSENSITIVE) val acceptedNIP19schemes = - listOf("npub1", "naddr1", "note1", "nprofile1", "nevent1") + - listOf("npub1", "naddr1", "note1", "nprofile1", "nevent1").map { + listOf("npub1", "naddr1", "note1", "nprofile1", "nevent1", "nembed") + + listOf("npub1", "naddr1", "note1", "nprofile1", "nevent1", "nembed").map { it.uppercase() } diff --git a/quartz/src/androidTest/java/com/vitorpamplona/quartz/NIP19EmbedTests.kt b/quartz/src/androidTest/java/com/vitorpamplona/quartz/NIP19EmbedTests.kt index 4e9506987..326fe5bb1 100644 --- a/quartz/src/androidTest/java/com/vitorpamplona/quartz/NIP19EmbedTests.kt +++ b/quartz/src/androidTest/java/com/vitorpamplona/quartz/NIP19EmbedTests.kt @@ -102,6 +102,38 @@ class NIP19EmbedTests { assertEquals(eyeglassesPrescriptionEvent!!.toJson(), decodedNote.toJson()) } + @Test + fun testVisionPrescriptionBundleEmbedEvent() { + val signer = + NostrSignerInternal( + KeyPair(Hex.decode("e8e7197ccc53c9ed4cf9b1c8dce085475fa1ffdd71f2c14e44fe23d0bdf77598")), + ) + + var eyeglassesPrescriptionEvent: Event? = null + + val countDownLatch = CountDownLatch(1) + + FhirResourceEvent.create(fhirPayload = visionPrescriptionBundle, signer = signer) { + eyeglassesPrescriptionEvent = it + countDownLatch.countDown() + } + + Assert.assertTrue(countDownLatch.await(1, TimeUnit.SECONDS)) + + assertNotNull(eyeglassesPrescriptionEvent) + + val bech32 = Nip19Bech32.createNEmbed(eyeglassesPrescriptionEvent!!) + + println(eyeglassesPrescriptionEvent!!.toJson()) + println(bech32) + + val decodedNote = (Nip19Bech32.uriToRoute(bech32)?.entity as Nip19Bech32.NEmbed).event + + assertTrue(decodedNote.hasValidSignature()) + + assertEquals(eyeglassesPrescriptionEvent!!.toJson(), decodedNote.toJson()) + } + /* @Test fun testCompressionSizes() { @@ -138,5 +170,6 @@ class NIP19EmbedTests { assertTrue(true) }*/ - val visionPrescriptionFhir = "{\"resourceType\":\"VisionPrescription\",\"status\":\"active\",\"created\":\"2014-06-15\",\"patient\":{\"reference\":\"Patient/example\"},\"dateWritten\":\"2014-06-15\",\"prescriber\":{\"reference\":\"Practitioner/example\"},\"lensSpecification\":[{\"eye\":\"right\",\"sphere\":-2,\"prism\":[{\"amount\":0.5,\"base\":\"down\"}],\"add\":2},{\"eye\":\"left\",\"sphere\":-1,\"cylinder\":-0.5,\"axis\":180,\"prism\":[{\"amount\":0.5,\"base\":\"up\"}],\"add\":2}]}" + val visionPrescriptionFhir = "{\"resourceType\":\"VisionPrescription\",\"status\":\"active\",\"created\":\"2014-06-15\",\"patient\":{\"reference\":\"Patient/Donald Duck\"},\"dateWritten\":\"2014-06-15\",\"prescriber\":{\"reference\":\"Practitioner/Adam Careful\"},\"lensSpecification\":[{\"eye\":\"right\",\"sphere\":-2,\"prism\":[{\"amount\":0.5,\"base\":\"down\"}],\"add\":2},{\"eye\":\"left\",\"sphere\":-1,\"cylinder\":-0.5,\"axis\":180,\"prism\":[{\"amount\":0.5,\"base\":\"up\"}],\"add\":2}]}" + val visionPrescriptionBundle = "{\"resourceType\":\"Bundle\",\"id\":\"bundle-vision-test\",\"type\":\"document\",\"entry\":[{\"resourceType\":\"Practitioner\",\"id\":\"2\",\"active\":true,\"name\":[{\"use\":\"official\",\"family\":\"Careful\",\"given\":[\"Adam\"]}],\"gender\":\"male\"},{\"resourceType\":\"Patient\",\"id\":\"1\",\"active\":true,\"name\":[{\"use\":\"official\",\"family\":\"Duck\",\"given\":[\"Donald\"]}],\"gender\":\"male\"},{\"resourceType\":\"VisionPrescription\",\"status\":\"active\",\"created\":\"2014-06-15\",\"patient\":{\"reference\":\"#1\"},\"dateWritten\":\"2014-06-15\",\"prescriber\":{\"reference\":\"#2\"},\"lensSpecification\":[{\"eye\":\"right\",\"sphere\":-2,\"prism\":[{\"amount\":0.5,\"base\":\"down\"}],\"add\":2},{\"eye\":\"left\",\"sphere\":-1,\"cylinder\":-0.5,\"axis\":180,\"prism\":[{\"amount\":0.5,\"base\":\"up\"}],\"add\":2}]}]}" } diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/events/EventFactory.kt b/quartz/src/main/java/com/vitorpamplona/quartz/events/EventFactory.kt index ae0b8e962..951901e4e 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/events/EventFactory.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/events/EventFactory.kt @@ -87,6 +87,7 @@ class EventFactory { FileStorageEvent.KIND -> FileStorageEvent(id, pubKey, createdAt, tags, content, sig) FileStorageHeaderEvent.KIND -> FileStorageHeaderEvent(id, pubKey, createdAt, tags, content, sig) + FhirResourceEvent.KIND -> FhirResourceEvent(id, pubKey, createdAt, tags, content, sig) GenericRepostEvent.KIND -> GenericRepostEvent(id, pubKey, createdAt, tags, content, sig) GiftWrapEvent.KIND -> GiftWrapEvent(id, pubKey, createdAt, tags, content, sig) GitIssueEvent.KIND -> GitIssueEvent(id, pubKey, createdAt, tags, content, sig) diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/events/FhirResourceEvent.kt b/quartz/src/main/java/com/vitorpamplona/quartz/events/FhirResourceEvent.kt new file mode 100644 index 000000000..923fb47a7 --- /dev/null +++ b/quartz/src/main/java/com/vitorpamplona/quartz/events/FhirResourceEvent.kt @@ -0,0 +1,51 @@ +/** + * 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.events + +import androidx.compose.runtime.Immutable +import com.vitorpamplona.quartz.encoders.HexKey +import com.vitorpamplona.quartz.signers.NostrSigner +import com.vitorpamplona.quartz.utils.TimeUtils + +@Immutable +class FhirResourceEvent( + id: HexKey, + pubKey: HexKey, + createdAt: Long, + tags: Array>, + content: String, + sig: HexKey, +) : Event(id, pubKey, createdAt, KIND, tags, content, sig) { + companion object { + const val KIND = 82 + + fun create( + fhirPayload: String, + signer: NostrSigner, + createdAt: Long = TimeUtils.now(), + onReady: (FhirResourceEvent) -> Unit, + ) { + val tags = mutableListOf>() + + signer.sign(createdAt, KIND, tags.toTypedArray(), fhirPayload, onReady) + } + } +}