From bc833fadcfe295876e08c9346153abb252af04ef Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Wed, 28 Feb 2024 11:23:05 -0500 Subject: [PATCH 01/98] Fixes event loading for nembeds --- .../amethyst/ui/screen/loggedIn/AccountViewModel.kt | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/AccountViewModel.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/AccountViewModel.kt index 6b6969f78..17ee736cc 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/AccountViewModel.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/AccountViewModel.kt @@ -1003,6 +1003,13 @@ class AccountViewModel(val account: Account, val settings: SettingsState) : View } } + fun loadNEmbedIfNeeded(nembed: Event) { + val baseNote = LocalCache.getNoteIfExists(nembed.id) + if (baseNote?.event == null) { + LocalCache.verifyAndConsume(nembed, null) + } + } + suspend fun parseNIP19( str: String, onNote: (LoadedBechLink) -> Unit, @@ -1018,9 +1025,7 @@ class AccountViewModel(val account: Account, val settings: SettingsState) : View is Nip19Bech32.Note -> LocalCache.checkGetOrCreateNote(parsed.hex)?.let { note -> returningNote = note } is Nip19Bech32.NEvent -> LocalCache.checkGetOrCreateNote(parsed.hex)?.let { note -> returningNote = note } is Nip19Bech32.NEmbed -> { - if (LocalCache.getNoteIfExists(parsed.event.id) == null) { - LocalCache.verifyAndConsume(parsed.event, null) - } + loadNEmbedIfNeeded(parsed.event) LocalCache.checkGetOrCreateNote(parsed.event.id)?.let { note -> returningNote = note From fdb0830d1bdce5a0716149134eaa3e9872a982d2 Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Wed, 28 Feb 2024 11:23:56 -0500 Subject: [PATCH 02/98] Refactors extra chars for Bech Links --- .../vitorpamplona/amethyst/ui/components/RichTextViewer.kt | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/components/RichTextViewer.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/components/RichTextViewer.kt index e6d8a1fef..9c525fc3c 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/components/RichTextViewer.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/components/RichTextViewer.kt @@ -647,7 +647,7 @@ fun BechLink( accountViewModel, backgroundColor, nav, - loadedLink!!, + loadedLink?.nip19?.additionalChars?.ifBlank { null }, ) } } else if (loadedLink?.nip19 != null) { @@ -672,7 +672,7 @@ private fun DisplayFullNote( accountViewModel: AccountViewModel, backgroundColor: MutableState, nav: (String) -> Unit, - loadedLink: LoadedBechLink, + extraChars: String?, ) { NoteCompose( baseNote = it, @@ -683,8 +683,6 @@ private fun DisplayFullNote( nav = nav, ) - val extraChars = remember(loadedLink) { loadedLink.nip19.additionalChars.ifBlank { null } } - extraChars?.let { Text( it, From 73e3b607211b800ecac92556819f056f92de2674 Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Wed, 28 Feb 2024 16:50:23 -0500 Subject: [PATCH 03/98] Fixes missing nsec processing --- .../java/com/vitorpamplona/quartz/encoders/Nip19Bech32.kt | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/encoders/Nip19Bech32.kt b/quartz/src/main/java/com/vitorpamplona/quartz/encoders/Nip19Bech32.kt index 0516f1916..de2e719b3 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/encoders/Nip19Bech32.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/encoders/Nip19Bech32.kt @@ -111,6 +111,7 @@ object Nip19Bech32 { val bytes = (type + key).bechToBytes() when (type.lowercase()) { + "nsec1" -> nsec(bytes) "npub1" -> npub(bytes) "note1" -> note(bytes) "nprofile1" -> nprofile(bytes) @@ -133,6 +134,11 @@ object Nip19Bech32 { return NEmbed(Event.fromJson(ungzip(bytes))) } + private fun nsec(bytes: ByteArray): NSec? { + if (bytes.isEmpty()) return null + return NSec(bytes.toHexKey()) + } + private fun npub(bytes: ByteArray): NPub? { if (bytes.isEmpty()) return null return NPub(bytes.toHexKey()) From 86aab2e8425cb8747294b47ed5e9e5e995756758 Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Wed, 28 Feb 2024 16:50:58 -0500 Subject: [PATCH 04/98] Fixes DM chatroom edit button --- .../amethyst/ui/screen/loggedIn/ChatroomScreen.kt | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/ChatroomScreen.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/ChatroomScreen.kt index 41df0fd51..c7537ddba 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/ChatroomScreen.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/ChatroomScreen.kt @@ -45,7 +45,6 @@ import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.EditNote import androidx.compose.material.icons.filled.Send import androidx.compose.material3.Button -import androidx.compose.material3.ButtonDefaults import androidx.compose.material3.Divider import androidx.compose.material3.Icon import androidx.compose.material3.IconButton @@ -106,7 +105,6 @@ import com.vitorpamplona.amethyst.ui.note.UserCompose import com.vitorpamplona.amethyst.ui.note.UsernameDisplay import com.vitorpamplona.amethyst.ui.screen.NostrChatroomFeedViewModel import com.vitorpamplona.amethyst.ui.screen.RefreshingChatroomFeedView -import com.vitorpamplona.amethyst.ui.theme.ButtonBorder import com.vitorpamplona.amethyst.ui.theme.DividerThickness import com.vitorpamplona.amethyst.ui.theme.EditFieldBorder import com.vitorpamplona.amethyst.ui.theme.EditFieldModifier @@ -115,6 +113,7 @@ import com.vitorpamplona.amethyst.ui.theme.Size20Modifier import com.vitorpamplona.amethyst.ui.theme.Size30Modifier import com.vitorpamplona.amethyst.ui.theme.Size34dp import com.vitorpamplona.amethyst.ui.theme.StdPadding +import com.vitorpamplona.amethyst.ui.theme.ZeroPadding import com.vitorpamplona.amethyst.ui.theme.placeholderText import com.vitorpamplona.quartz.events.ChatMessageEvent import com.vitorpamplona.quartz.events.ChatroomKey @@ -661,11 +660,7 @@ private fun EditRoomSubjectButton( Button( modifier = Modifier.padding(horizontal = 3.dp).width(50.dp), onClick = { wantsToPost = true }, - shape = ButtonBorder, - colors = - ButtonDefaults.buttonColors( - containerColor = MaterialTheme.colorScheme.primary, - ), + contentPadding = ZeroPadding, ) { Icon( tint = Color.White, From 9f4a30657be553ad2ab170237340bce9f2ce62c4 Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Wed, 28 Feb 2024 16:51:14 -0500 Subject: [PATCH 05/98] Fixes Ots Observation check --- app/src/main/java/com/vitorpamplona/amethyst/model/Note.kt | 1 + 1 file changed, 1 insertion(+) diff --git a/app/src/main/java/com/vitorpamplona/amethyst/model/Note.kt b/app/src/main/java/com/vitorpamplona/amethyst/model/Note.kt index 73526c322..1ab49b3f4 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/model/Note.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/model/Note.kt @@ -1002,6 +1002,7 @@ class NoteLiveSet(u: Note) { replyCount.hasObservers() || reactionCount.hasObservers() || boostCount.hasObservers() + innerOts.hasObservers() || } fun destroy() { From 7f35d7d41646e952d3d2a6f20bfb1a82ab0f760e Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Wed, 28 Feb 2024 16:52:33 -0500 Subject: [PATCH 06/98] Improves rendering of prescriptions --- .../java/com/vitorpamplona/amethyst/ui/note/NoteCompose.kt | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) 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 fbab305e1..2cdb08945 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 @@ -4440,7 +4440,7 @@ fun RenderEyeGlassesPrescription( modifier = Modifier .fillMaxWidth() - .padding(Size10dp), + .padding(horizontal = Size10dp), ) { val rightEye = visionPrescription.lensSpecification.firstOrNull { it.eye == "right" } val leftEye = visionPrescription.lensSpecification.firstOrNull { it.eye == "left" } @@ -4485,12 +4485,11 @@ fun RenderEyeGlassesPrescription( HorizontalDivider(thickness = DividerThickness) } - Spacer(DoubleVertSpacer) - visionPrescription.prescriber?.reference?.let { val practitioner = findReferenceInDb(it, db) as? Practitioner practitioner?.name?.firstOrNull()?.assembleName()?.let { + Spacer(DoubleVertSpacer) Text( text = "Signed by: $it", modifier = Modifier.padding(4.dp).fillMaxWidth(), From d4ba13536850abb207b002a0634ff5a0efdc0f9c Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Wed, 28 Feb 2024 16:54:28 -0500 Subject: [PATCH 07/98] Second embed bundle tests with nsecs --- .../vitorpamplona/quartz/NIP19EmbedTests.kt | 28 +++++++++---------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/quartz/src/androidTest/java/com/vitorpamplona/quartz/NIP19EmbedTests.kt b/quartz/src/androidTest/java/com/vitorpamplona/quartz/NIP19EmbedTests.kt index 326fe5bb1..8876fd703 100644 --- a/quartz/src/androidTest/java/com/vitorpamplona/quartz/NIP19EmbedTests.kt +++ b/quartz/src/androidTest/java/com/vitorpamplona/quartz/NIP19EmbedTests.kt @@ -24,6 +24,8 @@ import androidx.test.ext.junit.runners.AndroidJUnit4 import com.vitorpamplona.quartz.crypto.KeyPair import com.vitorpamplona.quartz.encoders.Hex import com.vitorpamplona.quartz.encoders.Nip19Bech32 +import com.vitorpamplona.quartz.encoders.decodePrivateKeyAsHexOrNull +import com.vitorpamplona.quartz.encoders.hexToByteArray import com.vitorpamplona.quartz.events.Event import com.vitorpamplona.quartz.events.FhirResourceEvent import com.vitorpamplona.quartz.events.TextNoteEvent @@ -134,19 +136,18 @@ class NIP19EmbedTests { assertEquals(eyeglassesPrescriptionEvent!!.toJson(), decodedNote.toJson()) } - /* @Test - fun testCompressionSizes() { + fun testVisionPrescriptionBundle2EmbedEvent() { val signer = NostrSignerInternal( - KeyPair(Hex.decode("e8e7197ccc53c9ed4cf9b1c8dce085475fa1ffdd71f2c14e44fe23d0bdf77598")), + KeyPair(decodePrivateKeyAsHexOrNull("nsec1arn3jlxv20y76n8ek8ydecy9ga06rl7aw8evznjylc3ap00hwkvqx4vvy6")!!.hexToByteArray()), ) var eyeglassesPrescriptionEvent: Event? = null val countDownLatch = CountDownLatch(1) - FhirResourceEvent.create(fhirPayload = visionPrescriptionFhir, signer = signer) { + FhirResourceEvent.create(fhirPayload = visionPrescriptionBundle2, signer = signer) { eyeglassesPrescriptionEvent = it countDownLatch.countDown() } @@ -155,21 +156,20 @@ class NIP19EmbedTests { assertNotNull(eyeglassesPrescriptionEvent) - val json = eyeglassesPrescriptionEvent!!.toJson() + val bech32 = Nip19Bech32.createNEmbed(eyeglassesPrescriptionEvent!!) - Bech32.encodeBytes(hrp = "nembed", json.toByteArray(), Bech32.Encoding.Bech32) - Bech32.encodeBytes(hrp = "nembed", gzip(json), Bech32.Encoding.Bech32) + println(eyeglassesPrescriptionEvent!!.toJson()) + println(bech32) - val (bech32json, elapsedjson) = measureTimedValue { json.toByteArray().toNEmbed() } - val (bech32gzip, elapsedgzip) = measureTimedValue { gzip(json).toNEmbed() } + val decodedNote = (Nip19Bech32.uriToRoute(bech32)?.entity as Nip19Bech32.NEmbed).event - println("Raw JSON ${json.length} chars") - println("Bech32 JSON ${bech32json.length} chars in $elapsedjson") - println("Bech32 GZIP ${bech32gzip.length} chars in $elapsedgzip") + assertTrue(decodedNote.hasValidSignature()) - assertTrue(true) - }*/ + assertEquals(eyeglassesPrescriptionEvent!!.toJson(), decodedNote.toJson()) + } 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}]}]}" + + val visionPrescriptionBundle2 = "{\"resourceType\":\"Bundle\",\"id\":\"bundle-vision-test\",\"type\":\"document\",\"entry\":[{\"resourceType\":\"Practitioner\",\"id\":\"2\",\"active\":true,\"name\":[{\"use\":\"official\",\"family\":\"Smith\",\"given\":[\"Dr. Joe\"]}],\"gender\":\"male\"},{\"resourceType\":\"Patient\",\"id\":\"1\",\"active\":true,\"name\":[{\"use\":\"official\",\"family\":\"Doe\",\"given\":[\"Jane\"]}],\"gender\":\"male\"},{\"resourceType\":\"VisionPrescription\",\"status\":\"active\",\"created\":\"2014-06-15\",\"patient\":{\"reference\":\"#1\"},\"dateWritten\":\"2014-06-15\",\"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}]}]}" } From 3eb6983a94b79e6cf5bee0bdfacbf774c878e5d5 Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Wed, 28 Feb 2024 17:42:06 -0500 Subject: [PATCH 08/98] Support for Kind 1 Edits --- .../vitorpamplona/amethyst/model/Account.kt | 20 + .../amethyst/model/LocalCache.kt | 33 ++ .../com/vitorpamplona/amethyst/model/Note.kt | 5 +- .../service/NostrSingleEventDataSource.kt | 2 + .../amethyst/ui/actions/EditPostView.kt | 499 ++++++++++++++++++ .../amethyst/ui/actions/EditPostViewModel.kt | 362 +++++++++++++ .../amethyst/ui/note/BadgeCompose.kt | 2 +- .../amethyst/ui/note/MessageSetCompose.kt | 2 +- .../amethyst/ui/note/MultiSetCompose.kt | 2 +- .../amethyst/ui/note/NoteCompose.kt | 86 ++- .../amethyst/ui/note/UserProfilePicture.kt | 24 + .../amethyst/ui/screen/ThreadFeedView.kt | 30 +- .../ui/screen/loggedIn/AccountViewModel.kt | 9 + .../ui/screen/loggedIn/ChannelScreen.kt | 2 +- .../ui/screen/loggedIn/VideoScreen.kt | 8 +- app/src/main/res/values/strings.xml | 4 + .../quartz/events/EventFactory.kt | 1 + .../events/TextNoteModificationEvent.kt | 52 ++ 18 files changed, 1119 insertions(+), 24 deletions(-) create mode 100644 app/src/main/java/com/vitorpamplona/amethyst/ui/actions/EditPostView.kt create mode 100644 app/src/main/java/com/vitorpamplona/amethyst/ui/actions/EditPostViewModel.kt create mode 100644 quartz/src/main/java/com/vitorpamplona/quartz/events/TextNoteModificationEvent.kt diff --git a/app/src/main/java/com/vitorpamplona/amethyst/model/Account.kt b/app/src/main/java/com/vitorpamplona/amethyst/model/Account.kt index c7421c35f..ae52d71f3 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/model/Account.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/model/Account.kt @@ -89,6 +89,7 @@ import com.vitorpamplona.quartz.events.Response import com.vitorpamplona.quartz.events.SealedGossipEvent import com.vitorpamplona.quartz.events.StatusEvent import com.vitorpamplona.quartz.events.TextNoteEvent +import com.vitorpamplona.quartz.events.TextNoteModificationEvent import com.vitorpamplona.quartz.events.WrappedEvent import com.vitorpamplona.quartz.events.ZapSplitSetup import com.vitorpamplona.quartz.signers.NostrSigner @@ -1405,6 +1406,25 @@ class Account( } } + fun sendEdit( + message: String, + originalNote: Note, + relayList: List? = null, + ) { + if (!isWriteable()) return + + val idHex = originalNote.event?.id() ?: return + + TextNoteModificationEvent.create( + content = message, + eventId = idHex, + signer = signer, + ) { + Client.send(it, relayList = relayList) + LocalCache.justConsume(it, null) + } + } + fun sendPoll( message: String, replyTo: List?, 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 f366531a5..5e4d860e5 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/model/LocalCache.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/model/LocalCache.kt @@ -98,6 +98,7 @@ import com.vitorpamplona.quartz.events.RepostEvent import com.vitorpamplona.quartz.events.SealedGossipEvent import com.vitorpamplona.quartz.events.StatusEvent import com.vitorpamplona.quartz.events.TextNoteEvent +import com.vitorpamplona.quartz.events.TextNoteModificationEvent import com.vitorpamplona.quartz.events.VideoHorizontalEvent import com.vitorpamplona.quartz.events.VideoVerticalEvent import com.vitorpamplona.quartz.events.WikiNoteEvent @@ -1384,6 +1385,26 @@ object LocalCache { refreshObservers(note) } + fun consume( + event: TextNoteModificationEvent, + 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?, @@ -1695,6 +1716,17 @@ object LocalCache { return minTime } + suspend fun findLatestModificationForNote(note: Note): List { + checkNotInMainThread() + val time = TimeUtils.now() + + return noteListCache.filter { item -> + val noteEvent = item.event + + noteEvent is TextNoteModificationEvent && noteEvent.isTaggedEvent(note.idHex) && !noteEvent.isExpirationBefore(time) + } + } + fun cleanObservers() { noteListCache.forEach { it.clearLive() } @@ -2048,6 +2080,7 @@ object LocalCache { } is StatusEvent -> consume(event, relay) is TextNoteEvent -> consume(event, relay) + is TextNoteModificationEvent -> consume(event, relay) is VideoHorizontalEvent -> consume(event, relay) is VideoVerticalEvent -> consume(event, relay) is WikiNoteEvent -> consume(event, relay) diff --git a/app/src/main/java/com/vitorpamplona/amethyst/model/Note.kt b/app/src/main/java/com/vitorpamplona/amethyst/model/Note.kt index 1ab49b3f4..77f380b3c 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/model/Note.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/model/Note.kt @@ -948,6 +948,7 @@ class NoteLiveSet(u: Note) { val innerRelays = NoteBundledRefresherLiveData(u) val innerZaps = NoteBundledRefresherLiveData(u) val innerOts = NoteBundledRefresherLiveData(u) + val innerModifications = NoteBundledRefresherLiveData(u) val metadata = innerMetadata.map { it } val reactions = innerReactions.map { it } @@ -1001,8 +1002,9 @@ class NoteLiveSet(u: Note) { hasReactions.hasObservers() || replyCount.hasObservers() || reactionCount.hasObservers() || - boostCount.hasObservers() + boostCount.hasObservers() || innerOts.hasObservers() || + innerModifications.hasObservers() } fun destroy() { @@ -1014,6 +1016,7 @@ class NoteLiveSet(u: Note) { innerRelays.destroy() innerZaps.destroy() innerOts.destroy() + innerModifications.destroy() } } diff --git a/app/src/main/java/com/vitorpamplona/amethyst/service/NostrSingleEventDataSource.kt b/app/src/main/java/com/vitorpamplona/amethyst/service/NostrSingleEventDataSource.kt index e29566892..618dcbeb9 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/service/NostrSingleEventDataSource.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/service/NostrSingleEventDataSource.kt @@ -37,6 +37,7 @@ import com.vitorpamplona.quartz.events.ReactionEvent import com.vitorpamplona.quartz.events.ReportEvent import com.vitorpamplona.quartz.events.RepostEvent import com.vitorpamplona.quartz.events.TextNoteEvent +import com.vitorpamplona.quartz.events.TextNoteModificationEvent object NostrSingleEventDataSource : NostrDataSource("SingleEventFeed") { private var eventsToWatch = setOf() @@ -136,6 +137,7 @@ object NostrSingleEventDataSource : NostrDataSource("SingleEventFeed") { LnZapEvent.KIND, PollNoteEvent.KIND, OtsEvent.KIND, + TextNoteModificationEvent.KIND, ), tags = mapOf("e" to it.map { it.idHex }), since = findMinimumEOSEs(it), diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/EditPostView.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/EditPostView.kt new file mode 100644 index 000000000..1d35c13f0 --- /dev/null +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/EditPostView.kt @@ -0,0 +1,499 @@ +/** + * 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.actions + +import android.widget.Toast +import androidx.compose.foundation.border +import androidx.compose.foundation.horizontalScroll +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +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.fillMaxHeight +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.heightIn +import androidx.compose.foundation.layout.imePadding +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.itemsIndexed +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.foundation.text.KeyboardOptions +import androidx.compose.foundation.verticalScroll +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.CurrencyBitcoin +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.LocalTextStyle +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.OutlinedTextField +import androidx.compose.material3.OutlinedTextFieldDefaults +import androidx.compose.material3.Scaffold +import androidx.compose.material3.Surface +import androidx.compose.material3.Text +import androidx.compose.material3.TopAppBar +import androidx.compose.material3.TopAppBarDefaults +import androidx.compose.runtime.Composable +import androidx.compose.runtime.DisposableEffect +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +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.CenterVertically +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.focus.FocusRequester +import androidx.compose.ui.focus.focusRequester +import androidx.compose.ui.focus.onFocusChanged +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.layout.ContentScale +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.platform.LocalSoftwareKeyboardController +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.input.KeyboardCapitalization +import androidx.compose.ui.text.input.TextFieldValue +import androidx.compose.ui.text.style.TextDirection +import androidx.compose.ui.unit.dp +import androidx.compose.ui.window.Dialog +import androidx.compose.ui.window.DialogProperties +import androidx.lifecycle.viewmodel.compose.viewModel +import coil.compose.AsyncImage +import com.vitorpamplona.amethyst.R +import com.vitorpamplona.amethyst.commons.RichTextParser +import com.vitorpamplona.amethyst.model.Note +import com.vitorpamplona.amethyst.service.NostrSearchEventOrUserDataSource +import com.vitorpamplona.amethyst.ui.components.BechLink +import com.vitorpamplona.amethyst.ui.components.InvoiceRequest +import com.vitorpamplona.amethyst.ui.components.LoadUrlPreview +import com.vitorpamplona.amethyst.ui.components.VideoView +import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel +import com.vitorpamplona.amethyst.ui.screen.loggedIn.UserLine +import com.vitorpamplona.amethyst.ui.theme.BitcoinOrange +import com.vitorpamplona.amethyst.ui.theme.QuoteBorder +import com.vitorpamplona.amethyst.ui.theme.Size10dp +import com.vitorpamplona.amethyst.ui.theme.Size5dp +import com.vitorpamplona.amethyst.ui.theme.StdHorzSpacer +import com.vitorpamplona.amethyst.ui.theme.placeholderText +import com.vitorpamplona.amethyst.ui.theme.subtleBorder +import kotlinx.collections.immutable.toImmutableList +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.delay +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun EditPostView( + onClose: () -> Unit, + edit: Note, + accountViewModel: AccountViewModel, + nav: (String) -> Unit, +) { + val postViewModel: EditPostViewModel = viewModel() + + val context = LocalContext.current + + val scrollState = rememberScrollState() + val scope = rememberCoroutineScope() + var showRelaysDialog by remember { mutableStateOf(false) } + var relayList = remember { accountViewModel.account.activeWriteRelays().toImmutableList() } + + LaunchedEffect(Unit) { + postViewModel.load(edit, accountViewModel) + + launch(Dispatchers.IO) { + postViewModel.imageUploadingError.collect { error -> + withContext(Dispatchers.Main) { Toast.makeText(context, error, Toast.LENGTH_SHORT).show() } + } + } + } + + DisposableEffect(Unit) { + NostrSearchEventOrUserDataSource.start() + + onDispose { + NostrSearchEventOrUserDataSource.clear() + NostrSearchEventOrUserDataSource.stop() + } + } + + Dialog( + onDismissRequest = { onClose() }, + properties = + DialogProperties( + usePlatformDefaultWidth = false, + dismissOnClickOutside = false, + decorFitsSystemWindows = false, + ), + ) { + if (showRelaysDialog) { + RelaySelectionDialog( + preSelectedList = relayList, + onClose = { showRelaysDialog = false }, + onPost = { relayList = it }, + accountViewModel = accountViewModel, + nav = nav, + ) + } + + Scaffold( + topBar = { + TopAppBar( + title = { + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.CenterVertically, + ) { + Spacer(modifier = StdHorzSpacer) + + Box { + IconButton( + modifier = Modifier.align(Alignment.Center), + onClick = { showRelaysDialog = true }, + ) { + Icon( + painter = painterResource(R.drawable.relays), + contentDescription = stringResource(id = R.string.relay_list_selector), + modifier = Modifier.height(25.dp), + tint = MaterialTheme.colorScheme.onBackground, + ) + } + } + PostButton( + onPost = { + postViewModel.sendPost(relayList = relayList) + scope.launch { + delay(100) + onClose() + } + }, + isActive = postViewModel.canPost(), + ) + } + }, + navigationIcon = { + Row { + Spacer(modifier = StdHorzSpacer) + CloseButton( + onPress = { + postViewModel.cancel() + scope.launch { + delay(100) + onClose() + } + }, + ) + } + }, + colors = + TopAppBarDefaults.topAppBarColors( + containerColor = MaterialTheme.colorScheme.surface, + ), + ) + }, + ) { pad -> + Surface( + modifier = + Modifier + .padding( + start = Size10dp, + top = pad.calculateTopPadding(), + end = Size10dp, + bottom = pad.calculateBottomPadding(), + ) + .fillMaxSize(), + ) { + Column( + modifier = + Modifier + .fillMaxWidth() + .fillMaxHeight(), + ) { + Column( + modifier = + Modifier + .imePadding() + .weight(1f), + ) { + Row( + modifier = + Modifier + .fillMaxWidth() + .weight(1f), + ) { + Column( + modifier = + Modifier + .fillMaxWidth() + .verticalScroll(scrollState), + ) { + MessageField(postViewModel) + + val myUrlPreview = postViewModel.urlPreview + if (myUrlPreview != null) { + Row(modifier = Modifier.padding(vertical = Size5dp, horizontal = Size10dp)) { + if (RichTextParser.isValidURL(myUrlPreview)) { + if (RichTextParser.isImageUrl(myUrlPreview)) { + AsyncImage( + model = myUrlPreview, + contentDescription = myUrlPreview, + contentScale = ContentScale.FillWidth, + modifier = + Modifier + .padding(top = 4.dp) + .fillMaxWidth() + .clip(shape = QuoteBorder) + .border( + 1.dp, + MaterialTheme.colorScheme.subtleBorder, + QuoteBorder, + ), + ) + } else if (RichTextParser.isVideoUrl(myUrlPreview)) { + VideoView( + myUrlPreview, + roundedCorner = true, + accountViewModel = accountViewModel, + ) + } else { + LoadUrlPreview(myUrlPreview, myUrlPreview, accountViewModel) + } + } else if (RichTextParser.startsWithNIP19Scheme(myUrlPreview)) { + val bgColor = MaterialTheme.colorScheme.background + val backgroundColor = remember { mutableStateOf(bgColor) } + + BechLink( + myUrlPreview, + true, + backgroundColor, + accountViewModel, + nav, + ) + } else if (RichTextParser.isUrlWithoutScheme(myUrlPreview)) { + LoadUrlPreview("https://$myUrlPreview", myUrlPreview, accountViewModel) + } + } + } + + val url = postViewModel.contentToAddUrl + if (url != null) { + Row( + verticalAlignment = Alignment.CenterVertically, + modifier = Modifier.padding(vertical = Size5dp, horizontal = Size10dp), + ) { + ImageVideoDescription( + url, + accountViewModel.account.defaultFileServer, + onAdd = { alt, server, sensitiveContent -> + postViewModel.upload(url, alt, sensitiveContent, false, server, context) + if (!server.isNip95) { + accountViewModel.account.changeDefaultFileServer(server.server) + } + }, + onCancel = { postViewModel.contentToAddUrl = null }, + onError = { scope.launch { postViewModel.imageUploadingError.emit(it) } }, + accountViewModel = accountViewModel, + ) + } + } + + val user = postViewModel.account?.userProfile() + val lud16 = user?.info?.lnAddress() + + if (lud16 != null && postViewModel.wantsInvoice) { + Row( + verticalAlignment = Alignment.CenterVertically, + modifier = Modifier.padding(vertical = Size5dp, horizontal = Size10dp), + ) { + Column(Modifier.fillMaxWidth()) { + InvoiceRequest( + lud16, + user.pubkeyHex, + accountViewModel.account, + stringResource(id = R.string.lightning_invoice), + stringResource(id = R.string.lightning_create_and_add_invoice), + onSuccess = { + postViewModel.message = + TextFieldValue(postViewModel.message.text + "\n\n" + it) + postViewModel.wantsInvoice = false + }, + onClose = { postViewModel.wantsInvoice = false }, + onError = { title, message -> accountViewModel.toast(title, message) }, + ) + } + } + } + } + } + + ShowUserSuggestionListForEdit( + postViewModel, + accountViewModel, + modifier = Modifier.heightIn(0.dp, 300.dp), + ) + + BottomRowActions(postViewModel) + } + } + } + } + } +} + +@Composable +fun ShowUserSuggestionListForEdit( + editPostViewModel: EditPostViewModel, + accountViewModel: AccountViewModel, + modifier: Modifier = Modifier.heightIn(0.dp, 200.dp), +) { + val userSuggestions = editPostViewModel.userSuggestions + if (userSuggestions.isNotEmpty()) { + LazyColumn( + contentPadding = + PaddingValues( + top = 10.dp, + ), + modifier = modifier, + ) { + itemsIndexed( + userSuggestions, + key = { _, item -> item.pubkeyHex }, + ) { _, item -> + UserLine(item, accountViewModel) { editPostViewModel.autocompleteWithUser(item) } + } + } + } +} + +@Composable +private fun BottomRowActions(postViewModel: EditPostViewModel) { + val scrollState = rememberScrollState() + + Row( + modifier = + Modifier + .horizontalScroll(scrollState) + .fillMaxWidth() + .height(50.dp), + verticalAlignment = CenterVertically, + ) { + UploadFromGallery( + isUploading = postViewModel.isUploadingImage, + tint = MaterialTheme.colorScheme.onBackground, + modifier = Modifier, + ) { + postViewModel.selectImage(it) + } + + if (postViewModel.canAddInvoice) { + AddLnInvoiceButton(postViewModel.wantsInvoice) { + postViewModel.wantsInvoice = !postViewModel.wantsInvoice + } + } + } +} + +@Composable +private fun MessageField(postViewModel: EditPostViewModel) { + val focusRequester = remember { FocusRequester() } + val keyboardController = LocalSoftwareKeyboardController.current + + LaunchedEffect(Unit) { + launch { + delay(200) + focusRequester.requestFocus() + } + } + + OutlinedTextField( + value = postViewModel.message, + onValueChange = { postViewModel.updateMessage(it) }, + keyboardOptions = + KeyboardOptions.Default.copy( + capitalization = KeyboardCapitalization.Sentences, + ), + modifier = + Modifier + .fillMaxWidth() + .border( + width = 1.dp, + color = MaterialTheme.colorScheme.surface, + shape = RoundedCornerShape(8.dp), + ) + .focusRequester(focusRequester) + .onFocusChanged { + if (it.isFocused) { + keyboardController?.show() + } + }, + placeholder = { + Text( + text = stringResource(R.string.what_s_on_your_mind), + color = MaterialTheme.colorScheme.placeholderText, + ) + }, + colors = + OutlinedTextFieldDefaults.colors( + focusedBorderColor = Color.Transparent, + unfocusedBorderColor = Color.Transparent, + ), + visualTransformation = UrlUserTagTransformation(MaterialTheme.colorScheme.primary), + textStyle = LocalTextStyle.current.copy(textDirection = TextDirection.Content), + ) +} + +@Composable +private fun AddLnInvoiceButton( + isLnInvoiceActive: Boolean, + onClick: () -> Unit, +) { + IconButton( + onClick = { onClick() }, + ) { + if (!isLnInvoiceActive) { + Icon( + imageVector = Icons.Default.CurrencyBitcoin, + contentDescription = stringResource(id = R.string.add_bitcoin_invoice), + modifier = Modifier.size(20.dp), + tint = MaterialTheme.colorScheme.onBackground, + ) + } else { + Icon( + imageVector = Icons.Default.CurrencyBitcoin, + contentDescription = stringResource(id = R.string.cancel_bitcoin_invoice), + modifier = Modifier.size(20.dp), + tint = BitcoinOrange, + ) + } + } +} diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/EditPostViewModel.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/EditPostViewModel.kt new file mode 100644 index 000000000..99c0b6314 --- /dev/null +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/EditPostViewModel.kt @@ -0,0 +1,362 @@ +/** + * 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.actions + +import android.content.Context +import android.net.Uri +import android.util.Log +import androidx.compose.runtime.Stable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.setValue +import androidx.compose.ui.text.TextRange +import androidx.compose.ui.text.input.TextFieldValue +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import com.vitorpamplona.amethyst.commons.RichTextParser +import com.vitorpamplona.amethyst.commons.insertUrlAtCursor +import com.vitorpamplona.amethyst.model.Account +import com.vitorpamplona.amethyst.model.LocalCache +import com.vitorpamplona.amethyst.model.Note +import com.vitorpamplona.amethyst.model.User +import com.vitorpamplona.amethyst.service.FileHeader +import com.vitorpamplona.amethyst.service.Nip96Uploader +import com.vitorpamplona.amethyst.service.NostrSearchEventOrUserDataSource +import com.vitorpamplona.amethyst.service.relays.Relay +import com.vitorpamplona.amethyst.ui.components.MediaCompressor +import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel +import com.vitorpamplona.quartz.events.FileHeaderEvent +import com.vitorpamplona.quartz.events.FileStorageEvent +import com.vitorpamplona.quartz.events.FileStorageHeaderEvent +import kotlinx.coroutines.CancellationException +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.channels.BufferOverflow +import kotlinx.coroutines.flow.MutableSharedFlow +import kotlinx.coroutines.launch + +@Stable +open class EditPostViewModel() : ViewModel() { + var accountViewModel: AccountViewModel? = null + var account: Account? = null + + var editedFromNote: Note? = null + + var nip94attachments by mutableStateOf>(emptyList()) + var nip95attachments by + mutableStateOf>>(emptyList()) + + var message by mutableStateOf(TextFieldValue("")) + var urlPreview by mutableStateOf(null) + var isUploadingImage by mutableStateOf(false) + val imageUploadingError = + MutableSharedFlow(0, 3, onBufferOverflow = BufferOverflow.DROP_OLDEST) + + var userSuggestions by mutableStateOf>(emptyList()) + var userSuggestionAnchor: TextRange? = null + var userSuggestionsMainMessage: UserSuggestionAnchor? = null + + // Images and Videos + var contentToAddUrl by mutableStateOf(null) + + // Invoices + var canAddInvoice by mutableStateOf(false) + var wantsInvoice by mutableStateOf(false) + + open fun load( + edit: Note, + accountViewModel: AccountViewModel, + ) { + this.accountViewModel = accountViewModel + this.account = accountViewModel.account + + canAddInvoice = accountViewModel.userProfile().info?.lnAddress() != null + contentToAddUrl = null + + message = TextFieldValue(edit.event?.content() ?: "") + urlPreview = findUrlInMessage() + + editedFromNote = edit + } + + fun sendPost(relayList: List? = null) { + viewModelScope.launch(Dispatchers.IO) { innerSendPost(relayList) } + } + + suspend fun innerSendPost(relayList: List? = null) { + if (accountViewModel == null) { + cancel() + return + } + + nip95attachments.forEach { + account?.sendNip95(it.first, it.second, relayList) + } + + account?.sendEdit( + message = message.text, + originalNote = editedFromNote!!, + relayList = relayList, + ) + + cancel() + } + + fun upload( + galleryUri: Uri, + alt: String?, + sensitiveContent: Boolean, + isPrivate: Boolean = false, + server: ServerOption, + context: Context, + ) { + isUploadingImage = true + contentToAddUrl = null + + val contentResolver = context.contentResolver + val contentType = contentResolver.getType(galleryUri) + + viewModelScope.launch(Dispatchers.IO) { + MediaCompressor() + .compress( + galleryUri, + contentType, + context.applicationContext, + onReady = { fileUri, contentType, size -> + if (server.isNip95) { + contentResolver.openInputStream(fileUri)?.use { + createNIP95Record(it.readBytes(), contentType, alt, sensitiveContent) + } + } else { + viewModelScope.launch(Dispatchers.IO) { + try { + val result = + Nip96Uploader(account) + .uploadImage( + uri = fileUri, + contentType = contentType, + size = size, + alt = alt, + sensitiveContent = if (sensitiveContent) "" else null, + server = server.server, + contentResolver = contentResolver, + onProgress = {}, + ) + + createNIP94Record( + uploadingResult = result, + localContentType = contentType, + alt = alt, + sensitiveContent = sensitiveContent, + ) + } catch (e: Exception) { + if (e is CancellationException) throw e + Log.e( + "ImageUploader", + "Failed to upload ${e.message}", + e, + ) + isUploadingImage = false + viewModelScope.launch { + imageUploadingError.emit("Failed to upload: ${e.message}") + } + } + } + } + }, + onError = { + isUploadingImage = false + viewModelScope.launch { imageUploadingError.emit(it) } + }, + ) + } + } + + open fun cancel() { + message = TextFieldValue("") + + editedFromNote = null + + contentToAddUrl = null + urlPreview = null + isUploadingImage = false + + wantsInvoice = false + + userSuggestions = emptyList() + userSuggestionAnchor = null + userSuggestionsMainMessage = null + + NostrSearchEventOrUserDataSource.clear() + } + + open fun findUrlInMessage(): String? { + return message.text.split('\n').firstNotNullOfOrNull { paragraph -> + paragraph.split(' ').firstOrNull { word: String -> + RichTextParser.isValidURL(word) || RichTextParser.isUrlWithoutScheme(word) + } + } + } + + open fun updateMessage(it: TextFieldValue) { + message = it + urlPreview = findUrlInMessage() + + if (it.selection.collapsed) { + val lastWord = + it.text.substring(0, it.selection.end).substringAfterLast("\n").substringAfterLast(" ") + userSuggestionAnchor = it.selection + userSuggestionsMainMessage = UserSuggestionAnchor.MAIN_MESSAGE + if (lastWord.startsWith("@") && lastWord.length > 2) { + NostrSearchEventOrUserDataSource.search(lastWord.removePrefix("@")) + viewModelScope.launch(Dispatchers.IO) { + userSuggestions = + LocalCache.findUsersStartingWith(lastWord.removePrefix("@")) + .sortedWith(compareBy({ account?.isFollowing(it) }, { it.toBestDisplayName() })) + .reversed() + } + } else { + NostrSearchEventOrUserDataSource.clear() + userSuggestions = emptyList() + } + } + } + + open fun autocompleteWithUser(item: User) { + userSuggestionAnchor?.let { + if (userSuggestionsMainMessage == UserSuggestionAnchor.MAIN_MESSAGE) { + val lastWord = + message.text.substring(0, it.end).substringAfterLast("\n").substringAfterLast(" ") + val lastWordStart = it.end - lastWord.length + val wordToInsert = "@${item.pubkeyNpub()}" + + message = + TextFieldValue( + message.text.replaceRange(lastWordStart, it.end, wordToInsert), + TextRange(lastWordStart + wordToInsert.length, lastWordStart + wordToInsert.length), + ) + } + + userSuggestionAnchor = null + userSuggestionsMainMessage = null + userSuggestions = emptyList() + } + } + + fun canPost(): Boolean { + return message.text.isNotBlank() && + !isUploadingImage && + !wantsInvoice && + contentToAddUrl == null + } + + suspend fun createNIP94Record( + uploadingResult: Nip96Uploader.PartialEvent, + localContentType: String?, + alt: String?, + sensitiveContent: Boolean, + ) { + // Images don't seem to be ready immediately after upload + val imageUrl = uploadingResult.tags?.firstOrNull { it.size > 1 && it[0] == "url" }?.get(1) + val remoteMimeType = + uploadingResult.tags?.firstOrNull { it.size > 1 && it[0] == "m" }?.get(1)?.ifBlank { null } + val originalHash = + uploadingResult.tags?.firstOrNull { it.size > 1 && it[0] == "ox" }?.get(1)?.ifBlank { null } + val dim = + uploadingResult.tags?.firstOrNull { it.size > 1 && it[0] == "dim" }?.get(1)?.ifBlank { null } + val magnet = + uploadingResult.tags + ?.firstOrNull { it.size > 1 && it[0] == "magnet" } + ?.get(1) + ?.ifBlank { null } + + if (imageUrl.isNullOrBlank()) { + Log.e("ImageDownload", "Couldn't download image from server") + cancel() + isUploadingImage = false + viewModelScope.launch { imageUploadingError.emit("Failed to upload the image / video") } + return + } + + FileHeader.prepare( + fileUrl = imageUrl, + mimeType = remoteMimeType ?: localContentType, + dimPrecomputed = dim, + onReady = { header: FileHeader -> + account?.createHeader(imageUrl, magnet, header, alt, sensitiveContent, originalHash) { event -> + isUploadingImage = false + nip94attachments = nip94attachments + event + + message = message.insertUrlAtCursor(imageUrl) + urlPreview = findUrlInMessage() + } + }, + onError = { + isUploadingImage = false + viewModelScope.launch { imageUploadingError.emit("Failed to upload the image / video") } + }, + ) + } + + fun createNIP95Record( + bytes: ByteArray, + mimeType: String?, + alt: String?, + sensitiveContent: Boolean, + ) { + if (bytes.size > 80000) { + viewModelScope.launch { + imageUploadingError.emit("Media is too big for NIP-95") + isUploadingImage = false + } + return + } + + viewModelScope.launch(Dispatchers.IO) { + FileHeader.prepare( + bytes, + mimeType, + null, + onReady = { + account?.createNip95(bytes, headerInfo = it, alt, sensitiveContent) { nip95 -> + nip95attachments = nip95attachments + nip95 + val note = nip95.let { it1 -> account?.consumeNip95(it1.first, it1.second) } + + isUploadingImage = false + + note?.let { + message = message.insertUrlAtCursor("nostr:" + it.toNEvent()) + } + + urlPreview = findUrlInMessage() + } + }, + onError = { + isUploadingImage = false + viewModelScope.launch { imageUploadingError.emit("Failed to upload the image / video") } + }, + ) + } + } + + fun selectImage(uri: Uri) { + contentToAddUrl = uri + } +} diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/note/BadgeCompose.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/BadgeCompose.kt index 007f5ea66..fe987ca47 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/note/BadgeCompose.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/BadgeCompose.kt @@ -167,7 +167,7 @@ fun BadgeCompose( tint = MaterialTheme.colorScheme.placeholderText, ) - NoteDropDownMenu(note, popupExpanded, accountViewModel) + NoteDropDownMenu(note, popupExpanded, accountViewModel, nav) } } diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/note/MessageSetCompose.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/MessageSetCompose.kt index 3e75719d9..17e95d00c 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/note/MessageSetCompose.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/MessageSetCompose.kt @@ -129,7 +129,7 @@ fun MessageSetCompose( nav = nav, ) - NoteDropDownMenu(baseNote, popupExpanded, accountViewModel) + NoteDropDownMenu(baseNote, popupExpanded, accountViewModel, nav) } } diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/note/MultiSetCompose.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/MultiSetCompose.kt index 3226d05cc..5a8cb21ff 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/note/MultiSetCompose.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/MultiSetCompose.kt @@ -166,7 +166,7 @@ fun MultiSetCompose( nav = nav, ) - NoteDropDownMenu(baseNote, popupExpanded, accountViewModel) + NoteDropDownMenu(baseNote, popupExpanded, accountViewModel, nav) } Divider( 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 2cdb08945..ee4584be8 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 @@ -60,16 +60,16 @@ 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.Stable import androidx.compose.runtime.derivedStateOf import androidx.compose.runtime.getValue import androidx.compose.runtime.livedata.observeAsState import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.produceState 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 @@ -85,7 +85,6 @@ 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 @@ -785,7 +784,7 @@ fun LongCommunityHeader( ) Spacer(DoubleHorzSpacer) NormalTimeAgo(baseNote = baseNote, Modifier.weight(1f)) - MoreOptionsButton(baseNote, accountViewModel) + MoreOptionsButton(baseNote, accountViewModel, nav) } } @@ -1091,6 +1090,12 @@ fun InnerNoteWithReactions( } } +@Stable +class EditState( + val showOriginal: MutableState = mutableStateOf(false), + val modificationsInOrder: MutableState> = mutableStateOf(emptyList()), +) + @Composable private fun NoteBody( baseNote: Note, @@ -1103,9 +1108,19 @@ private fun NoteBody( accountViewModel: AccountViewModel, nav: (String) -> Unit, ) { + val editState by + produceState(initialValue = EditState(), key1 = baseNote) { + accountViewModel.findModificationEventsForNote(baseNote) { newModifications -> + if (value.modificationsInOrder.value != newModifications) { + value.modificationsInOrder.value = newModifications + } + } + } + FirstUserInfoRow( baseNote = baseNote, showAuthorPicture = showAuthorPicture, + editState = editState, accountViewModel = accountViewModel, nav = nav, ) @@ -1133,12 +1148,13 @@ private fun NoteBody( } RenderNoteRow( - baseNote, - backgroundColor, - makeItShort, - canPreview, - accountViewModel, - nav, + baseNote = baseNote, + backgroundColor = backgroundColor, + makeItShort = makeItShort, + canPreview = canPreview, + editState = editState, + accountViewModel = accountViewModel, + nav = nav, ) val noteEvent = baseNote.event @@ -1155,6 +1171,7 @@ private fun RenderNoteRow( backgroundColor: MutableState, makeItShort: Boolean, canPreview: Boolean, + editState: EditState, accountViewModel: AccountViewModel, nav: (String) -> Unit, ) { @@ -1287,6 +1304,7 @@ private fun RenderNoteRow( makeItShort, canPreview, backgroundColor, + editState, accountViewModel, nav, ) @@ -1343,19 +1361,29 @@ fun RenderTextEvent( makeItShort: Boolean, canPreview: Boolean, backgroundColor: MutableState, + editState: EditState, accountViewModel: AccountViewModel, nav: (String) -> Unit, ) { - LoadDecryptedContent(note, accountViewModel) { body -> + LoadDecryptedContent( + note, + accountViewModel, + ) { body -> val eventContent by remember(note.event) { derivedStateOf { val subject = (note.event as? TextNoteEvent)?.subject()?.ifEmpty { null } + val newBody = + if (editState.showOriginal.value || editState.modificationsInOrder.value.isEmpty()) { + body + } else { + editState.modificationsInOrder.value.firstOrNull()?.event?.content() ?: body + } - if (!subject.isNullOrBlank() && !body.split("\n")[0].contains(subject)) { - "### $subject\n$body" + if (!subject.isNullOrBlank() && !newBody.split("\n")[0].contains(subject)) { + "### $subject\n$newBody" } else { - body + newBody } } } @@ -2789,6 +2817,7 @@ fun DisplayLocation( fun FirstUserInfoRow( baseNote: Note, showAuthorPicture: Boolean, + editState: EditState, accountViewModel: AccountViewModel, nav: (String) -> Unit, ) { @@ -2823,12 +2852,37 @@ fun FirstUserInfoRow( DisplayFollowingHashtagsInPost(baseNote, accountViewModel, nav) } + if (!editState.modificationsInOrder.value.isEmpty()) { + DisplayEditStatus(editState.showOriginal) + } + TimeAgo(baseNote) - MoreOptionsButton(baseNote, accountViewModel) + MoreOptionsButton(baseNote, accountViewModel, nav) } } +@Composable +fun DisplayEditStatus(showOriginal: MutableState) { + ClickableText( + text = + if (showOriginal.value) { + buildAnnotatedString { append(stringResource(id = R.string.original)) } + } else { + buildAnnotatedString { append(stringResource(id = R.string.edited)) } + }, + onClick = { showOriginal.value = !showOriginal.value }, + style = + LocalTextStyle.current.copy( + color = MaterialTheme.colorScheme.placeholderText, + fontSize = Font14SP, + fontWeight = FontWeight.Bold, + ), + maxLines = 1, + modifier = HalfStartPadding, + ) +} + @Composable private fun BoostedMark() { Text( @@ -2844,6 +2898,7 @@ private fun BoostedMark() { fun MoreOptionsButton( baseNote: Note, accountViewModel: AccountViewModel, + nav: (String) -> Unit, ) { val popupExpanded = remember { mutableStateOf(false) } val enablePopup = remember { { popupExpanded.value = true } } @@ -2858,6 +2913,7 @@ fun MoreOptionsButton( baseNote, popupExpanded, accountViewModel, + nav, ) } } diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/note/UserProfilePicture.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/UserProfilePicture.kt index b49ef2786..965aa9394 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/note/UserProfilePicture.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/UserProfilePicture.kt @@ -66,6 +66,7 @@ import androidx.lifecycle.map import com.vitorpamplona.amethyst.R import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.model.User +import com.vitorpamplona.amethyst.ui.actions.EditPostView import com.vitorpamplona.amethyst.ui.components.RobohashAsyncImage import com.vitorpamplona.amethyst.ui.components.RobohashFallbackAsyncImage import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel @@ -455,6 +456,7 @@ fun NoteDropDownMenu( note: Note, popupExpanded: MutableState, accountViewModel: AccountViewModel, + nav: (String) -> Unit, ) { var reportDialogShowing by remember { mutableStateOf(false) } @@ -473,6 +475,20 @@ fun NoteDropDownMenu( val onDismiss = remember(popupExpanded) { { popupExpanded.value = false } } + val wantsToEditPost = + remember { + mutableStateOf(false) + } + + if (wantsToEditPost.value) { + EditPostView( + onClose = { wantsToEditPost.value = false }, + edit = note, + accountViewModel = accountViewModel, + nav = nav, + ) + } + DropdownMenu( expanded = popupExpanded.value, onDismissRequest = onDismiss, @@ -551,6 +567,14 @@ fun NoteDropDownMenu( }, ) Divider() + if (state.isLoggedUser) { + DropdownMenuItem( + text = { Text(stringResource(R.string.edit_post)) }, + onClick = { + wantsToEditPost.value = true + }, + ) + } DropdownMenuItem( text = { Text(stringResource(R.string.broadcast)) }, onClick = { diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/ThreadFeedView.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/ThreadFeedView.kt index 2d3153de5..f65619417 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/ThreadFeedView.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/ThreadFeedView.kt @@ -58,6 +58,7 @@ import androidx.compose.runtime.derivedStateOf import androidx.compose.runtime.getValue import androidx.compose.runtime.livedata.observeAsState import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.produceState import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.setValue @@ -102,11 +103,13 @@ import com.vitorpamplona.amethyst.ui.note.AudioTrackHeader import com.vitorpamplona.amethyst.ui.note.BadgeDisplay import com.vitorpamplona.amethyst.ui.note.BlankNote import com.vitorpamplona.amethyst.ui.note.CreateImageHeader +import com.vitorpamplona.amethyst.ui.note.DisplayEditStatus import com.vitorpamplona.amethyst.ui.note.DisplayHighlight import com.vitorpamplona.amethyst.ui.note.DisplayLocation import com.vitorpamplona.amethyst.ui.note.DisplayOts import com.vitorpamplona.amethyst.ui.note.DisplayPeopleList import com.vitorpamplona.amethyst.ui.note.DisplayRelaySet +import com.vitorpamplona.amethyst.ui.note.EditState import com.vitorpamplona.amethyst.ui.note.FileHeaderDisplay import com.vitorpamplona.amethyst.ui.note.FileStorageHeaderDisplay import com.vitorpamplona.amethyst.ui.note.HiddenNote @@ -120,6 +123,7 @@ import com.vitorpamplona.amethyst.ui.note.NoteUsernameDisplay import com.vitorpamplona.amethyst.ui.note.ReactionsRow import com.vitorpamplona.amethyst.ui.note.RenderAppDefinition import com.vitorpamplona.amethyst.ui.note.RenderEmojiPack +import com.vitorpamplona.amethyst.ui.note.RenderFhirResource import com.vitorpamplona.amethyst.ui.note.RenderGitPatchEvent import com.vitorpamplona.amethyst.ui.note.RenderGitRepositoryEvent import com.vitorpamplona.amethyst.ui.note.RenderPinListEvent @@ -156,6 +160,7 @@ import com.vitorpamplona.quartz.events.CommunityDefinitionEvent import com.vitorpamplona.quartz.events.CommunityPostApprovalEvent import com.vitorpamplona.quartz.events.EmojiPackEvent 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 @@ -372,6 +377,15 @@ fun NoteMaster( onClick = { showHiddenNote = true }, ) } else { + val editState by + produceState(initialValue = EditState(), key1 = baseNote) { + accountViewModel.findModificationEventsForNote(baseNote) { newModifications -> + if (value.modificationsInOrder.value != newModifications) { + value.modificationsInOrder.value = newModifications + } + } + } + Column( modifier .fillMaxWidth() @@ -407,6 +421,10 @@ fun NoteMaster( DisplayFollowingHashtagsInPost(baseNote, accountViewModel, nav) } + if (!editState.modificationsInOrder.value.isEmpty()) { + DisplayEditStatus(editState.showOriginal) + } + Text( timeAgo(note.createdAt(), context = context), color = MaterialTheme.colorScheme.placeholderText, @@ -424,7 +442,7 @@ fun NoteMaster( tint = MaterialTheme.colorScheme.placeholderText, ) - NoteDropDownMenu(baseNote, moreActionsExpanded, accountViewModel) + NoteDropDownMenu(baseNote, moreActionsExpanded, accountViewModel, nav) } } @@ -532,6 +550,8 @@ fun NoteMaster( accountViewModel, nav, ) + } else if (noteEvent is FhirResourceEvent) { + RenderFhirResource(baseNote, accountViewModel, nav) } else if (noteEvent is GitRepositoryEvent) { RenderGitRepositoryEvent(baseNote, accountViewModel, nav) } else if (noteEvent is GitPatchEvent) { @@ -577,6 +597,7 @@ fun NoteMaster( false, canPreview, backgroundColor, + editState, accountViewModel, nav, ) @@ -829,6 +850,11 @@ private fun RenderWikiHeaderForThreadPreview() { val accountViewModel = mockAccountViewModel() val nav: (String) -> Unit = {} + val editState by + remember { + mutableStateOf(EditState()) + } + runBlocking { withContext(Dispatchers.IO) { LocalCache.justConsume(event, null) @@ -851,6 +877,7 @@ private fun RenderWikiHeaderForThreadPreview() { false, true, backgroundColor, + editState, accountViewModel, nav, ) @@ -870,6 +897,7 @@ private fun RenderWikiHeaderForThreadPreview() { false, true, backgroundColor, + editState, accountViewModel, nav, ) diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/AccountViewModel.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/AccountViewModel.kt index 17ee736cc..f18c22e44 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/AccountViewModel.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/AccountViewModel.kt @@ -932,6 +932,15 @@ class AccountViewModel(val account: Account, val settings: SettingsState) : View } } + suspend fun findModificationEventsForNote( + note: Note, + onResult: (List) -> Unit, + ) { + withContext(Dispatchers.IO) { + onResult(LocalCache.findLatestModificationForNote(note)) + } + } + private suspend fun checkGetOrCreateChannel(key: HexKey): Channel? { return LocalCache.checkGetOrCreateChannel(key) } diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/ChannelScreen.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/ChannelScreen.kt index 0a586fd87..f24fca756 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/ChannelScreen.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/ChannelScreen.kt @@ -865,7 +865,7 @@ fun LongChannelHeader( ) Spacer(DoubleHorzSpacer) NormalTimeAgo(note, remember { Modifier.weight(1f) }) - MoreOptionsButton(note, accountViewModel) + MoreOptionsButton(note, accountViewModel, nav) } } } diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/VideoScreen.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/VideoScreen.kt index 24ff01a66..9f3e9136e 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/VideoScreen.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/VideoScreen.kt @@ -363,12 +363,12 @@ private fun RenderAuthorInformation( ) { Row(verticalAlignment = Alignment.CenterVertically) { NoteUsernameDisplay(note, remember { Modifier.weight(1f) }) - VideoUserOptionAction(note, accountViewModel) + VideoUserOptionAction(note, accountViewModel, nav) } Row(verticalAlignment = Alignment.CenterVertically) { ObserveDisplayNip05Status( - remember { note.author!! }, - remember { Modifier.weight(1f) }, + note.author!!, + Modifier.weight(1f), accountViewModel, nav = nav, ) @@ -387,6 +387,7 @@ private fun RenderAuthorInformation( private fun VideoUserOptionAction( note: Note, accountViewModel: AccountViewModel, + nav: (String) -> Unit, ) { val popupExpanded = remember { mutableStateOf(false) } val enablePopup = remember { { popupExpanded.value = true } } @@ -406,6 +407,7 @@ private fun VideoUserOptionAction( note, popupExpanded, accountViewModel, + nav, ) } } diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index d36dbb21a..f56c9324b 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -50,6 +50,8 @@ View count Boost boosted + edited + original Quote Fork New Amount in Sats @@ -790,4 +792,6 @@ Timestamp Proof There\'s proof this post was signed sometime before %1$s. The proof was stamped in the Bitcoin blockchain at that date and time. + + Edit Post 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 951901e4e..306277d04 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/events/EventFactory.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/events/EventFactory.kt @@ -126,6 +126,7 @@ class EventFactory { SealedGossipEvent.KIND -> SealedGossipEvent(id, pubKey, createdAt, tags, content, sig) StatusEvent.KIND -> StatusEvent(id, pubKey, createdAt, tags, content, sig) TextNoteEvent.KIND -> TextNoteEvent(id, pubKey, createdAt, tags, content, sig) + TextNoteModificationEvent.KIND -> TextNoteModificationEvent(id, pubKey, createdAt, tags, content, sig) VideoHorizontalEvent.KIND -> VideoHorizontalEvent(id, pubKey, createdAt, tags, content, sig) VideoVerticalEvent.KIND -> VideoVerticalEvent(id, pubKey, createdAt, tags, content, sig) VideoViewEvent.KIND -> VideoViewEvent(id, pubKey, createdAt, tags, content, sig) diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/events/TextNoteModificationEvent.kt b/quartz/src/main/java/com/vitorpamplona/quartz/events/TextNoteModificationEvent.kt new file mode 100644 index 000000000..377dbc91d --- /dev/null +++ b/quartz/src/main/java/com/vitorpamplona/quartz/events/TextNoteModificationEvent.kt @@ -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.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 TextNoteModificationEvent( + 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 = 1010 + const val ALT = "Content Change Event" + + fun create( + content: String, + eventId: HexKey, + signer: NostrSigner, + createdAt: Long = TimeUtils.now(), + onReady: (TextNoteModificationEvent) -> Unit, + ) { + val tags = arrayOf(arrayOf("e", eventId), arrayOf("alt", CalendarDateSlotEvent.ALT)) + signer.sign(createdAt, KIND, tags, content, onReady) + } + } +} From 417c29bd722762e092299d58ec9a083764d731ff Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Thu, 29 Feb 2024 09:18:56 -0500 Subject: [PATCH 09/98] Updates Compose UI Version --- app/build.gradle | 2 +- build.gradle | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/app/build.gradle b/app/build.gradle index 7f6f5be40..c91e93ea9 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -249,7 +249,7 @@ dependencies { implementation 'androidx.appcompat:appcompat:1.6.1' // Local model for language identification - playImplementation 'com.google.mlkit:language-id:17.0.4' + playImplementation 'com.google.mlkit:language-id:17.0.5' // Google services model the translate text playImplementation 'com.google.mlkit:translate:17.0.2' diff --git a/build.gradle b/build.gradle index e72a837d7..293e75fba 100644 --- a/build.gradle +++ b/build.gradle @@ -4,7 +4,7 @@ buildscript { ext { fragment_version = "1.6.2" lifecycle_version = '2.7.0' - compose_ui_version = '1.6.1' + compose_ui_version = '1.6.2' nav_version = '2.7.7' room_version = "2.4.3" accompanist_version = '0.34.0' From 6e0701b65df40e4e21111e226058fd84c5eee9c2 Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Thu, 29 Feb 2024 09:22:15 -0500 Subject: [PATCH 10/98] Removes unused kotlin serialization --- build.gradle | 1 - quartz/build.gradle | 1 - 2 files changed, 2 deletions(-) diff --git a/build.gradle b/build.gradle index 293e75fba..ac635ba5f 100644 --- a/build.gradle +++ b/build.gradle @@ -24,7 +24,6 @@ plugins { id 'com.android.library' version '8.2.2' apply false id 'org.jetbrains.kotlin.android' version '1.9.22' apply false id 'org.jetbrains.kotlin.jvm' version '1.9.22' apply false - id 'org.jetbrains.kotlin.plugin.serialization' version '1.9.22' apply false id 'androidx.benchmark' version '1.2.3' apply false id 'com.diffplug.spotless' version '6.25.0' apply false } diff --git a/quartz/build.gradle b/quartz/build.gradle index c5ff3ec13..48bc3289f 100644 --- a/quartz/build.gradle +++ b/quartz/build.gradle @@ -1,7 +1,6 @@ plugins { id 'com.android.library' id 'org.jetbrains.kotlin.android' - id 'org.jetbrains.kotlin.plugin.serialization' } android { From c686e775bf108ba9bf0c7776fd2a1d7560afa855 Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Thu, 29 Feb 2024 10:09:19 -0500 Subject: [PATCH 11/98] Deletes unused code --- .../com/vitorpamplona/amethyst/ui/navigation/Routes.kt | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/navigation/Routes.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/navigation/Routes.kt index ecdc18bf2..69d567ccd 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/navigation/Routes.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/navigation/Routes.kt @@ -240,16 +240,6 @@ sealed class Route( } } -// ** -// * Functions below only exist because we have not broken the datasource classes into backend and -// frontend. -// ** -@Composable -fun currentRoute(navController: NavHostController): String? { - val navBackStackEntry by navController.currentBackStackEntryAsState() - return navBackStackEntry?.destination?.route -} - open class LatestItem { var newestItemPerAccount: Map = mapOf() From 0084c2b532132bcbb64bb673eaa3b6aefeb9a2e1 Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Thu, 29 Feb 2024 14:19:50 -0500 Subject: [PATCH 12/98] Improves the speed of Location services. --- .../amethyst/service/LocationUtil.kt | 29 +++++++- .../amethyst/ui/actions/NewPostView.kt | 62 +++++++---------- .../amethyst/ui/navigation/AppTopBar.kt | 16 ++--- .../amethyst/ui/navigation/Routes.kt | 2 - .../amethyst/ui/note/NoteCompose.kt | 69 ++++++++++--------- .../ui/screen/loggedIn/GeoHashScreen.kt | 34 ++------- 6 files changed, 103 insertions(+), 109 deletions(-) diff --git a/app/src/main/java/com/vitorpamplona/amethyst/service/LocationUtil.kt b/app/src/main/java/com/vitorpamplona/amethyst/service/LocationUtil.kt index cd741c52a..cf02079bf 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/service/LocationUtil.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/service/LocationUtil.kt @@ -27,6 +27,7 @@ import android.location.Location import android.location.LocationListener import android.location.LocationManager import android.os.HandlerThread +import android.util.LruCache import androidx.compose.runtime.MutableState import androidx.compose.runtime.mutableStateOf import kotlinx.coroutines.CancellationException @@ -92,7 +93,33 @@ class LocationUtil(context: Context) { } } -class ReverseGeoLocationUtil { +object CachedGeoLocations { + val locationNames = LruCache(20) + + fun cached(geoHashStr: String): String? { + return locationNames[geoHashStr] + } + + suspend fun geoLocate( + geoHashStr: String, + location: Location, + context: Context, + ): String? { + locationNames[geoHashStr]?.let { + return it + } + + val name = ReverseGeoLocationUtil().execute(location, context)?.ifBlank { null } + + if (name != null) { + locationNames.put(geoHashStr, name) + } + + return name + } +} + +private class ReverseGeoLocationUtil { suspend fun execute( location: Location, context: Context, diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewPostView.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewPostView.kt index ef685f603..e8bdad62f 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewPostView.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewPostView.kt @@ -124,7 +124,6 @@ import androidx.compose.ui.window.DialogProperties import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.lifecycle.viewmodel.compose.viewModel import coil.compose.AsyncImage -import com.fonfon.kgeohash.toGeoHash import com.google.accompanist.permissions.ExperimentalPermissionsApi import com.google.accompanist.permissions.isGranted import com.google.accompanist.permissions.rememberPermissionState @@ -134,7 +133,6 @@ import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.model.User import com.vitorpamplona.amethyst.service.Nip96MediaServers import com.vitorpamplona.amethyst.service.NostrSearchEventOrUserDataSource -import com.vitorpamplona.amethyst.service.ReverseGeoLocationUtil import com.vitorpamplona.amethyst.ui.components.BechLink import com.vitorpamplona.amethyst.ui.components.CreateTextWithEmoji import com.vitorpamplona.amethyst.ui.components.InvoiceRequest @@ -144,6 +142,7 @@ import com.vitorpamplona.amethyst.ui.components.ZapRaiserRequest import com.vitorpamplona.amethyst.ui.note.BaseUserPicture import com.vitorpamplona.amethyst.ui.note.CancelIcon import com.vitorpamplona.amethyst.ui.note.CloseIcon +import com.vitorpamplona.amethyst.ui.note.LoadCityName import com.vitorpamplona.amethyst.ui.note.NoteCompose import com.vitorpamplona.amethyst.ui.note.PollIcon import com.vitorpamplona.amethyst.ui.note.RegularPostIcon @@ -1172,23 +1171,12 @@ fun FowardZapTo( @OptIn(ExperimentalPermissionsApi::class) @Composable fun LocationAsHash(postViewModel: NewPostViewModel) { - val context = LocalContext.current - val locationPermissionState = rememberPermissionState( Manifest.permission.ACCESS_COARSE_LOCATION, ) if (locationPermissionState.status.isGranted) { - var locationDescriptionFlow by remember(postViewModel) { mutableStateOf?>(null) } - - DisposableEffect(key1 = Unit) { - postViewModel.startLocation(context = context) - locationDescriptionFlow = postViewModel.location - - onDispose { postViewModel.stopLocation() } - } - Column( modifier = Modifier.fillMaxWidth(), ) { @@ -1219,7 +1207,7 @@ fun LocationAsHash(postViewModel: NewPostViewModel) { modifier = Modifier.padding(start = 10.dp), ) - locationDescriptionFlow?.let { geoLocation -> DisplayLocationObserver(geoLocation) } + DisplayLocationObserver(postViewModel) } Divider() @@ -1236,41 +1224,39 @@ fun LocationAsHash(postViewModel: NewPostViewModel) { } @Composable -fun DisplayLocationObserver(geoLocation: Flow) { - val location by geoLocation.collectAsStateWithLifecycle(null) +fun DisplayLocationObserver(postViewModel: NewPostViewModel) { + val context = LocalContext.current + var locationDescriptionFlow by remember(postViewModel) { mutableStateOf?>(null) } - location?.let { DisplayLocationInTitle(geohash = it) } + DisposableEffect(key1 = context) { + postViewModel.startLocation(context = context) + locationDescriptionFlow = postViewModel.location + + onDispose { postViewModel.stopLocation() } + } + + locationDescriptionFlow?.let { + val location by it.collectAsStateWithLifecycle(null) + + location?.let { DisplayLocationInTitle(geohash = it) } + } } @Composable fun DisplayLocationInTitle(geohash: String) { - val context = LocalContext.current - - var cityName by remember(geohash) { mutableStateOf(geohash) } - - LaunchedEffect(key1 = geohash) { - launch(Dispatchers.IO) { - val newCityName = - ReverseGeoLocationUtil().execute(geohash.toGeoHash().toLocation(), context)?.ifBlank { - null - } - - if (newCityName != null && newCityName != cityName) { - cityName = newCityName - } - } - } - - if (geohash != "s0000") { + LoadCityName( + geohashStr = geohash, + onLoading = { + Spacer(modifier = StdHorzSpacer) + LoadingAnimation() + }, + ) { cityName -> Text( text = cityName, fontSize = 20.sp, fontWeight = FontWeight.W500, modifier = Modifier.padding(start = Size5dp), ) - } else { - Spacer(modifier = StdHorzSpacer) - LoadingAnimation() } } diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/navigation/AppTopBar.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/navigation/AppTopBar.kt index c5dfcf639..ca5e7ff4c 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/navigation/AppTopBar.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/navigation/AppTopBar.kt @@ -74,7 +74,6 @@ import androidx.lifecycle.map import androidx.lifecycle.viewModelScope import androidx.navigation.NavBackStackEntry import coil.Coil -import com.fonfon.kgeohash.toGeoHash import com.vitorpamplona.amethyst.R import com.vitorpamplona.amethyst.model.Account import com.vitorpamplona.amethyst.model.AddressableNote @@ -817,15 +816,12 @@ fun SimpleTextSpinner( fun RenderOption(option: Name) { when (option) { is GeoHashName -> { - val geohash = runCatching { option.geoHashTag.toGeoHash() }.getOrNull() - if (geohash != null) { - LoadCityName(geohash) { - Row( - horizontalArrangement = Arrangement.Center, - modifier = Modifier.fillMaxWidth(), - ) { - Text(text = "/g/$it", color = MaterialTheme.colorScheme.onSurface) - } + LoadCityName(option.geoHashTag) { + Row( + horizontalArrangement = Arrangement.Center, + modifier = Modifier.fillMaxWidth(), + ) { + Text(text = "/g/$it", color = MaterialTheme.colorScheme.onSurface) } } } diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/navigation/Routes.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/navigation/Routes.kt index 69d567ccd..8ad6a0d5b 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/navigation/Routes.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/navigation/Routes.kt @@ -22,7 +22,6 @@ package com.vitorpamplona.amethyst.ui.navigation import android.os.Bundle import androidx.compose.foundation.layout.size -import androidx.compose.runtime.Composable import androidx.compose.runtime.Immutable import androidx.compose.runtime.State import androidx.compose.runtime.getValue @@ -32,7 +31,6 @@ import androidx.navigation.NavBackStackEntry import androidx.navigation.NavDestination import androidx.navigation.NavHostController import androidx.navigation.NavType -import androidx.navigation.compose.currentBackStackEntryAsState import androidx.navigation.navArgument import com.vitorpamplona.amethyst.R import com.vitorpamplona.amethyst.model.Account 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 ee4584be8..b81624a5a 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 @@ -101,7 +101,6 @@ 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 import com.vitorpamplona.amethyst.commons.BaseMediaContent @@ -115,7 +114,7 @@ import com.vitorpamplona.amethyst.model.Channel import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.model.RelayBriefInfoCache import com.vitorpamplona.amethyst.model.User -import com.vitorpamplona.amethyst.service.ReverseGeoLocationUtil +import com.vitorpamplona.amethyst.service.CachedGeoLocations import com.vitorpamplona.amethyst.ui.actions.NewRelayListView import com.vitorpamplona.amethyst.ui.components.ClickableUrl import com.vitorpamplona.amethyst.ui.components.CreateClickableTextWithEmoji @@ -2768,23 +2767,34 @@ fun LoadOts( @Composable fun LoadCityName( - geohash: GeoHash, + geohashStr: String, + onLoading: (@Composable () -> Unit)? = null, content: @Composable (String) -> Unit, ) { - val context = LocalContext.current - var cityName by remember(geohash) { mutableStateOf(geohash.toString()) } + var cityName by remember(geohashStr) { mutableStateOf(CachedGeoLocations.cached(geohashStr)) } - LaunchedEffect(key1 = geohash) { - launch(Dispatchers.IO) { - val newCityName = - ReverseGeoLocationUtil().execute(geohash.toLocation(), context)?.ifBlank { null } - if (newCityName != null && newCityName != cityName) { - cityName = newCityName + if (cityName == null) { + if (onLoading != null) { + onLoading() + } + + val context = LocalContext.current + + LaunchedEffect(key1 = geohashStr, context) { + launch(Dispatchers.IO) { + val geoHash = runCatching { geohashStr.toGeoHash() }.getOrNull() + if (geoHash != null) { + val newCityName = + CachedGeoLocations.geoLocate(geohashStr, geoHash.toLocation(), context)?.ifBlank { null } + if (newCityName != null && newCityName != cityName) { + cityName = newCityName + } + } } } + } else { + cityName?.let { content(it) } } - - content(cityName) } @Composable @@ -2792,24 +2802,21 @@ fun DisplayLocation( geohashStr: String, nav: (String) -> Unit, ) { - val geoHash = runCatching { geohashStr.toGeoHash() }.getOrNull() - if (geoHash != null) { - LoadCityName(geoHash) { cityName -> - ClickableText( - text = AnnotatedString(cityName), - onClick = { nav("Geohash/$geoHash") }, - style = - LocalTextStyle.current.copy( - color = - MaterialTheme.colorScheme.primary.copy( - alpha = 0.52f, - ), - fontSize = Font14SP, - fontWeight = FontWeight.Bold, - ), - maxLines = 1, - ) - } + LoadCityName(geohashStr) { cityName -> + ClickableText( + text = AnnotatedString(cityName), + onClick = { nav("Geohash/$geohashStr") }, + style = + LocalTextStyle.current.copy( + color = + MaterialTheme.colorScheme.primary.copy( + alpha = 0.52f, + ), + fontSize = Font14SP, + fontWeight = FontWeight.Bold, + ), + maxLines = 1, + ) } } diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/GeoHashScreen.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/GeoHashScreen.kt index 33b0dc61c..e1f4515b0 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/GeoHashScreen.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/GeoHashScreen.kt @@ -31,32 +31,26 @@ import androidx.compose.material3.Divider import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.DisposableEffect -import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.derivedStateOf import androidx.compose.runtime.getValue import androidx.compose.runtime.livedata.observeAsState -import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalLifecycleOwner import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.unit.dp import androidx.lifecycle.Lifecycle import androidx.lifecycle.LifecycleEventObserver import androidx.lifecycle.viewmodel.compose.viewModel -import com.fonfon.kgeohash.toGeoHash import com.vitorpamplona.amethyst.R import com.vitorpamplona.amethyst.service.NostrGeohashDataSource -import com.vitorpamplona.amethyst.service.ReverseGeoLocationUtil +import com.vitorpamplona.amethyst.ui.note.LoadCityName import com.vitorpamplona.amethyst.ui.screen.NostrGeoHashFeedViewModel import com.vitorpamplona.amethyst.ui.screen.RefresheableFeedView import com.vitorpamplona.amethyst.ui.theme.DividerThickness import com.vitorpamplona.amethyst.ui.theme.StdPadding -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.launch @Composable fun GeoHashScreen( @@ -174,27 +168,13 @@ fun DislayGeoTagHeader( geohash: String, modifier: Modifier, ) { - val context = LocalContext.current - - var cityName by remember(geohash) { mutableStateOf(geohash) } - - LaunchedEffect(key1 = geohash) { - launch(Dispatchers.IO) { - val newCityName = - ReverseGeoLocationUtil().execute(geohash.toGeoHash().toLocation(), context)?.ifBlank { - null - } - if (newCityName != null && newCityName != cityName) { - cityName = "$newCityName ($geohash)" - } - } + LoadCityName(geohashStr = geohash) { cityName -> + Text( + cityName, + fontWeight = FontWeight.Bold, + modifier = modifier, + ) } - - Text( - cityName, - fontWeight = FontWeight.Bold, - modifier = modifier, - ) } @Composable From fd37826663732b42eb503d9605474941a988cd93 Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Thu, 29 Feb 2024 14:52:56 -0500 Subject: [PATCH 13/98] Refactoring to improve legibility and compose stability of FhirResources --- .../amethyst/ui/note/MiniFhir.kt | 38 +++++++++++++++ .../amethyst/ui/note/NoteCompose.kt | 46 +++++-------------- 2 files changed, 50 insertions(+), 34 deletions(-) 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 index b782a1c19..7f4b59514 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/note/MiniFhir.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/MiniFhir.kt @@ -20,8 +20,14 @@ */ package com.vitorpamplona.amethyst.ui.note +import android.util.Log import com.fasterxml.jackson.annotation.JsonSubTypes import com.fasterxml.jackson.annotation.JsonTypeInfo +import com.fasterxml.jackson.databind.DeserializationFeature +import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper +import kotlinx.collections.immutable.ImmutableMap +import kotlinx.collections.immutable.persistentMapOf +import kotlinx.collections.immutable.toImmutableMap @JsonTypeInfo( use = JsonTypeInfo.Id.NAME, @@ -110,6 +116,11 @@ class Reference( var reference: String? = null, ) +data class FhirElementDatabase( + var baseResource: Resource? = null, + var localDb: ImmutableMap = persistentMapOf(), +) + fun findReferenceInDb( it: String, db: Map, @@ -121,3 +132,30 @@ fun findReferenceInDb( db.get(it.removePrefix("#")) } } + +fun parseResourceBundleOrNull(json: String): FhirElementDatabase? { + val mapper = + jacksonObjectMapper().configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false) + + return try { + val resource = mapper.readValue(json, Resource::class.java) + + val db = + when (resource) { + is Bundle -> { + resource.entry.associateBy { it.id }.toImmutableMap() + } + else -> { + persistentMapOf(resource.id to resource) + } + } + + FhirElementDatabase( + localDb = db, + baseResource = resource, + ) + } catch (e: Exception) { + Log.e("RenderEyeGlassesPrescription", "Parser error", e) + null + } +} 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 b81624a5a..6f91988e8 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 @@ -99,8 +99,6 @@ 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.toGeoHash import com.vitorpamplona.amethyst.R import com.vitorpamplona.amethyst.commons.BaseMediaContent @@ -131,7 +129,6 @@ 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 @@ -244,6 +241,7 @@ import com.vitorpamplona.quartz.events.VideoVerticalEvent import com.vitorpamplona.quartz.events.WikiNoteEvent import com.vitorpamplona.quartz.events.toImmutableListOfLists import kotlinx.collections.immutable.ImmutableList +import kotlinx.collections.immutable.ImmutableMap import kotlinx.collections.immutable.persistentListOf import kotlinx.collections.immutable.toImmutableList import kotlinx.coroutines.Dispatchers @@ -4430,12 +4428,9 @@ 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) + RenderFhirResource(prescriptionEvent) } @Composable @@ -4446,45 +4441,30 @@ fun RenderFhirResource( ) { val event = baseNote.event as? FhirResourceEvent ?: return - RenderFhirResource(event, accountViewModel, nav) + RenderFhirResource(event) } @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) +fun RenderFhirResource(event: FhirResourceEvent) { + val state by produceState(initialValue = FhirElementDatabase(), key1 = event) { + withContext(Dispatchers.Default) { + parseResourceBundleOrNull(event.content)?.let { + value = it } } } - baseResource?.let { resource -> + state.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) + RenderEyeGlassesPrescription(it, state.localDb) } } is VisionPrescription -> { - val db = mapOf(resource.id to resource) - RenderEyeGlassesPrescription(resource, db, accountViewModel, nav) + RenderEyeGlassesPrescription(resource, state.localDb) } else -> { } @@ -4495,9 +4475,7 @@ fun RenderFhirResource( @Composable fun RenderEyeGlassesPrescription( visionPrescription: VisionPrescription, - db: Map, - accountViewModel: AccountViewModel, - nav: (String) -> Unit, + db: ImmutableMap, ) { Column( modifier = From 8d8990899b20491559fc39404f04a8b523ca2ee5 Mon Sep 17 00:00:00 2001 From: Crowdin Bot Date: Thu, 29 Feb 2024 19:55:30 +0000 Subject: [PATCH 14/98] New Crowdin translations by GitHub Action --- app/src/main/res/values-fr/strings.xml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/app/src/main/res/values-fr/strings.xml b/app/src/main/res/values-fr/strings.xml index 266c0422d..07ed62499 100644 --- a/app/src/main/res/values-fr/strings.xml +++ b/app/src/main/res/values-fr/strings.xml @@ -47,6 +47,8 @@ Nombre de vues Boost boosté + modifié + original Citation Fork Nouveau montant en Sats @@ -670,4 +672,5 @@ OTS: %1$s Preuve d\'horodatage Il y a une preuve que ce message a été signé avant %1$s. La preuve a été estampillée dans la blockchain Bitcoin à cette date et à cette heure. + Modifier le Message From d506cf13a9965f0aa2c4420421b775a8d60cbd42 Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Thu, 29 Feb 2024 17:05:41 -0500 Subject: [PATCH 15/98] Adjustments to padding of the last element and the size of the relay box in kind 1 --- .../amethyst/ui/components/LoadUrlPreview.kt | 22 ++--- .../amethyst/ui/components/RichTextViewer.kt | 12 +++ .../amethyst/ui/note/ReactionsRow.kt | 2 +- .../amethyst/ui/note/RelayListBox.kt | 93 ++++++++++++++++--- .../amethyst/ui/note/RelayListRow.kt | 9 +- .../ui/screen/loggedIn/DiscoverScreen.kt | 70 +++++++++----- .../vitorpamplona/amethyst/ui/theme/Shape.kt | 6 +- .../vitorpamplona/amethyst/ui/theme/Theme.kt | 4 +- 8 files changed, 159 insertions(+), 59 deletions(-) diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/components/LoadUrlPreview.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/components/LoadUrlPreview.kt index 2d71b7079..ca0b2cb5e 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/components/LoadUrlPreview.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/components/LoadUrlPreview.kt @@ -24,9 +24,8 @@ import androidx.compose.animation.Crossfade import androidx.compose.animation.core.tween import androidx.compose.foundation.layout.Box import androidx.compose.runtime.Composable -import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.produceState import androidx.compose.runtime.remember import androidx.compose.runtime.setValue import com.vitorpamplona.amethyst.commons.MediaUrlImage @@ -46,19 +45,16 @@ fun LoadUrlPreview( if (!automaticallyShowUrlPreview) { ClickableUrl(urlText, url) } else { - var urlPreviewState by - remember(url) { - mutableStateOf( - UrlCachedPreviewer.cache.get(url) ?: UrlPreviewState.Loading, - ) + val urlPreviewState by + produceState( + initialValue = UrlCachedPreviewer.cache.get(url) ?: UrlPreviewState.Loading, + key1 = url, + ) { + if (value == UrlPreviewState.Loading) { + accountViewModel.urlPreview(url) { value = it } + } } - // Doesn't use a viewModel because of viewModel reusing issues (too many UrlPreview are - // created). - if (urlPreviewState == UrlPreviewState.Loading) { - LaunchedEffect(url) { accountViewModel.urlPreview(url) { urlPreviewState = it } } - } - Crossfade( targetState = urlPreviewState, animationSpec = tween(durationMillis = 100), diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/components/RichTextViewer.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/components/RichTextViewer.kt index 9c525fc3c..db6a35bc8 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/components/RichTextViewer.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/components/RichTextViewer.kt @@ -28,6 +28,7 @@ import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.ExperimentalLayoutApi import androidx.compose.foundation.layout.FlowRow import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.padding import androidx.compose.foundation.text.InlineTextContent import androidx.compose.foundation.text.appendInlineContent @@ -102,6 +103,7 @@ import com.vitorpamplona.amethyst.ui.screen.loggedIn.LoadedBechLink import com.vitorpamplona.amethyst.ui.theme.Font17SP import com.vitorpamplona.amethyst.ui.theme.HalfVertPadding import com.vitorpamplona.amethyst.ui.theme.MarkdownTextStyle +import com.vitorpamplona.amethyst.ui.theme.StdVertSpacer import com.vitorpamplona.amethyst.ui.theme.innerPostModifier import com.vitorpamplona.amethyst.ui.theme.markdownStyle import com.vitorpamplona.amethyst.ui.uriToRoute @@ -344,6 +346,16 @@ private fun RenderRegular( } } } + + // UrlPreviews and Images have a 5dp spacing down. This also adds the space to Text. + val lastElement = state.paragraphs.lastOrNull()?.words?.lastOrNull() + if (lastElement !is ImageSegment && + lastElement !is LinkSegment && + lastElement !is InvoiceSegment && + lastElement !is CashuSegment + ) { + Spacer(modifier = StdVertSpacer) + } } } diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/note/ReactionsRow.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/ReactionsRow.kt index 794a20752..25de08d53 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/note/ReactionsRow.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/ReactionsRow.kt @@ -150,7 +150,7 @@ fun ReactionsRow( ) { val wantsToSeeReactions = remember(baseNote) { mutableStateOf(false) } - Spacer(modifier = HalfDoubleVertSpacer) + // Spacer(modifier = HalfDoubleVertSpacer) InnerReactionRow(baseNote, showReactionDetail, wantsToSeeReactions, accountViewModel, nav) diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/note/RelayListBox.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/RelayListBox.kt index 71c661083..2d946e5a1 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/note/RelayListBox.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/RelayListBox.kt @@ -20,11 +20,19 @@ */ package com.vitorpamplona.amethyst.ui.note +import androidx.compose.foundation.background import androidx.compose.foundation.layout.Arrangement +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.Row import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.shape.CircleShape import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.ExpandMore import androidx.compose.material3.Icon @@ -37,14 +45,17 @@ import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.Color import androidx.compose.ui.res.stringResource +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp import com.vitorpamplona.amethyst.R import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel -import com.vitorpamplona.amethyst.ui.theme.DoubleVertSpacer import com.vitorpamplona.amethyst.ui.theme.ShowMoreRelaysButtonBoxModifer -import com.vitorpamplona.amethyst.ui.theme.ShowMoreRelaysButtonIconButtonModifier -import com.vitorpamplona.amethyst.ui.theme.ShowMoreRelaysButtonIconModifier +import com.vitorpamplona.amethyst.ui.theme.StdVertSpacer import com.vitorpamplona.amethyst.ui.theme.placeholderText @OptIn(ExperimentalLayoutApi::class) @@ -58,16 +69,18 @@ fun RelayBadges( val relayList by baseNote.live().relayInfo.observeAsState(baseNote.relays) - Spacer(DoubleVertSpacer) + Spacer(StdVertSpacer) // FlowRow Seems to be a lot faster than LazyVerticalGrid - FlowRow { - if (expanded) { - relayList?.forEach { RenderRelay(it, accountViewModel, nav) } - } else { - relayList?.getOrNull(0)?.let { RenderRelay(it, accountViewModel, nav) } - relayList?.getOrNull(1)?.let { RenderRelay(it, accountViewModel, nav) } - relayList?.getOrNull(2)?.let { RenderRelay(it, accountViewModel, nav) } + Box(modifier = Modifier.fillMaxWidth().padding(start = 2.dp, end = 1.dp)) { + FlowRow(modifier = Modifier.fillMaxWidth()) { + if (expanded) { + relayList?.forEach { RenderRelay(it, accountViewModel, nav) } + } else { + relayList?.getOrNull(0)?.let { RenderRelay(it, accountViewModel, nav) } + relayList?.getOrNull(1)?.let { RenderRelay(it, accountViewModel, nav) } + relayList?.getOrNull(2)?.let { RenderRelay(it, accountViewModel, nav) } + } } } @@ -76,6 +89,62 @@ fun RelayBadges( } } +@OptIn(ExperimentalLayoutApi::class) +@Preview +@Composable +fun RelayIconLayoutPreview() { + Column(modifier = Modifier.width(55.dp)) { + Spacer(StdVertSpacer) + + // FlowRow Seems to be a lot faster than LazyVerticalGrid + Box(modifier = Modifier.fillMaxWidth().padding(start = 2.dp, end = 1.dp)) { + FlowRow { + Box( + modifier = Modifier.size(17.dp), + contentAlignment = Alignment.Center, + ) { + Box( + Modifier.size(13.dp).clip(shape = CircleShape).background(Color.Black), + ) + } + Box( + modifier = Modifier.size(17.dp), + contentAlignment = Alignment.Center, + ) { + Box(Modifier.size(13.dp).clip(shape = CircleShape).background(Color.Black)) + } + Box( + modifier = Modifier.size(17.dp), + contentAlignment = Alignment.Center, + ) { + Box(Modifier.size(13.dp).clip(shape = CircleShape).background(Color.Black)) + } + + Box( + modifier = Modifier.size(17.dp), + contentAlignment = Alignment.Center, + ) { + Box(Modifier.size(13.dp).clip(shape = CircleShape).background(Color.Black)) + } + Box( + modifier = Modifier.size(17.dp), + contentAlignment = Alignment.Center, + ) { + Box(Modifier.size(13.dp).clip(shape = CircleShape).background(Color.Black)) + } + Box( + modifier = Modifier.size(17.dp), + contentAlignment = Alignment.Center, + ) { + Box(Modifier.size(13.dp).clip(shape = CircleShape).background(Color.Black)) + } + } + } + + ShowMoreRelaysButton { } + } +} + @Composable private fun ShowMoreRelaysButton(onClick: () -> Unit) { Row( @@ -84,13 +153,11 @@ private fun ShowMoreRelaysButton(onClick: () -> Unit) { verticalAlignment = Alignment.Top, ) { IconButton( - modifier = ShowMoreRelaysButtonIconButtonModifier, onClick = onClick, ) { Icon( imageVector = Icons.Default.ExpandMore, contentDescription = stringResource(id = R.string.expand_relay_list), - modifier = ShowMoreRelaysButtonIconModifier, tint = MaterialTheme.colorScheme.placeholderText, ) } diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/note/RelayListRow.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/RelayListRow.kt index cac24a089..ee5ecfcde 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/note/RelayListRow.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/RelayListRow.kt @@ -26,7 +26,6 @@ import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.ExperimentalLayoutApi import androidx.compose.foundation.layout.FlowRow -import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.ChevronRight @@ -47,7 +46,6 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.stringResource import androidx.compose.ui.semantics.Role -import androidx.compose.ui.unit.dp import androidx.lifecycle.map import com.vitorpamplona.amethyst.R import com.vitorpamplona.amethyst.model.Note @@ -59,7 +57,7 @@ import com.vitorpamplona.amethyst.ui.components.RobohashFallbackAsyncImage import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel import com.vitorpamplona.amethyst.ui.theme.RelayIconFilter import com.vitorpamplona.amethyst.ui.theme.Size15Modifier -import com.vitorpamplona.amethyst.ui.theme.Size15dp +import com.vitorpamplona.amethyst.ui.theme.Size17dp import com.vitorpamplona.amethyst.ui.theme.StdStartPadding import com.vitorpamplona.amethyst.ui.theme.placeholderText import com.vitorpamplona.amethyst.ui.theme.relayIconModifier @@ -164,13 +162,12 @@ fun RenderRelay( val context = LocalContext.current val interactionSource = remember { MutableInteractionSource() } - val ripple = rememberRipple(bounded = false, radius = Size15dp) + val ripple = rememberRipple(bounded = false, radius = Size17dp) val clickableModifier = remember(relay) { Modifier - .padding(1.dp) - .size(Size15dp) + .size(Size17dp) .clickable( role = Role.Button, interactionSource = interactionSource, diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/DiscoverScreen.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/DiscoverScreen.kt index e42c2fe6c..10ff2a720 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/DiscoverScreen.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/DiscoverScreen.kt @@ -23,7 +23,6 @@ package com.vitorpamplona.amethyst.ui.screen.loggedIn import androidx.compose.animation.Crossfade import androidx.compose.animation.core.tween import androidx.compose.foundation.ExperimentalFoundationApi -import androidx.compose.foundation.gestures.ScrollableState import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.fillMaxHeight @@ -232,7 +231,7 @@ private fun RenderDiscoverFeed( viewModel: FeedViewModel, routeForLastRead: String?, forceEventKind: Int?, - listState: ScrollableState, + listState: LazyGridState, accountViewModel: AccountViewModel, nav: (String) -> Unit, ) { @@ -251,25 +250,54 @@ private fun RenderDiscoverFeed( FeedError(state.errorMessage) { viewModel.invalidateData() } } is FeedState.Loaded -> { - if (listState is LazyGridState) { - DiscoverFeedColumnsLoaded( - state, - routeForLastRead, - listState, - forceEventKind, - accountViewModel, - nav, - ) - } else if (listState is LazyListState) { - DiscoverFeedLoaded( - state, - routeForLastRead, - listState, - forceEventKind, - accountViewModel, - nav, - ) - } + DiscoverFeedColumnsLoaded( + state, + routeForLastRead, + listState, + forceEventKind, + accountViewModel, + nav, + ) + } + is FeedState.Loading -> { + LoadingFeed() + } + } + } +} + +@Composable +private fun RenderDiscoverFeed( + viewModel: FeedViewModel, + routeForLastRead: String?, + forceEventKind: Int?, + listState: LazyListState, + accountViewModel: AccountViewModel, + nav: (String) -> Unit, +) { + val feedState by viewModel.feedContent.collectAsStateWithLifecycle() + + Crossfade( + targetState = feedState, + animationSpec = tween(durationMillis = 100), + label = "RenderDiscoverFeed", + ) { state -> + when (state) { + is FeedState.Empty -> { + FeedEmpty { viewModel.invalidateData() } + } + is FeedState.FeedError -> { + FeedError(state.errorMessage) { viewModel.invalidateData() } + } + is FeedState.Loaded -> { + DiscoverFeedLoaded( + state, + routeForLastRead, + listState, + forceEventKind, + accountViewModel, + nav, + ) } is FeedState.Loading -> { LoadingFeed() diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/theme/Shape.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/theme/Shape.kt index 0d10f938d..29b1300ed 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/theme/Shape.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/theme/Shape.kt @@ -146,9 +146,9 @@ val Height4dpModifier = Modifier.height(4.dp) val AccountPictureModifier = Modifier.size(55.dp).clip(shape = CircleShape) val HeaderPictureModifier = Modifier.size(34.dp).clip(shape = CircleShape) -val ShowMoreRelaysButtonIconButtonModifier = Modifier.size(24.dp) -val ShowMoreRelaysButtonIconModifier = Modifier.size(15.dp) -val ShowMoreRelaysButtonBoxModifer = Modifier.fillMaxWidth().height(25.dp) +val ShowMoreRelaysButtonIconButtonModifier = Modifier.size(15.dp) +val ShowMoreRelaysButtonIconModifier = Modifier.size(20.dp) +val ShowMoreRelaysButtonBoxModifer = Modifier.fillMaxWidth().height(17.dp) val ChatBubbleMaxSizeModifier = Modifier.fillMaxWidth(0.85f) diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/theme/Theme.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/theme/Theme.kt index 2fcdc92b9..4bfebcc01 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/theme/Theme.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/theme/Theme.kt @@ -167,14 +167,14 @@ val LightReplyBorderModifier = val DarkInnerPostBorderModifier = Modifier - .padding(top = 5.dp) + .padding(vertical = 5.dp) .fillMaxWidth() .clip(shape = QuoteBorder) .border(1.dp, DarkSubtleBorder, QuoteBorder) val LightInnerPostBorderModifier = Modifier - .padding(top = 5.dp) + .padding(vertical = 5.dp) .fillMaxWidth() .clip(shape = QuoteBorder) .border(1.dp, LightSubtleBorder, QuoteBorder) From d257f7e253f8dc87c574e0b4eaa87cf027c9d83f Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Thu, 29 Feb 2024 17:35:44 -0500 Subject: [PATCH 16/98] Fixes not being able to create an invoice because of an old cache of the lud16 Fixes inserting invoice in the cursor. --- .../amethyst/ui/actions/NewPostView.kt | 48 +++++++++---------- .../amethyst/ui/actions/NewPostViewModel.kt | 16 +++++++ 2 files changed, 39 insertions(+), 25 deletions(-) diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewPostView.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewPostView.kt index e8bdad62f..854fa87f8 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewPostView.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewPostView.kt @@ -475,34 +475,32 @@ fun NewPostView( } } - val user = postViewModel.account?.userProfile() - val lud16 = user?.info?.lnAddress() - - if (lud16 != null && postViewModel.wantsInvoice) { - Row( - verticalAlignment = Alignment.CenterVertically, - modifier = Modifier.padding(vertical = Size5dp, horizontal = Size10dp), - ) { - Column(Modifier.fillMaxWidth()) { - InvoiceRequest( - lud16, - user.pubkeyHex, - accountViewModel.account, - stringResource(id = R.string.lightning_invoice), - stringResource(id = R.string.lightning_create_and_add_invoice), - onSuccess = { - postViewModel.message = - TextFieldValue(postViewModel.message.text + "\n\n" + it) - postViewModel.wantsInvoice = false - }, - onClose = { postViewModel.wantsInvoice = false }, - onError = { title, message -> accountViewModel.toast(title, message) }, - ) + if (postViewModel.wantsInvoice) { + postViewModel.lnAddress()?.let { lud16 -> + Row( + verticalAlignment = Alignment.CenterVertically, + modifier = Modifier.padding(vertical = Size5dp, horizontal = Size10dp), + ) { + Column(Modifier.fillMaxWidth()) { + InvoiceRequest( + lud16, + accountViewModel.account.userProfile().pubkeyHex, + accountViewModel.account, + stringResource(id = R.string.lightning_invoice), + stringResource(id = R.string.lightning_create_and_add_invoice), + onSuccess = { + postViewModel.insertAtCursor(it) + postViewModel.wantsInvoice = false + }, + onClose = { postViewModel.wantsInvoice = false }, + onError = { title, message -> accountViewModel.toast(title, message) }, + ) + } } } } - if (lud16 != null && postViewModel.wantsZapraiser) { + if (postViewModel.wantsZapraiser && postViewModel.hasLnAddress()) { Row( verticalAlignment = Alignment.CenterVertically, modifier = Modifier.padding(vertical = Size5dp, horizontal = Size10dp), @@ -569,7 +567,7 @@ private fun BottomRowActions(postViewModel: NewPostViewModel) { } } - if (postViewModel.canAddInvoice) { + if (postViewModel.canAddInvoice && postViewModel.hasLnAddress()) { AddLnInvoiceButton(postViewModel.wantsInvoice) { postViewModel.wantsInvoice = !postViewModel.wantsInvoice } diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewPostViewModel.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewPostViewModel.kt index 1c7284708..47838ecd5 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewPostViewModel.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewPostViewModel.kt @@ -163,6 +163,18 @@ open class NewPostViewModel() : ViewModel() { // NIP24 Wrapped DMs / Group messages var nip24 by mutableStateOf(false) + fun lnAddress(): String? { + return account?.userProfile()?.info?.lnAddress() + } + + fun hasLnAddress(): Boolean { + return account?.userProfile()?.info?.lnAddress() != null + } + + fun user(): User? { + return account?.userProfile() + } + open fun load( accountViewModel: AccountViewModel, replyingTo: Note?, @@ -819,6 +831,10 @@ open class NewPostViewModel() : ViewModel() { ) } + fun insertAtCursor(newElement: String) { + message = message.insertUrlAtCursor(newElement) + } + fun createNIP95Record( bytes: ByteArray, mimeType: String?, From 0755bd24746b0be2905bc747ebe5e59f2c7adec1 Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Thu, 29 Feb 2024 18:02:52 -0500 Subject: [PATCH 17/98] Improving the cache of LnInvoices. --- .../amethyst/service/lnurl/CachedLnInvoice.kt | 60 +++++++++++++++++++ .../amethyst/ui/components/InvoicePreview.kt | 59 +++++++++--------- 2 files changed, 89 insertions(+), 30 deletions(-) create mode 100644 app/src/main/java/com/vitorpamplona/amethyst/service/lnurl/CachedLnInvoice.kt diff --git a/app/src/main/java/com/vitorpamplona/amethyst/service/lnurl/CachedLnInvoice.kt b/app/src/main/java/com/vitorpamplona/amethyst/service/lnurl/CachedLnInvoice.kt new file mode 100644 index 000000000..67a7edcc4 --- /dev/null +++ b/app/src/main/java/com/vitorpamplona/amethyst/service/lnurl/CachedLnInvoice.kt @@ -0,0 +1,60 @@ +/** + * 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.service.lnurl + +import android.util.LruCache +import androidx.compose.runtime.Stable +import com.vitorpamplona.quartz.encoders.LnInvoiceUtil +import kotlinx.coroutines.CancellationException +import java.text.NumberFormat + +@Stable +data class InvoiceAmount(val invoice: String, val amount: String?) + +object CachedLnInvoiceParser { + val lnInvoicesCache = LruCache(20) + + fun cached(lnurl: String): InvoiceAmount? { + return lnInvoicesCache[lnurl] + } + + fun parse(lnbcWord: String): InvoiceAmount? { + val myInvoice = LnInvoiceUtil.findInvoice(lnbcWord) + if (myInvoice != null) { + val myInvoiceAmount = + try { + NumberFormat.getInstance().format(LnInvoiceUtil.getAmountInSats(myInvoice)) + } catch (e: Exception) { + if (e is CancellationException) throw e + e.printStackTrace() + null + } + + val lnInvoice = InvoiceAmount(myInvoice, myInvoiceAmount) + + lnInvoicesCache.put(lnbcWord, lnInvoice) + + return lnInvoice + } + + return null + } +} diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/components/InvoicePreview.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/components/InvoicePreview.kt index 497f70f51..e3afdfa93 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/components/InvoicePreview.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/components/InvoicePreview.kt @@ -34,10 +34,9 @@ import androidx.compose.material3.LocalTextStyle import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text import androidx.compose.runtime.Composable -import androidx.compose.runtime.LaunchedEffect -import androidx.compose.runtime.Stable import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.produceState import androidx.compose.runtime.remember import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment @@ -52,43 +51,30 @@ import androidx.compose.ui.text.style.TextDirection import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import com.vitorpamplona.amethyst.R +import com.vitorpamplona.amethyst.service.lnurl.CachedLnInvoiceParser +import com.vitorpamplona.amethyst.service.lnurl.InvoiceAmount import com.vitorpamplona.amethyst.ui.note.ErrorMessageDialog import com.vitorpamplona.amethyst.ui.note.payViaIntent import com.vitorpamplona.amethyst.ui.theme.QuoteBorder import com.vitorpamplona.amethyst.ui.theme.Size20Modifier import com.vitorpamplona.amethyst.ui.theme.subtleBorder -import com.vitorpamplona.quartz.encoders.LnInvoiceUtil -import kotlinx.coroutines.CancellationException import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.launch -import java.text.NumberFormat - -@Stable data class InvoiceAmount(val invoice: String, val amount: String?) +import kotlinx.coroutines.withContext @Composable fun LoadValueFromInvoice( lnbcWord: String, inner: @Composable (invoiceAmount: InvoiceAmount?) -> Unit, ) { - var lnInvoice by remember { mutableStateOf(null) } - - LaunchedEffect(key1 = lnbcWord) { - launch(Dispatchers.IO) { - val myInvoice = LnInvoiceUtil.findInvoice(lnbcWord) - if (myInvoice != null) { - val myInvoiceAmount = - try { - NumberFormat.getInstance().format(LnInvoiceUtil.getAmountInSats(myInvoice)) - } catch (e: Exception) { - if (e is CancellationException) throw e - e.printStackTrace() - null - } - - lnInvoice = InvoiceAmount(myInvoice, myInvoiceAmount) + val lnInvoice by + produceState(initialValue = CachedLnInvoiceParser.cached(lnbcWord), key1 = lnbcWord) { + withContext(Dispatchers.IO) { + val newLnInvoice = CachedLnInvoiceParser.parse(lnbcWord) + if (value != newLnInvoice) { + value = newLnInvoice + } } } - } inner(lnInvoice) } @@ -128,17 +114,24 @@ fun InvoicePreview( Column( modifier = - Modifier.fillMaxWidth() + Modifier + .fillMaxWidth() .padding(start = 20.dp, end = 20.dp, top = 10.dp, bottom = 10.dp) .clip(shape = QuoteBorder) .border(1.dp, MaterialTheme.colorScheme.subtleBorder, QuoteBorder), ) { Column( - modifier = Modifier.fillMaxWidth().padding(20.dp), + modifier = + Modifier + .fillMaxWidth() + .padding(20.dp), ) { Row( verticalAlignment = Alignment.CenterVertically, - modifier = Modifier.fillMaxWidth().padding(bottom = 10.dp), + modifier = + Modifier + .fillMaxWidth() + .padding(bottom = 10.dp), ) { Icon( painter = painterResource(R.drawable.lightning), @@ -162,12 +155,18 @@ fun InvoicePreview( text = "$it ${stringResource(id = R.string.sats)}", fontSize = 25.sp, fontWeight = FontWeight.W500, - modifier = Modifier.fillMaxWidth().padding(vertical = 10.dp), + modifier = + Modifier + .fillMaxWidth() + .padding(vertical = 10.dp), ) } Button( - modifier = Modifier.fillMaxWidth().padding(vertical = 10.dp), + modifier = + Modifier + .fillMaxWidth() + .padding(vertical = 10.dp), onClick = { payViaIntent(lnInvoice, context) { showErrorMessageDialog = it } }, shape = QuoteBorder, colors = From 3495866017cf2ea9025fa9b8a6548b282321bdb9 Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Thu, 29 Feb 2024 18:03:07 -0500 Subject: [PATCH 18/98] Improving cache of cashu tokens --- .../amethyst/service/CashuProcessor.kt | 19 +++++++ .../amethyst/ui/components/CashuRedeem.kt | 57 ++++++++++++------- 2 files changed, 57 insertions(+), 19 deletions(-) diff --git a/app/src/main/java/com/vitorpamplona/amethyst/service/CashuProcessor.kt b/app/src/main/java/com/vitorpamplona/amethyst/service/CashuProcessor.kt index ecd1918f7..ce3b280c6 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/service/CashuProcessor.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/service/CashuProcessor.kt @@ -21,6 +21,7 @@ package com.vitorpamplona.amethyst.service import android.content.Context +import android.util.LruCache import androidx.compose.runtime.Immutable import com.fasterxml.jackson.databind.JsonNode import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper @@ -42,6 +43,24 @@ data class CashuToken( val proofs: JsonNode, ) +object CachedCashuProcessor { + val cashuCache = LruCache>(20) + + fun cached(token: String): GenericLoadable { + return cashuCache[token] ?: GenericLoadable.Loading() + } + + fun parse(token: String): GenericLoadable { + if (cashuCache[token] !is GenericLoadable.Loaded) { + val newCachuData = CashuProcessor().parse(token) + + cashuCache.put(token, newCachuData) + } + + return cashuCache[token] + } +} + class CashuProcessor { fun parse(cashuToken: String): GenericLoadable { checkNotInMainThread() diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/components/CashuRedeem.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/components/CashuRedeem.kt index c77dd6782..f7c1009ba 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/components/CashuRedeem.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/components/CashuRedeem.kt @@ -42,9 +42,9 @@ import androidx.compose.material3.LocalTextStyle import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text import androidx.compose.runtime.Composable -import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.produceState import androidx.compose.runtime.remember import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment @@ -66,7 +66,7 @@ import androidx.lifecycle.viewmodel.compose.viewModel import com.fasterxml.jackson.databind.node.TextNode import com.vitorpamplona.amethyst.R import com.vitorpamplona.amethyst.model.ThemeType -import com.vitorpamplona.amethyst.service.CashuProcessor +import com.vitorpamplona.amethyst.service.CachedCashuProcessor import com.vitorpamplona.amethyst.service.CashuToken import com.vitorpamplona.amethyst.ui.actions.LoadingAnimation import com.vitorpamplona.amethyst.ui.note.CashuIcon @@ -85,25 +85,26 @@ import com.vitorpamplona.amethyst.ui.theme.StdHorzSpacer import com.vitorpamplona.amethyst.ui.theme.subtleBorder import kotlinx.coroutines.CancellationException import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext @Composable fun CashuPreview( cashutoken: String, accountViewModel: AccountViewModel, ) { - var cachuData by remember { - mutableStateOf>(GenericLoadable.Loading()) - } - - LaunchedEffect(key1 = cashutoken) { - launch(Dispatchers.IO) { - val newCachuData = CashuProcessor().parse(cashutoken) - launch(Dispatchers.Main) { cachuData = newCachuData } + val cashuData by produceState( + initialValue = CachedCashuProcessor.cached(cashutoken), + key1 = cashutoken, + ) { + withContext(Dispatchers.IO) { + val newToken = CachedCashuProcessor.parse(cashutoken) + if (value != newToken) { + value = newToken + } } } - Crossfade(targetState = cachuData, label = "CashuPreview(") { + Crossfade(targetState = cashuData, label = "CashuPreview") { when (it) { is GenericLoadable.Loaded -> CashuPreview(it.loaded, accountViewModel) is GenericLoadable.Error -> @@ -160,17 +161,24 @@ fun CashuPreview( Column( modifier = - Modifier.fillMaxWidth() + Modifier + .fillMaxWidth() .padding(start = 20.dp, end = 20.dp, top = 10.dp, bottom = 10.dp) .clip(shape = QuoteBorder) .border(1.dp, MaterialTheme.colorScheme.subtleBorder, QuoteBorder), ) { Column( - modifier = Modifier.fillMaxWidth().padding(20.dp), + modifier = + Modifier + .fillMaxWidth() + .padding(20.dp), ) { Row( verticalAlignment = Alignment.CenterVertically, - modifier = Modifier.fillMaxWidth().padding(bottom = 10.dp), + modifier = + Modifier + .fillMaxWidth() + .padding(bottom = 10.dp), ) { Icon( painter = painterResource(R.drawable.cashu), @@ -193,11 +201,17 @@ fun CashuPreview( text = "${token.totalAmount} ${stringResource(id = R.string.sats)}", fontSize = 25.sp, fontWeight = FontWeight.W500, - modifier = Modifier.fillMaxWidth().padding(vertical = 10.dp), + modifier = + Modifier + .fillMaxWidth() + .padding(vertical = 10.dp), ) Row( - modifier = Modifier.padding(top = 5.dp).fillMaxWidth(), + modifier = + Modifier + .padding(top = 5.dp) + .fillMaxWidth(), ) { var isRedeeming by remember { mutableStateOf(false) } @@ -289,13 +303,17 @@ fun CashuPreviewNew( Card( modifier = - Modifier.fillMaxWidth() + Modifier + .fillMaxWidth() .padding(start = 10.dp, end = 10.dp, top = 10.dp, bottom = 10.dp) .clip(shape = QuoteBorder), ) { Column( horizontalAlignment = Alignment.CenterHorizontally, - modifier = Modifier.fillMaxWidth().padding(10.dp), + modifier = + Modifier + .fillMaxWidth() + .padding(10.dp), ) { Row( verticalAlignment = Alignment.CenterVertically, @@ -317,6 +335,7 @@ fun CashuPreviewNew( Text( text = "${token.totalAmount} ${stringResource(id = R.string.sats)}", fontSize = 20.sp, + modifier = Modifier.padding(top = 5.dp), ) Row(modifier = Modifier.padding(top = 5.dp)) { From 1efef5887a4c4fd1153f6463bc3e58b0b8055c0f Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Thu, 29 Feb 2024 18:50:55 -0500 Subject: [PATCH 19/98] Reduces the cache size for parsed posts. --- .../com/vitorpamplona/amethyst/service/CachedRichTextParser.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/main/java/com/vitorpamplona/amethyst/service/CachedRichTextParser.kt b/app/src/main/java/com/vitorpamplona/amethyst/service/CachedRichTextParser.kt index 25e6c6050..0598ee30c 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/service/CachedRichTextParser.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/service/CachedRichTextParser.kt @@ -26,7 +26,7 @@ import com.vitorpamplona.amethyst.commons.RichTextViewerState import com.vitorpamplona.quartz.events.ImmutableListOfLists object CachedRichTextParser { - val richTextCache = LruCache(200) + val richTextCache = LruCache(50) fun parseText( content: String, From 600aab5e8d7fd24730249aee8190292304322480 Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Thu, 29 Feb 2024 18:51:27 -0500 Subject: [PATCH 20/98] Reverts the ReactionRow change due to misalignments --- .../vitorpamplona/amethyst/ui/components/RichTextViewer.kt | 5 ++--- .../java/com/vitorpamplona/amethyst/ui/note/ReactionsRow.kt | 2 +- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/components/RichTextViewer.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/components/RichTextViewer.kt index db6a35bc8..bd3ffa1ea 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/components/RichTextViewer.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/components/RichTextViewer.kt @@ -28,7 +28,6 @@ import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.ExperimentalLayoutApi import androidx.compose.foundation.layout.FlowRow import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.padding import androidx.compose.foundation.text.InlineTextContent import androidx.compose.foundation.text.appendInlineContent @@ -103,7 +102,6 @@ import com.vitorpamplona.amethyst.ui.screen.loggedIn.LoadedBechLink import com.vitorpamplona.amethyst.ui.theme.Font17SP import com.vitorpamplona.amethyst.ui.theme.HalfVertPadding import com.vitorpamplona.amethyst.ui.theme.MarkdownTextStyle -import com.vitorpamplona.amethyst.ui.theme.StdVertSpacer import com.vitorpamplona.amethyst.ui.theme.innerPostModifier import com.vitorpamplona.amethyst.ui.theme.markdownStyle import com.vitorpamplona.amethyst.ui.uriToRoute @@ -347,6 +345,7 @@ private fun RenderRegular( } } + /* // UrlPreviews and Images have a 5dp spacing down. This also adds the space to Text. val lastElement = state.paragraphs.lastOrNull()?.words?.lastOrNull() if (lastElement !is ImageSegment && @@ -355,7 +354,7 @@ private fun RenderRegular( lastElement !is CashuSegment ) { Spacer(modifier = StdVertSpacer) - } + }*/ } } diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/note/ReactionsRow.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/ReactionsRow.kt index 25de08d53..794a20752 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/note/ReactionsRow.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/ReactionsRow.kt @@ -150,7 +150,7 @@ fun ReactionsRow( ) { val wantsToSeeReactions = remember(baseNote) { mutableStateOf(false) } - // Spacer(modifier = HalfDoubleVertSpacer) + Spacer(modifier = HalfDoubleVertSpacer) InnerReactionRow(baseNote, showReactionDetail, wantsToSeeReactions, accountViewModel, nav) From 4d2a7bbc324db3495d0c765f8a3e19c480f74677 Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Thu, 29 Feb 2024 18:51:39 -0500 Subject: [PATCH 21/98] Fixes Poll Caching problems. --- .../amethyst/ui/note/NoteCompose.kt | 2 +- .../amethyst/ui/note/PollNote.kt | 59 +++++++------- .../amethyst/ui/note/PollNoteViewModel.kt | 78 +++++++++---------- 3 files changed, 71 insertions(+), 68 deletions(-) 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 6f91988e8..32b3b853f 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 @@ -1432,7 +1432,7 @@ fun RenderPoll( nav: (String) -> Unit, ) { val noteEvent = note.event as? PollNoteEvent ?: return - val eventContent = remember(note) { noteEvent.content() } + val eventContent = noteEvent.content() if (makeItShort && accountViewModel.isLoggedUser(note.author)) { Text( diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/note/PollNote.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/PollNote.kt index e40d7ac40..e17404ba2 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/note/PollNote.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/PollNote.kt @@ -68,7 +68,6 @@ import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.unit.IntOffset import androidx.compose.ui.unit.dp import androidx.compose.ui.window.Popup -import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.lifecycle.viewmodel.compose.viewModel import com.vitorpamplona.amethyst.R import com.vitorpamplona.amethyst.model.Note @@ -101,7 +100,7 @@ fun PollNote( accountViewModel: AccountViewModel, nav: (String) -> Unit, ) { - val pollViewModel: PollNoteViewModel = viewModel(key = "PollNoteViewModel") + val pollViewModel: PollNoteViewModel = viewModel(key = "PollNoteViewModel${baseNote.idHex}") pollViewModel.load(accountViewModel.account, baseNote) @@ -126,11 +125,9 @@ fun PollNote( ) { WatchZapsAndUpdateTallies(baseNote, pollViewModel) - val tallies by pollViewModel.tallies.collectAsStateWithLifecycle() - - tallies.forEach { poll_op -> + pollViewModel.tallies.forEach { option -> OptionNote( - poll_op, + option, pollViewModel, baseNote, accountViewModel, @@ -167,9 +164,9 @@ private fun OptionNote( verticalAlignment = Alignment.CenterVertically, modifier = Modifier.padding(vertical = 3.dp), ) { - if (!pollViewModel.canZap()) { + if (!pollViewModel.canZap.value) { val color = - if (poolOption.consensusThreadhold) { + if (poolOption.consensusThreadhold.value) { Color.Green.copy(alpha = 0.32f) } else { MaterialTheme.colorScheme.mediumImportanceLink @@ -181,8 +178,7 @@ private fun OptionNote( pollViewModel = pollViewModel, nonClickablePrepend = { RenderOptionAfterVote( - poolOption.descriptor, - poolOption.tally.toFloat(), + poolOption, color, canPreview, tags, @@ -220,8 +216,7 @@ private fun OptionNote( @Composable private fun RenderOptionAfterVote( - description: String, - totalRatio: Float, + poolOption: PollOption, color: Color, canPreview: Boolean, tags: ImmutableListOfLists, @@ -229,10 +224,9 @@ private fun RenderOptionAfterVote( accountViewModel: AccountViewModel, nav: (String) -> Unit, ) { - val totalPercentage = remember(totalRatio) { "${(totalRatio * 100).roundToInt()}%" } - Box( - Modifier.fillMaxWidth(0.75f) + Modifier + .fillMaxWidth(0.75f) .clip(shape = QuoteBorder) .border( 2.dp, @@ -243,7 +237,9 @@ private fun RenderOptionAfterVote( LinearProgressIndicator( modifier = Modifier.matchParentSize(), color = color, - progress = totalRatio, + progress = { + poolOption.tally.value.toFloat() + }, ) Row( @@ -251,21 +247,31 @@ private fun RenderOptionAfterVote( ) { Column( horizontalAlignment = Alignment.End, - modifier = remember { Modifier.padding(horizontal = 10.dp).width(40.dp) }, + modifier = + remember { + Modifier + .padding(horizontal = 10.dp) + .width(45.dp) + }, ) { Text( - text = totalPercentage, + text = "${(poolOption.tally.value.toFloat() * 100).roundToInt()}%", fontWeight = FontWeight.Bold, ) } Column( - modifier = remember { Modifier.fillMaxWidth().padding(15.dp) }, + modifier = + remember { + Modifier + .fillMaxWidth() + .padding(vertical = 15.dp, horizontal = 10.dp) + }, ) { TranslatableRichTextViewer( - description, + poolOption.descriptor, canPreview, - remember { Modifier }, + Modifier, tags, backgroundColor, accountViewModel, @@ -286,7 +292,8 @@ private fun RenderOptionBeforeVote( nav: (String) -> Unit, ) { Box( - Modifier.fillMaxWidth(0.75f) + Modifier + .fillMaxWidth(0.75f) .clip(shape = QuoteBorder) .border( 2.dp, @@ -358,7 +365,7 @@ fun ZapVote( R.string.poll_unable_to_vote, R.string.poll_author_no_vote, ) - } else if (pollViewModel.isVoteAmountAtomic() && poolOption.zappedByLoggedIn) { + } else if (pollViewModel.isVoteAmountAtomic() && poolOption.zappedByLoggedIn.value) { // only allow one vote per option when min==max, i.e. atomic vote amount specified accountViewModel.toast( R.string.poll_unable_to_vote, @@ -443,7 +450,7 @@ fun ZapVote( clickablePrepend() - if (poolOption.zappedByLoggedIn) { + if (poolOption.zappedByLoggedIn.value) { zappingProgress = 1f Icon( imageVector = Icons.Default.Bolt, @@ -471,8 +478,8 @@ fun ZapVote( } // only show tallies after a user has zapped note - if (!pollViewModel.canZap()) { - val amountStr = remember(poolOption.zappedValue) { showAmount(poolOption.zappedValue) } + if (!pollViewModel.canZap.value) { + val amountStr = remember(poolOption.zappedValue.value) { showAmount(poolOption.zappedValue.value) } Text( text = amountStr, fontSize = Font14SP, diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/note/PollNoteViewModel.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/PollNoteViewModel.kt index 0fb6008fa..bfbfa71c8 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/note/PollNoteViewModel.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/PollNoteViewModel.kt @@ -20,8 +20,9 @@ */ package com.vitorpamplona.amethyst.ui.note -import androidx.compose.runtime.Immutable +import androidx.compose.runtime.MutableState import androidx.compose.runtime.Stable +import androidx.compose.runtime.mutableStateOf import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import com.vitorpamplona.amethyst.model.Account @@ -36,20 +37,18 @@ import com.vitorpamplona.quartz.events.VALUE_MAXIMUM import com.vitorpamplona.quartz.events.VALUE_MINIMUM import com.vitorpamplona.quartz.utils.TimeUtils import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.launch import java.math.BigDecimal import java.math.RoundingMode -@Immutable +@Stable data class PollOption( val option: Int, val descriptor: String, - val zappedValue: BigDecimal, - val tally: BigDecimal, - val consensusThreadhold: Boolean, - val zappedByLoggedIn: Boolean, + var zappedValue: MutableState = mutableStateOf(BigDecimal.ZERO), + var tally: MutableState = mutableStateOf(BigDecimal.ZERO), + var consensusThreadhold: MutableState = mutableStateOf(false), + var zappedByLoggedIn: MutableState = mutableStateOf(false), ) @Stable @@ -70,8 +69,8 @@ class PollNoteViewModel : ViewModel() { private var totalZapped: BigDecimal = BigDecimal.ZERO private var wasZappedByLoggedInAccount: Boolean = false - private val _tallies = MutableStateFlow>(emptyList()) - val tallies = _tallies.asStateFlow() + var canZap = mutableStateOf(false) + var tallies: List = emptyList() fun load( acc: Account, @@ -88,47 +87,44 @@ class PollNoteViewModel : ViewModel() { consensusThreshold = pollEvent?.getTagLong(CONSENSUS_THRESHOLD)?.toFloat()?.div(100)?.toBigDecimal() closedAt = pollEvent?.getTagLong(CLOSED_AT) + + totalZapped = BigDecimal.ZERO + wasZappedByLoggedInAccount = false + + canZap.value = checkIfCanZap() + + tallies = pollOptions?.keys?.map { option -> + PollOption( + option, + pollOptions?.get(option) ?: "", + ) + } ?: emptyList() } fun refreshTallies() { viewModelScope.launch(Dispatchers.Default) { totalZapped = totalZapped() wasZappedByLoggedInAccount = false - account?.calculateIfNoteWasZappedByAccount(pollNote) { wasZappedByLoggedInAccount = true } + account?.calculateIfNoteWasZappedByAccount(pollNote) { + wasZappedByLoggedInAccount = true + canZap.value = checkIfCanZap() + } - val newOptions = - pollOptions?.keys?.map { option -> - val zappedInOption = zappedPollOptionAmount(option) - - val myTally = - if (totalZapped.compareTo(BigDecimal.ZERO) > 0) { - zappedInOption.divide(totalZapped, 2, RoundingMode.HALF_UP) - } else { - BigDecimal.ZERO - } - - val cachedZappedByLoggedIn = - account?.userProfile()?.let { it1 -> cachedIsPollOptionZappedBy(option, it1) } ?: false - - val consensus = consensusThreshold != null && myTally >= consensusThreshold!! - - PollOption( - option, - pollOptions?.get(option) ?: "", - zappedInOption, - myTally, - consensus, - cachedZappedByLoggedIn, - ) - } - - _tallies.emit( - newOptions ?: emptyList(), - ) + tallies.forEach { + it.zappedValue.value = zappedPollOptionAmount(it.option) + it.tally.value = + if (totalZapped.compareTo(BigDecimal.ZERO) > 0) { + it.zappedValue.value.divide(totalZapped, 2, RoundingMode.HALF_UP) + } else { + BigDecimal.ZERO + } + it.consensusThreadhold.value = consensusThreshold != null && it.tally.value >= consensusThreshold!! + it.zappedByLoggedIn.value = account?.userProfile()?.let { it1 -> cachedIsPollOptionZappedBy(it.option, it1) } ?: false + } } } - fun canZap(): Boolean { + fun checkIfCanZap(): Boolean { val account = account ?: return false val note = pollNote ?: return false return account.userProfile() != note.author && !wasZappedByLoggedInAccount From ee4e20bac8ac53c24c64adb658868adda5af1d85 Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Thu, 29 Feb 2024 19:58:18 -0500 Subject: [PATCH 22/98] Fixing alt of git reply --- .../main/java/com/vitorpamplona/quartz/events/GitReplyEvent.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/events/GitReplyEvent.kt b/quartz/src/main/java/com/vitorpamplona/quartz/events/GitReplyEvent.kt index a6b1a5658..505685b16 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/events/GitReplyEvent.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/events/GitReplyEvent.kt @@ -57,7 +57,7 @@ class GitReplyEvent( companion object { const val KIND = 1622 - const val ALT = "A Git Issue" + const val ALT = "A Git Reply" fun create( patch: String, From b6e096ec968b3a857587ac8ff2bd5e61271bd05c Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Thu, 29 Feb 2024 20:00:19 -0500 Subject: [PATCH 23/98] Only display edit option when looking at a text note. --- .../com/vitorpamplona/amethyst/ui/note/UserProfilePicture.kt | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/note/UserProfilePicture.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/UserProfilePicture.kt index 965aa9394..df856f55e 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/note/UserProfilePicture.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/UserProfilePicture.kt @@ -72,6 +72,7 @@ import com.vitorpamplona.amethyst.ui.components.RobohashFallbackAsyncImage import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel import com.vitorpamplona.amethyst.ui.screen.loggedIn.ReportNoteDialog import com.vitorpamplona.quartz.encoders.HexKey +import com.vitorpamplona.quartz.events.TextNoteEvent import kotlinx.collections.immutable.ImmutableSet import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch @@ -567,7 +568,7 @@ fun NoteDropDownMenu( }, ) Divider() - if (state.isLoggedUser) { + if (state.isLoggedUser && note.event is TextNoteEvent) { DropdownMenuItem( text = { Text(stringResource(R.string.edit_post)) }, onClick = { From cf1e5f952daa9816c3f59b720af80ac007aa6117 Mon Sep 17 00:00:00 2001 From: Crowdin Bot Date: Fri, 1 Mar 2024 12:57:52 +0000 Subject: [PATCH 24/98] New Crowdin translations by GitHub Action --- app/src/main/res/values-ar/strings.xml | 14 ++++++++++ app/src/main/res/values-nl/strings.xml | 38 ++++++++++++++++++++++++++ 2 files changed, 52 insertions(+) diff --git a/app/src/main/res/values-ar/strings.xml b/app/src/main/res/values-ar/strings.xml index 6242ab259..0c2a3d4a0 100644 --- a/app/src/main/res/values-ar/strings.xml +++ b/app/src/main/res/values-ar/strings.xml @@ -41,6 +41,8 @@ مشاهدة العد تعزيز معزز + تم تعديلها + الأصلية إقتباس مبلغ جديد في Sats إضافة @@ -143,6 +145,7 @@ "اقبل شروط الاستخدام " شروط الاستخدام يجب قبول شروط الإستخدام + كلمة السر مطلوبة المفتاح مطلوب يجب ادخال اسم تسجيل دخول @@ -193,8 +196,19 @@ ضع علامة على جميع الرسائل/الاشعارات الجديدة كمقروء ضع علامة على جميع الرسائل/الاشعارات كمقروء قم بعمل نسخة احتياطية للمفاتيح + ## النصائح الرئيسية للنسخ الاحتياطي والسلامة + \n\n- يتم تأمين حسابك بمفتاح سري. المفتاح السري هو سلسلة عشوائية طويلة تبدأ بـ **nsec1**. أي شخص لديه مفتاحك السري يمكنه نشر المحتوى باستخدام هويتك. + \n\n- لا تقم بوضع مفتاحك السري في أي موقع أو برنامج لا تثق به. + \n- مطوري Amethyst لن يطلبوا منك **أبدا** المفتاح السري الخاص بك. + \n- حافظ على نسخة احتياطية آمنة لمفتاحك السري لاسترداد الحساب. نوصي باستخدام برنامج لادارة كلمات المرور. + + للحصول على أمان إضافي، يمكنك تشفير مفتاحك الخاص بكلمة مرور. يبدأ هذا المفتاح المشفر بـ **ncryptsec1** ولا يمكن استخدامه دون كلمة المرور الخاصة بك. + \n\nإذا فقدت كلمة المرور، لن تتمكن من استرداد مفتاحك الخاص. + + فشل في تشفير مفتاحك الخاص مفتاحك السري (nsec) قد تم نسخه في الحافظة إنسخ مفتاحي السري + تشفير و نسخ المفتاح الخاص فشلت المصادقة خطأ "تم إنشاؤه بواسطة %1$s" diff --git a/app/src/main/res/values-nl/strings.xml b/app/src/main/res/values-nl/strings.xml index ea7fb69b2..445f7b117 100644 --- a/app/src/main/res/values-nl/strings.xml +++ b/app/src/main/res/values-nl/strings.xml @@ -16,6 +16,7 @@ Spam Imitatie Illegaal gedrag + Overige Onbekend Relay icoon Onbekende auteur @@ -23,6 +24,9 @@ Kopieer auteur ID Kopieer note ID Verzenden + Tijdstempel + Tijdstempel: in behandeling zijnde bevestigingen + OTS: In behandeling Verwijdering aanvragen Blokkeren / Rapporteren @@ -43,7 +47,10 @@ Aantal keer bekeken Boost boosted + gewijzigd + origineel Citaat + Fork Nieuw bedrag in sats Toevoegen "reageert op " @@ -139,12 +146,15 @@ Wissen App logo nsec.. of npub.. + wachtwoord om de sleutel te openen Toon wachtwoord Verberg wachtwoord Ongeldige sleutel + Ongeldige sleutel: %1$s "Ik accepteer de " gebruiksvoorwaarden Accepteren van de voorwaarden is vereist + Wachtwoord is vereist Sleutel is vereist Een naam is verplicht Inloggen @@ -195,8 +205,19 @@ Nieuwe markeren als gelezen Alles markeren als gelezen Back-up sleutels + ## Sleutel back-up en veiligheidstips + \n\nUw account is beveiligd met een privésleutel. De sleutel is een lange, willekeurige reeks die begint met **nsec1**. Iedereen die toegang heeft tot uw privésleutel kan inhoud publiceren met uw identiteit. + \n\n- Voer uw privésleutel **nooit** in een website of software die u niet vertrouwt. + \n- Amethyst ontwikkelaars zullen u **nooit** om uw privésleutel vragen. + \n- **Bewaar** een veilige back-up van uw privésleutel voor accountherstel. Wij raden u aan een wachtwoordmanager te gebruiken. + + Voor meer veiligheid kan je je sleutel versleutelen met een wachtwoord. Deze sleutel begint met **ncryptsec1** en kan niet worden gebruikt zonder uw wachtwoord. + \n\nAls je je wachtwoord verliest, je kan je sleutel niet herstellen. + + Versleutelen van uw privésleutel is mislukt Privésleutel (nsec) gekopieerd naar klembord Kopieer mijn privésleutel + Versleutel en kopieer mijn geheime sleutel Authenticatie mislukt Biometrie kon de eigenaar van deze telefoon niet verifiëren Biometrie kon de eigenaar van deze telefoon niet verifiëren. Fout: %1$s @@ -634,4 +655,21 @@ Toon npub als een QR-code Ongeldig adres Amethist ontving een URI om te openen, maar die uri was ongeldig: %1$s + Zap de devs! + Jouw donatie helpt ons om een verschil te maken. Elke sat telt! + Nu doneren + werd je aangeboden door: + Deze versie werd je aangeboden door: + Versie %1$s + Bedankt! + Max. limiet + Beperkte schrijfrechten + FORK + Git Repository: %1$s + Web: + Kopiëren: + OTS: %1$s + Bewijs van tijdstempel + Er is bewijs dat dit bericht enige tijd voor %1$sis getekend. Het bewijs is op dat tijdstip en op dat moment getypeerd in de Bitcoin-blockchain. + Bericht wijzigen From 3fb8a5938fbdd961ba29e0aaf26854d0375db985 Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Fri, 1 Mar 2024 08:03:58 -0500 Subject: [PATCH 25/98] Updates to Android Studio Iguana --- app/build.gradle | 5 ++--- build.gradle | 4 ++-- gradle/wrapper/gradle-wrapper.properties | 2 +- quartz/build.gradle | 7 ++++--- 4 files changed, 9 insertions(+), 9 deletions(-) diff --git a/app/build.gradle b/app/build.gradle index c91e93ea9..b83458b2f 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -145,14 +145,13 @@ android { // Should match compose version : https://developer.android.com/jetpack/androidx/releases/compose-kotlin kotlinCompilerExtensionVersion "1.5.8" } - packagingOptions { resources { - excludes += '/META-INF/{AL2.0,LGPL2.1}' + excludes += ['/META-INF/{AL2.0,LGPL2.1}', '**/libscrypt.dylib'] } - exclude '**/libscrypt.dylib' } + lint { disable 'MissingTranslation' } diff --git a/build.gradle b/build.gradle index ac635ba5f..1962c551a 100644 --- a/build.gradle +++ b/build.gradle @@ -20,8 +20,8 @@ buildscript { } plugins { - id 'com.android.application' version '8.2.2' apply false - id 'com.android.library' version '8.2.2' apply false + id 'com.android.application' version '8.3.0' apply false + id 'com.android.library' version '8.3.0' apply false id 'org.jetbrains.kotlin.android' version '1.9.22' apply false id 'org.jetbrains.kotlin.jvm' version '1.9.22' apply false id 'androidx.benchmark' version '1.2.3' apply false diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index df554462d..c738a045f 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ #Wed Jan 04 09:23:50 EST 2023 distributionBase=GRADLE_USER_HOME -distributionUrl=https\://services.gradle.org/distributions/gradle-8.2-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-8.4-bin.zip distributionPath=wrapper/dists zipStorePath=wrapper/dists zipStoreBase=GRADLE_USER_HOME diff --git a/quartz/build.gradle b/quartz/build.gradle index 48bc3289f..0b3565103 100644 --- a/quartz/build.gradle +++ b/quartz/build.gradle @@ -33,11 +33,12 @@ android { jvmTarget = '17' freeCompilerArgs += "-Xstring-concat=inline" } - packagingOptions { - // delete native scrypt lib because we cannot use dylibs in android - exclude '**/libscrypt.dylib' + resources { + excludes += ['**/libscrypt.dylib'] + } } + } dependencies { From 87fcba02b3e8952681c110d842b828f0131d8d5a Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Fri, 1 Mar 2024 10:38:03 -0500 Subject: [PATCH 26/98] Move project to version catalogs --- app/build.gradle | 124 +++++++++++++++++++------------------- benchmark/build.gradle | 14 ++--- build.gradle | 32 +++------- commons/build.gradle | 15 ++--- gradle/libs.versions.toml | 124 ++++++++++++++++++++++++++++++++++++++ quartz/build.gradle | 26 ++++---- settings.gradle | 12 +++- 7 files changed, 233 insertions(+), 114 deletions(-) create mode 100644 gradle/libs.versions.toml diff --git a/app/build.gradle b/app/build.gradle index b83458b2f..25f1ed849 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -1,7 +1,7 @@ plugins { - id 'com.android.application' - id 'org.jetbrains.kotlin.android' - id 'com.google.gms.google-services' + alias(libs.plugins.androidApplication) + alias(libs.plugins.jetbrainsKotlinAndroid) + alias(libs.plugins.googleServices) } android { @@ -164,75 +164,78 @@ android { dependencies { implementation project(path: ':quartz') implementation project(path: ':commons') - implementation "androidx.core:core-ktx:$core_ktx_version" - implementation 'androidx.activity:activity-compose:1.8.2' - implementation "androidx.compose.ui:ui:$compose_ui_version" - implementation "androidx.compose.ui:ui-tooling-preview:$compose_ui_version" + implementation libs.androidx.core.ktx + implementation libs.androidx.activity.compose + + implementation platform(libs.androidx.compose.bom) + + implementation libs.androidx.ui + implementation libs.androidx.ui.graphics + implementation libs.androidx.ui.tooling.preview // Needs this to open gallery / image upload - implementation "androidx.fragment:fragment-ktx:$fragment_version" + implementation libs.androidx.fragment.ktx // Navigation - implementation "androidx.navigation:navigation-compose:$nav_version" + implementation libs.androidx.navigation.compose // Observe Live data as State - implementation "androidx.compose.runtime:runtime-livedata:$compose_ui_version" + implementation libs.androidx.runtime.livedata // Material 3 Design - implementation "androidx.compose.material3:material3:${material3_version}" - implementation "androidx.compose.material:material-icons-extended:$compose_ui_version" + implementation libs.androidx.material3 + implementation libs.androidx.material.icons // Adaptive Layout / Two Pane - implementation "androidx.compose.material3:material3-window-size-class:${material3_version}" - implementation 'com.google.accompanist:accompanist-adaptive:0.34.0' - + implementation libs.androidx.material3.windowSize + implementation libs.accompanist.adaptive // Lifecycle - implementation "androidx.lifecycle:lifecycle-runtime-compose:$lifecycle_version" - implementation "androidx.lifecycle:lifecycle-viewmodel-compose:$lifecycle_version" - implementation "androidx.lifecycle:lifecycle-runtime-ktx:$lifecycle_version" - implementation "androidx.lifecycle:lifecycle-livedata-ktx:$lifecycle_version" + implementation libs.androidx.lifecycle.runtime.ktx + implementation libs.androidx.lifecycle.runtime.compose + implementation libs.androidx.lifecycle.viewmodel.compose + implementation libs.androidx.lifecycle.livedata.ktx // Zoomable images - implementation 'net.engawapg.lib:zoomable:1.6.0' + implementation libs.zoomable // Biometrics - implementation "androidx.biometric:biometric-ktx:1.2.0-alpha05" + implementation libs.androidx.biometric.ktx // Websockets API - implementation 'com.squareup.okhttp3:okhttp:5.0.0-alpha.12' + implementation libs.okhttp // HTML Parsing for Link Preview - implementation 'org.jsoup:jsoup:1.17.2' + implementation libs.jsoup // Encrypted Key Storage - implementation 'androidx.security:security-crypto-ktx:1.1.0-alpha06' + implementation libs.androidx.security.crypto.ktx // view videos - implementation "androidx.media3:media3-exoplayer:$media3_version" - implementation "androidx.media3:media3-exoplayer-hls:$media3_version" - implementation "androidx.media3:media3-ui:$media3_version" - implementation "androidx.media3:media3-session:$media3_version" + implementation libs.androidx.media3.exoplayer + implementation libs.androidx.media3.exoplayer.hls + implementation libs.androidx.media3.ui + implementation libs.androidx.media3.session // important for proxy / tor - implementation "androidx.media3:media3-datasource-okhttp:$media3_version" + implementation libs.androidx.media3.datasource.okhttp // Load images from the web. - implementation "io.coil-kt:coil-compose:$coil_version" + implementation libs.coil.compose // view gifs - implementation "io.coil-kt:coil-gif:$coil_version" + implementation libs.coil.gif // view svgs - implementation "io.coil-kt:coil-svg:$coil_version" + implementation libs.coil.svg // create blurhash - implementation group: 'io.trbl', name: 'blurhash', version: '1.0.0' + implementation libs.trbl.blurhash // Permission to upload pictures: - implementation "com.google.accompanist:accompanist-permissions:$accompanist_version" + implementation libs.accompanist.permissions // For QR generation - implementation 'com.google.zxing:core:3.5.3' - implementation 'com.journeyapps:zxing-android-embedded:4.3.0' + implementation libs.zxing + implementation libs.zxing.embedded // Markdown //implementation "com.halilibo.compose-richtext:richtext-ui:0.16.0" @@ -240,51 +243,50 @@ dependencies { //implementation "com.halilibo.compose-richtext:richtext-commonmark:0.16.0" // Markdown (With fix for full-image bleeds) - implementation('com.github.vitorpamplona.compose-richtext:richtext-ui:48702a8ced') - implementation('com.github.vitorpamplona.compose-richtext:richtext-ui-material3:48702a8ced') - implementation('com.github.vitorpamplona.compose-richtext:richtext-commonmark:48702a8ced') + implementation libs.markdown.ui + implementation libs.markdown.ui.material3 + implementation libs.markdown.commonmark // Language picker and Theme chooser - implementation 'androidx.appcompat:appcompat:1.6.1' + implementation libs.androidx.appcompat // Local model for language identification - playImplementation 'com.google.mlkit:language-id:17.0.5' + playImplementation libs.google.mlkit.language.id // Google services model the translate text - playImplementation 'com.google.mlkit:translate:17.0.2' + playImplementation libs.google.mlkit.translate // PushNotifications - playImplementation platform('com.google.firebase:firebase-bom:32.7.2') - playImplementation 'com.google.firebase:firebase-messaging-ktx' + playImplementation platform(libs.firebase.bom) + playImplementation libs.firebase.messaging //PushNotifications(FDroid) - fdroidImplementation 'com.github.UnifiedPush:android-connector:2.2.0' + fdroidImplementation libs.unifiedpush // Charts - implementation "com.patrykandpatrick.vico:core:${vico_version}" - implementation "com.patrykandpatrick.vico:compose:${vico_version}" - implementation "com.patrykandpatrick.vico:views:${vico_version}" - implementation "com.patrykandpatrick.vico:compose-m2:${vico_version}" + implementation libs.vico.charts.core + implementation libs.vico.charts.compose + implementation libs.vico.charts.views + implementation libs.vico.charts.m3 // GeoHash - implementation 'com.github.drfonfon:android-kotlin-geohash:1.0' + implementation libs.drfonfon.geohash // Waveform visualizer - implementation 'com.github.lincollincol:compose-audiowaveform:1.1.1' + implementation libs.audiowaveform // Video compression lib - implementation 'com.github.AbedElazizShe:LightCompressor:1.3.2' + implementation libs.abedElazizShe.image.compressor // Image compression lib - implementation 'id.zelory:compressor:3.0.1' + implementation libs.zelory.video.compressor - testImplementation 'junit:junit:4.13.2' - testImplementation 'io.mockk:mockk:1.13.9' - androidTestImplementation 'androidx.test.ext:junit:1.2.0-alpha03' - androidTestImplementation 'androidx.test.ext:junit-ktx:1.2.0-alpha03' - androidTestImplementation 'androidx.test.espresso:espresso-core:3.5.1' - androidTestImplementation "androidx.compose.ui:ui-test-junit4:$compose_ui_version" - debugImplementation "androidx.compose.ui:ui-tooling:$compose_ui_version" - debugImplementation "androidx.compose.ui:ui-test-manifest:$compose_ui_version" + testImplementation libs.junit + testImplementation libs.mockk + androidTestImplementation libs.androidx.junit + androidTestImplementation libs.androidx.junit.ktx + androidTestImplementation libs.androidx.espresso.core + debugImplementation libs.androidx.ui.tooling + debugImplementation libs.androidx.ui.test.manifest } // https://gitlab.com/fdroid/wiki/-/wikis/HOWTO:-diff-&-fix-APKs-for-Reproducible-Builds#differing-assetsdexoptbaselineprofm-easy-to-fix diff --git a/benchmark/build.gradle b/benchmark/build.gradle index 61ab23cfb..be097e2d7 100644 --- a/benchmark/build.gradle +++ b/benchmark/build.gradle @@ -1,7 +1,7 @@ plugins { - id 'com.android.library' - id 'androidx.benchmark' - id 'org.jetbrains.kotlin.android' + alias(libs.plugins.androidLibrary) + alias(libs.plugins.jetbrainsKotlinAndroid) + alias(libs.plugins.androidBenchmark) } android { @@ -49,10 +49,10 @@ android { } dependencies { - androidTestImplementation 'androidx.test:runner:1.5.2' - androidTestImplementation 'androidx.test.ext:junit:1.1.5' - androidTestImplementation 'junit:junit:4.13.2' - androidTestImplementation 'androidx.benchmark:benchmark-junit4:1.2.3' + androidTestImplementation libs.androidx.runner + androidTestImplementation libs.androidx.junit + androidTestImplementation libs.junit + androidTestImplementation libs.androidx.benchmark.junit4 androidTestImplementation project(path: ':quartz') androidTestImplementation project(path: ':commons') diff --git a/build.gradle b/build.gradle index 1962c551a..121417ec7 100644 --- a/build.gradle +++ b/build.gradle @@ -1,31 +1,13 @@ import org.jetbrains.kotlin.gradle.tasks.KotlinCompile -buildscript { - ext { - fragment_version = "1.6.2" - lifecycle_version = '2.7.0' - compose_ui_version = '1.6.2' - nav_version = '2.7.7' - room_version = "2.4.3" - accompanist_version = '0.34.0' - coil_version = '2.6.0' - vico_version = '1.14.0' - media3_version = '1.2.1' - core_ktx_version = '1.12.0' - material3_version = '1.2.0' - } - dependencies { - classpath 'com.google.gms:google-services:4.4.1' - } -} - plugins { - id 'com.android.application' version '8.3.0' apply false - id 'com.android.library' version '8.3.0' apply false - id 'org.jetbrains.kotlin.android' version '1.9.22' apply false - id 'org.jetbrains.kotlin.jvm' version '1.9.22' apply false - id 'androidx.benchmark' version '1.2.3' apply false - id 'com.diffplug.spotless' version '6.25.0' apply false + alias(libs.plugins.androidApplication) apply false + alias(libs.plugins.jetbrainsKotlinAndroid) apply false + alias(libs.plugins.androidLibrary) apply false + alias(libs.plugins.jetbrainsKotlinJvm) apply false + alias(libs.plugins.androidBenchmark) apply false + alias(libs.plugins.diffplugSpotless) apply false + alias(libs.plugins.googleServices) apply false } subprojects { diff --git a/commons/build.gradle b/commons/build.gradle index a30b159dd..6adfe0abd 100644 --- a/commons/build.gradle +++ b/commons/build.gradle @@ -1,6 +1,6 @@ plugins { - id 'com.android.library' - id 'org.jetbrains.kotlin.android' + alias(libs.plugins.androidLibrary) + alias(libs.plugins.jetbrainsKotlinAndroid) } android { @@ -38,12 +38,13 @@ dependencies { implementation project(path: ':quartz') // Import @Immutable and @Stable - implementation "androidx.compose.ui:ui:$compose_ui_version" + implementation platform(libs.androidx.compose.bom) + implementation libs.androidx.ui // immutable collections to avoid recomposition - api('org.jetbrains.kotlinx:kotlinx-collections-immutable:0.3.7') + api libs.kotlinx.collections.immutable - testImplementation 'junit:junit:4.13.2' - androidTestImplementation 'androidx.test.ext:junit:1.1.5' - androidTestImplementation 'androidx.test.espresso:espresso-core:3.5.1' + testImplementation libs.junit + androidTestImplementation libs.androidx.junit + androidTestImplementation libs.androidx.espresso.core } \ No newline at end of file diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml new file mode 100644 index 000000000..88a75dab3 --- /dev/null +++ b/gradle/libs.versions.toml @@ -0,0 +1,124 @@ +[versions] +accompanistAdaptive = "0.34.0" +activityCompose = "1.8.2" +agp = "8.3.0" +androidKotlinGeohash = "1.0" +androidLifecycle = "2.7.0" +androidxJunit = "1.2.0-alpha03" +appcompat = "1.6.1" +audiowaveform = "1.1.1" +benchmark = "1.2.3" +benchmarkJunit4 = "1.2.3" +biometricKtx = "1.2.0-alpha05" +blurhash = "1.0.0" +coil = "2.6.0" +composeBom = "2024.02.01" +coreKtx = "1.12.0" +espressoCore = "3.5.1" +firebaseBom = "32.7.3" +fragmentKtx = "1.6.2" +gms = "4.4.1" +jacksonModuleKotlin = "2.16.1" +jna = "5.14.0" +jsoup = "1.17.2" +junit = "4.13.2" +kotlin = "1.9.22" +kotlinxCollectionsImmutable = "0.3.7" +languageId = "17.0.5" +lazysodiumAndroid = "5.1.0" +lightcompressor = "1.3.2" +markdown = "48702a8ced" +media3 = "1.2.1" +mockk = "1.13.9" +navigationCompose = "2.7.7" +okhttp = "5.0.0-alpha.12" +runner = "1.5.2" +secp256k1KmpJniAndroid = "0.14.0" +securityCryptoKtx = "1.1.0-alpha06" +spotless = "6.25.0" +translate = "17.0.2" +unifiedpush = "2.2.0" +urlDetector = "0.1.23" +vico-charts = "1.14.0" +zelory = "3.0.1" +zoomable = "1.6.0" +zxing = "3.5.3" +zxingAndroidEmbedded = "4.3.0" + +[libraries] +abedElazizShe-image-compressor = { group = "com.github.AbedElazizShe", name = "LightCompressor", version.ref = "lightcompressor" } +accompanist-adaptive = { group = "com.google.accompanist", name = "accompanist-adaptive", version.ref = "accompanistAdaptive" } +accompanist-permissions = { group = "com.google.accompanist", name = "accompanist-permissions", version.ref = "accompanistAdaptive" } +androidx-activity-compose = { group = "androidx.activity", name = "activity-compose", version.ref = "activityCompose" } +androidx-appcompat = { group = "androidx.appcompat", name = "appcompat", version.ref = "appcompat" } +androidx-benchmark-junit4 = { group = "androidx.benchmark", name = "benchmark-junit4", version.ref = "benchmarkJunit4" } +androidx-biometric-ktx = { group = "androidx.biometric", name = "biometric-ktx", version.ref = "biometricKtx" } +androidx-compose-bom = { group = "androidx.compose", name = "compose-bom", version.ref = "composeBom" } +androidx-core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "coreKtx" } +androidx-espresso-core = { group = "androidx.test.espresso", name = "espresso-core", version.ref = "espressoCore" } +androidx-fragment-ktx = { group = "androidx.fragment", name = "fragment-ktx", version.ref = "fragmentKtx" } +androidx-junit = { group = "androidx.test.ext", name = "junit", version.ref = "androidxJunit" } +androidx-junit-ktx = { group = "androidx.test.ext", name = "junit-ktx", version.ref = "androidxJunit" } +androidx-lifecycle-livedata-ktx = { group = "androidx.lifecycle", name = "lifecycle-livedata-ktx", version.ref = "androidLifecycle" } +androidx-lifecycle-runtime-compose = { group = "androidx.lifecycle", name = "lifecycle-runtime-compose", version.ref = "androidLifecycle" } +androidx-lifecycle-runtime-ktx = { group = "androidx.lifecycle", name = "lifecycle-runtime-ktx", version.ref = "androidLifecycle" } +androidx-lifecycle-viewmodel-compose = { group = "androidx.lifecycle", name = "lifecycle-viewmodel-compose", version.ref = "androidLifecycle" } +androidx-material-icons = { group = "androidx.compose.material", name = "material-icons-extended" } +androidx-material3 = { group = "androidx.compose.material3", name = "material3" } +androidx-material3-windowSize = { group = "androidx.compose.material3", name = "material3-window-size-class" } +androidx-media3-datasource-okhttp = { group = "androidx.media3", name = "media3-datasource-okhttp", version.ref = "media3" } +androidx-media3-exoplayer = { group = "androidx.media3", name = "media3-exoplayer", version.ref = "media3" } +androidx-media3-exoplayer-hls = { group = "androidx.media3", name = "media3-exoplayer-hls", version.ref = "media3" } +androidx-media3-session = { group = "androidx.media3", name = "media3-session", version.ref = "media3" } +androidx-media3-ui = { group = "androidx.media3", name = "media3-ui", version.ref = "media3" } +androidx-navigation-compose = { group = "androidx.navigation", name = "navigation-compose", version.ref = "navigationCompose" } +androidx-runner = { group = "androidx.test", name = "runner", version.ref = "runner" } +androidx-runtime-livedata = { group = "androidx.compose.runtime", name = "runtime-livedata" } +androidx-runtime-runtime = { group = "androidx.compose.runtime", name = "runtime" } +androidx-security-crypto-ktx = { group = "androidx.security", name = "security-crypto-ktx", version.ref = "securityCryptoKtx" } +androidx-ui = { group = "androidx.compose.ui", name = "ui" } +androidx-ui-graphics = { group = "androidx.compose.ui", name = "ui-graphics" } +androidx-ui-test-manifest = { group = "androidx.compose.ui", name = "ui-test-manifest" } +androidx-ui-tooling = { group = "androidx.compose.ui", name = "ui-tooling" } +androidx-ui-tooling-preview = { group = "androidx.compose.ui", name = "ui-tooling-preview" } +audiowaveform = { group = "com.github.lincollincol", name = "compose-audiowaveform", version.ref = "audiowaveform" } +coil-compose = { group = "io.coil-kt", name = "coil-compose", version.ref = "coil" } +coil-gif = { group = "io.coil-kt", name = "coil-gif", version.ref = "coil" } +coil-svg = { group = "io.coil-kt", name = "coil-svg", version.ref = "coil" } +drfonfon-geohash = { group = "com.github.drfonfon", name = "android-kotlin-geohash", version.ref = "androidKotlinGeohash" } +firebase-bom = { group = "com.google.firebase", name = "firebase-bom", version.ref = "firebaseBom" } +firebase-messaging = { group = "com.google.firebase", name = "firebase-messaging-ktx" } +google-mlkit-language-id = { group = "com.google.mlkit", name = "language-id", version.ref = "languageId" } +google-mlkit-translate = { group = "com.google.mlkit", name = "translate", version.ref = "translate" } +jackson-module-kotlin = { group = "com.fasterxml.jackson.module", name = "jackson-module-kotlin", version.ref = "jacksonModuleKotlin" } +jna = { group = "net.java.dev.jna", name = "jna", version.ref = "jna" } +jsoup = { group = "org.jsoup", name = "jsoup", version.ref = "jsoup" } +junit = { group = "junit", name = "junit", version.ref = "junit" } +kotlinx-collections-immutable = { group = "org.jetbrains.kotlinx", name = "kotlinx-collections-immutable", version.ref = "kotlinxCollectionsImmutable" } +lazysodium-android = { group = "com.goterl", name = "lazysodium-android", version.ref = "lazysodiumAndroid" } +markdown-commonmark = { group = "com.github.vitorpamplona.compose-richtext", name = "richtext-commonmark", version.ref = "markdown" } +markdown-ui = { group = "com.github.vitorpamplona.compose-richtext", name = "richtext-ui", version.ref = "markdown" } +markdown-ui-material3 = { group = "com.github.vitorpamplona.compose-richtext", name = "richtext-ui-material3", version.ref = "markdown" } +mockk = { group = "io.mockk", name = "mockk", version.ref = "mockk" } +okhttp = { group = "com.squareup.okhttp3", name = "okhttp", version.ref = "okhttp" } +secp256k1-kmp-jni-android = { group = "fr.acinq.secp256k1", name = "secp256k1-kmp-jni-android", version.ref = "secp256k1KmpJniAndroid" } +trbl-blurhash = { group = "io.trbl", name = "blurhash", version.ref = "blurhash" } +unifiedpush = { group = "com.github.UnifiedPush", name = "android-connector", version.ref = "unifiedpush" } +url-detector = { group = "io.github.url-detector", name = "url-detector", version.ref = "urlDetector" } +vico-charts-compose = { group = "com.patrykandpatrick.vico", name = "compose", version.ref = "vico-charts" } +vico-charts-core = { group = "com.patrykandpatrick.vico", name = "core", version.ref = "vico-charts" } +vico-charts-m3 = { group = "com.patrykandpatrick.vico", name = "compose-m3", version.ref = "vico-charts" } +vico-charts-views = { group = "com.patrykandpatrick.vico", name = "views", version.ref = "vico-charts" } +zelory-video-compressor = { group = "id.zelory", name = "compressor", version.ref = "zelory" } +zoomable = { group = "net.engawapg.lib", name = "zoomable", version.ref = "zoomable" } +zxing = { group = "com.google.zxing", name = "core", version.ref = "zxing" } +zxing-embedded = { group = "com.journeyapps", name = "zxing-android-embedded", version.ref = "zxingAndroidEmbedded" } + +[plugins] +androidApplication = { id = "com.android.application", version.ref = "agp" } +androidBenchmark = { id = "androidx.benchmark", version.ref = "benchmark" } +androidLibrary = { id = "com.android.library", version.ref = "agp" } +diffplugSpotless = { id = "com.diffplug.spotless", version.ref = "spotless" } +googleServices = { id = "com.google.gms.google-services", version.ref = "gms" } +jetbrainsKotlinAndroid = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" } +jetbrainsKotlinJvm = { id = "org.jetbrains.kotlin.jvm", version.ref = "kotlin" } diff --git a/quartz/build.gradle b/quartz/build.gradle index 0b3565103..a9aa482f5 100644 --- a/quartz/build.gradle +++ b/quartz/build.gradle @@ -1,6 +1,6 @@ plugins { - id 'com.android.library' - id 'org.jetbrains.kotlin.android' + alias(libs.plugins.androidLibrary) + alias(libs.plugins.jetbrainsKotlinAndroid) } android { @@ -38,32 +38,34 @@ android { excludes += ['**/libscrypt.dylib'] } } - } dependencies { - implementation "androidx.core:core-ktx:$core_ktx_version" + implementation libs.androidx.core.ktx + + implementation platform(libs.androidx.compose.bom) // @Immutable and @Stable - implementation "androidx.compose.runtime:runtime:$compose_ui_version" + implementation libs.androidx.runtime.runtime // Bitcoin secp256k1 bindings to Android - api 'fr.acinq.secp256k1:secp256k1-kmp-jni-android:0.14.0' + api libs.secp256k1.kmp.jni.android // LibSodium for ChaCha encryption (NIP-44) + // Wait for @aar support in version catalogs implementation "com.goterl:lazysodium-android:5.1.0@aar" implementation 'net.java.dev.jna:jna:5.14.0@aar' // Performant Parser of JSONs into Events - api 'com.fasterxml.jackson.module:jackson-module-kotlin:2.16.1' + api libs.jackson.module.kotlin // immutable collections to avoid recomposition - api('org.jetbrains.kotlinx:kotlinx-collections-immutable:0.3.7') + api libs.kotlinx.collections.immutable // Parses URLs from Text: - api "io.github.url-detector:url-detector:0.1.23" + api libs.url.detector - testImplementation 'junit:junit:4.13.2' - androidTestImplementation 'androidx.test.ext:junit:1.1.5' - androidTestImplementation 'androidx.test.espresso:espresso-core:3.5.1' + testImplementation libs.junit + androidTestImplementation libs.androidx.junit + androidTestImplementation libs.androidx.espresso.core } \ No newline at end of file diff --git a/settings.gradle b/settings.gradle index 6ac181c95..45a4461c7 100644 --- a/settings.gradle +++ b/settings.gradle @@ -1,8 +1,14 @@ pluginManagement { repositories { - gradlePluginPortal() - google() + google { + content { + includeGroupByRegex("com\\.android.*") + includeGroupByRegex("com\\.google.*") + includeGroupByRegex("androidx.*") + } + } mavenCentral() + gradlePluginPortal() maven { url "https://jitpack.io" content { @@ -11,6 +17,7 @@ pluginManagement { } } } + dependencyResolutionManagement { repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS) repositories { @@ -19,6 +26,7 @@ dependencyResolutionManagement { maven { url "https://jitpack.io" } } } + rootProject.name = "Amethyst" include ':app' include ':benchmark' From 0653a69bbd9a1566a8ad3a281ec925771822772f Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Fri, 1 Mar 2024 10:39:32 -0500 Subject: [PATCH 27/98] Replaces deprecated put method --- .../amethyst/service/relays/JsonFilter.kt | 8 ++++---- .../amethyst/service/relays/Subscription.kt | 2 +- .../amethyst/service/relays/TypedFilter.kt | 12 ++++++------ .../java/com/vitorpamplona/quartz/events/Event.kt | 2 +- 4 files changed, 12 insertions(+), 12 deletions(-) diff --git a/app/src/main/java/com/vitorpamplona/amethyst/service/relays/JsonFilter.kt b/app/src/main/java/com/vitorpamplona/amethyst/service/relays/JsonFilter.kt index dc7d66d9b..eac7b0f8f 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/service/relays/JsonFilter.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/service/relays/JsonFilter.kt @@ -37,26 +37,26 @@ class JsonFilter( val filter = factory.objectNode().apply { ids?.run { - put( + replace( "ids", factory.arrayNode(ids.size).apply { ids.forEach { add(it) } }, ) } authors?.run { - put( + replace( "authors", factory.arrayNode(authors.size).apply { authors.forEach { add(it) } }, ) } kinds?.run { - put( + replace( "kinds", factory.arrayNode(kinds.size).apply { kinds.forEach { add(it) } }, ) } tags?.run { entries.forEach { kv -> - put( + replace( "#${kv.key}", factory.arrayNode(kv.value.size).apply { kv.value.forEach { add(it) } }, ) diff --git a/app/src/main/java/com/vitorpamplona/amethyst/service/relays/Subscription.kt b/app/src/main/java/com/vitorpamplona/amethyst/service/relays/Subscription.kt index eefe41b99..9de8de758 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/service/relays/Subscription.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/service/relays/Subscription.kt @@ -47,7 +47,7 @@ data class Subscription( return factory.objectNode().apply { put("id", id) typedFilters?.also { filters -> - put( + replace( "typedFilters", factory.arrayNode(filters.size).apply { filters.forEach { filter -> add(filter.toJsonObject()) } diff --git a/app/src/main/java/com/vitorpamplona/amethyst/service/relays/TypedFilter.kt b/app/src/main/java/com/vitorpamplona/amethyst/service/relays/TypedFilter.kt index 84ef11d26..ec3c9e05c 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/service/relays/TypedFilter.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/service/relays/TypedFilter.kt @@ -36,8 +36,8 @@ class TypedFilter( val factory = Event.mapper.nodeFactory return factory.objectNode().apply { - put("types", typesToJson(types)) - put("filter", filterToJson(filter)) + replace("types", typesToJson(types)) + replace("filter", filterToJson(filter)) } } @@ -50,26 +50,26 @@ class TypedFilter( val factory = Event.mapper.nodeFactory return factory.objectNode().apply { filter.ids?.run { - put( + replace( "ids", factory.arrayNode(filter.ids.size).apply { filter.ids.forEach { add(it) } }, ) } filter.authors?.run { - put( + replace( "authors", factory.arrayNode(filter.authors.size).apply { filter.authors.forEach { add(it) } }, ) } filter.kinds?.run { - put( + replace( "kinds", factory.arrayNode(filter.kinds.size).apply { filter.kinds.forEach { add(it) } }, ) } filter.tags?.run { entries.forEach { kv -> - put( + replace( "#${kv.key}", factory.arrayNode(kv.value.size).apply { kv.value.forEach { add(it) } }, ) diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/events/Event.kt b/quartz/src/main/java/com/vitorpamplona/quartz/events/Event.kt index aba51ab92..1d8549679 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/events/Event.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/events/Event.kt @@ -386,7 +386,7 @@ open class Event( put("pubkey", pubKey) put("created_at", createdAt) put("kind", kind) - put( + replace( "tags", factory.arrayNode(tags.size).apply { tags.forEach { tag -> From 238e577b93b45ba91fec2f9d80680d7fe347be67 Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Fri, 1 Mar 2024 10:39:49 -0500 Subject: [PATCH 28/98] avoids changing and reading the state at the same time --- .../amethyst/ui/note/PollNoteViewModel.kt | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/note/PollNoteViewModel.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/PollNoteViewModel.kt index bfbfa71c8..0c03ebd7a 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/note/PollNoteViewModel.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/PollNoteViewModel.kt @@ -111,14 +111,17 @@ class PollNoteViewModel : ViewModel() { } tallies.forEach { - it.zappedValue.value = zappedPollOptionAmount(it.option) - it.tally.value = - if (totalZapped.compareTo(BigDecimal.ZERO) > 0) { - it.zappedValue.value.divide(totalZapped, 2, RoundingMode.HALF_UP) + val zappedValue = zappedPollOptionAmount(it.option) + val tallyValue = + if (totalZapped > BigDecimal.ZERO) { + zappedValue.divide(totalZapped, 2, RoundingMode.HALF_UP) } else { BigDecimal.ZERO } - it.consensusThreadhold.value = consensusThreshold != null && it.tally.value >= consensusThreshold!! + + it.zappedValue.value = zappedValue + it.tally.value = tallyValue + it.consensusThreadhold.value = consensusThreshold != null && tallyValue >= consensusThreshold!! it.zappedByLoggedIn.value = account?.userProfile()?.let { it1 -> cachedIsPollOptionZappedBy(it.option, it1) } ?: false } } From 7468352795eb9ac59ee042360f3d9b94479b43e7 Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Fri, 1 Mar 2024 10:40:12 -0500 Subject: [PATCH 29/98] Removes warnings from the quartz library. --- .../vitorpamplona/quartz/encoders/Lud06.kt | 2 +- .../quartz/encoders/Nip19Bech32.kt | 4 ++- .../quartz/events/BaseTextNoteEvent.kt | 32 +++++++++++-------- .../quartz/events/EventInterface.kt | 2 +- .../quartz/events/GitPatchEvent.kt | 2 +- 5 files changed, 24 insertions(+), 18 deletions(-) diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/encoders/Lud06.kt b/quartz/src/main/java/com/vitorpamplona/quartz/encoders/Lud06.kt index d4d2bb178..775a7c85f 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/encoders/Lud06.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/encoders/Lud06.kt @@ -30,7 +30,7 @@ class Lud06 { fun toLud16(str: String): String? { return try { - val url = toLnUrlp(str) + val url = toLnUrlp(str) ?: return null val matcher = LNURLP_PATTERN.matcher(url) if (matcher.find()) { diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/encoders/Nip19Bech32.kt b/quartz/src/main/java/com/vitorpamplona/quartz/encoders/Nip19Bech32.kt index de2e719b3..31267b345 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/encoders/Nip19Bech32.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/encoders/Nip19Bech32.kt @@ -94,7 +94,9 @@ object Nip19Bech32 { val key = matcher.group(3) // bech32 val additionalChars = matcher.group(4) // additional chars - return parseComponents(type!!, key, additionalChars) + if (type == null) return null + + return parseComponents(type, key, additionalChars) } catch (e: Throwable) { Log.e("NIP19 Parser", "Issue trying to Decode NIP19 $uri: ${e.message}", e) } diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/events/BaseTextNoteEvent.kt b/quartz/src/main/java/com/vitorpamplona/quartz/events/BaseTextNoteEvent.kt index 26a522e49..a6d202298 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/events/BaseTextNoteEvent.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/events/BaseTextNoteEvent.kt @@ -105,14 +105,16 @@ open class BaseTextNoteEvent( val additionalChars = matcher2.group(4) // additional chars try { - val parsed = Nip19Bech32.parseComponents(type, key, additionalChars)?.entity + if (type != null) { + val parsed = Nip19Bech32.parseComponents(type, key, additionalChars)?.entity - if (parsed != null) { - if (parsed is Nip19Bech32.NProfile) { - returningList.add(parsed.hex) - } - if (parsed is Nip19Bech32.NPub) { - returningList.add(parsed.hex) + if (parsed != null) { + if (parsed is Nip19Bech32.NProfile) { + returningList.add(parsed.hex) + } + if (parsed is Nip19Bech32.NPub) { + returningList.add(parsed.hex) + } } } } catch (e: Exception) { @@ -151,14 +153,16 @@ open class BaseTextNoteEvent( val key = matcher2.group(3) // bech32 val additionalChars = matcher2.group(4) // additional chars - val parsed = Nip19Bech32.parseComponents(type, key, additionalChars)?.entity + if (type != null) { + val parsed = Nip19Bech32.parseComponents(type, key, additionalChars)?.entity - if (parsed != null) { - when (parsed) { - is Nip19Bech32.NEvent -> citations.add(parsed.hex) - is Nip19Bech32.NAddress -> citations.add(parsed.atag) - is Nip19Bech32.Note -> citations.add(parsed.hex) - is Nip19Bech32.NEmbed -> citations.add(parsed.event.id) + if (parsed != null) { + when (parsed) { + is Nip19Bech32.NEvent -> citations.add(parsed.hex) + is Nip19Bech32.NAddress -> citations.add(parsed.atag) + is Nip19Bech32.Note -> citations.add(parsed.hex) + is Nip19Bech32.NEmbed -> citations.add(parsed.event.id) + } } } } diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/events/EventInterface.kt b/quartz/src/main/java/com/vitorpamplona/quartz/events/EventInterface.kt index 55f0b4b2b..ae2d56396 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/events/EventInterface.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/events/EventInterface.kt @@ -63,7 +63,7 @@ interface EventInterface { fun isTaggedUser(idHex: String): Boolean - fun isTaggedUsers(idHex: Set): Boolean + fun isTaggedUsers(idHexes: Set): Boolean fun isTaggedEvent(idHex: String): Boolean diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/events/GitPatchEvent.kt b/quartz/src/main/java/com/vitorpamplona/quartz/events/GitPatchEvent.kt index d9a6670e1..4c5a04ce8 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/events/GitPatchEvent.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/events/GitPatchEvent.kt @@ -60,7 +60,7 @@ class GitPatchEvent( fun commitPGPSig() = tags.firstOrNull { it.size > 1 && it[0] == "commit-pgp-sig" }?.get(1) fun committer() = - tags.filter { it.size > 1 && it[0] == "committer" }?.mapNotNull { + tags.filter { it.size > 1 && it[0] == "committer" }.mapNotNull { Committer(it.getOrNull(1), it.getOrNull(2), it.getOrNull(3), it.getOrNull(4)) } From 1cc4fd51835ddf62a837e9b531ca6cbd3bd47620 Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Fri, 1 Mar 2024 10:40:43 -0500 Subject: [PATCH 30/98] Closes popup when finished editing. --- .../com/vitorpamplona/amethyst/ui/note/UserProfilePicture.kt | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/note/UserProfilePicture.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/UserProfilePicture.kt index df856f55e..747b6c2e9 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/note/UserProfilePicture.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/UserProfilePicture.kt @@ -483,7 +483,10 @@ fun NoteDropDownMenu( if (wantsToEditPost.value) { EditPostView( - onClose = { wantsToEditPost.value = false }, + onClose = { + popupExpanded.value = false + wantsToEditPost.value = false + }, edit = note, accountViewModel = accountViewModel, nav = nav, From 8fa6d1d4901271c5a5698a2a2c211608799f7fad Mon Sep 17 00:00:00 2001 From: David Kaspar Date: Fri, 1 Mar 2024 17:12:06 +0000 Subject: [PATCH 31/98] Updated translations. DE, SV, PT, CS --- app/src/main/res/values-cs/strings.xml | 10 ++++++++++ app/src/main/res/values-de/strings.xml | 10 ++++++++++ app/src/main/res/values-pt-rBR/strings.xml | 10 ++++++++++ app/src/main/res/values-sv-rSE/strings.xml | 10 ++++++++++ 4 files changed, 40 insertions(+) diff --git a/app/src/main/res/values-cs/strings.xml b/app/src/main/res/values-cs/strings.xml index 5cd064b5e..aa267eb06 100644 --- a/app/src/main/res/values-cs/strings.xml +++ b/app/src/main/res/values-cs/strings.xml @@ -661,4 +661,14 @@ Git repositář: %1$s Internet: Klon: + Časové razítko + Časové razítko: Čeká na potvrzení + OTS: Čeká + upraveno + originál + OTS: %1$s + Důkaz časového razítka + Existuje důkaz, že tento příspěvek byl podepsán někdy před %1$s. Důkaz byl označen v Bitcoin blockchainu v tomto datu a čase. + Upravit příspěvek + Jiný diff --git a/app/src/main/res/values-de/strings.xml b/app/src/main/res/values-de/strings.xml index 3f5c2d99d..3e9cbcc58 100644 --- a/app/src/main/res/values-de/strings.xml +++ b/app/src/main/res/values-de/strings.xml @@ -666,4 +666,14 @@ anz der Bedingungen ist erforderlich Git Repository: %1$s Internet: Klonen: + Zeitstempel es + Zeitstempel: Ausstehende Bestätigungen + OTS: Ausstehend + bearbeitet + Original + OTS: %1$s + Zeitstempel Beweis + Es gibt einen Beweis, dass dieser Beitrag irgendwann vor %1$s signiert wurde. Der Beweis wurde zu diesem Datum und Uhrzeit in der Bitcoin-Blockchain gestempelt. + Beitrag bearbeiten + Andere diff --git a/app/src/main/res/values-pt-rBR/strings.xml b/app/src/main/res/values-pt-rBR/strings.xml index d62aaa9e2..2125caf75 100644 --- a/app/src/main/res/values-pt-rBR/strings.xml +++ b/app/src/main/res/values-pt-rBR/strings.xml @@ -661,4 +661,14 @@ Repositório Git: %1$s Site: Clonar: + Carimbá-lo com data/hora + Carimbo de data/hora: Confirmações pendentes + OTS: Pendente + editado + original + OTS: %1$s + Prova de Carimbo de data/hora + Há prova de que esta postagem foi assinada antes de %1$s. A prova foi carimbada no blockchain do Bitcoin naquela data e hora. + Editar postagem + Outro diff --git a/app/src/main/res/values-sv-rSE/strings.xml b/app/src/main/res/values-sv-rSE/strings.xml index e099d4ffb..341c4a1fc 100644 --- a/app/src/main/res/values-sv-rSE/strings.xml +++ b/app/src/main/res/values-sv-rSE/strings.xml @@ -660,4 +660,14 @@ Git Repository: %1$s Webbplats: Klona: + Tidsstämpla det + Tidsstämpel: Väntar på bekräftelser + OTS: Väntar + redigerat + original + OTS: %1$s + Tidsstämpel Bevis + Det finns bevis på att detta inlägg signerades någon gång före %1$s. Beviset stämplades i Bitcoin-blockchainen vid det datumet och den tiden. + Redigera inlägg + Annat From 21a18cfa38f0f7d60d8961c23ca94ae82d82d89d Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Fri, 1 Mar 2024 12:15:48 -0500 Subject: [PATCH 32/98] Rotates all versions of a note --- .../amethyst/model/LocalCache.kt | 32 +++- .../amethyst/ui/note/NoteCompose.kt | 150 +++++++++++++++--- .../amethyst/ui/screen/ThreadFeedView.kt | 24 ++- .../ui/screen/loggedIn/AccountViewModel.kt | 2 + app/src/main/res/values/strings.xml | 1 + .../events/TextNoteModificationEvent.kt | 2 + 6 files changed, 167 insertions(+), 44 deletions(-) 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 5e4d860e5..fc50dc25e 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/model/LocalCache.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/model/LocalCache.kt @@ -21,6 +21,7 @@ package com.vitorpamplona.amethyst.model import android.util.Log +import android.util.LruCache import androidx.compose.runtime.Stable import com.vitorpamplona.amethyst.Amethyst import com.vitorpamplona.amethyst.service.checkNotInMainThread @@ -1402,6 +1403,13 @@ object LocalCache { note.loadEvent(event, author, emptyList()) + event.editedNote()?.let { + getNoteIfExists(it)?.let { editedNote -> + modificationCache.remove(editedNote.idHex) + editedNote.liveSet?.innerModifications?.invalidateData() + } + } + refreshObservers(note) } @@ -1716,15 +1724,31 @@ object LocalCache { return minTime } + val modificationCache = LruCache>(20) + + fun cachedModificationEventsForNote(note: Note): List? { + return modificationCache[note.idHex] + } + suspend fun findLatestModificationForNote(note: Note): List { checkNotInMainThread() + + modificationCache[note.idHex]?.let { + return it + } + val time = TimeUtils.now() - return noteListCache.filter { item -> - val noteEvent = item.event + val newNotes = + noteListCache.filter { item -> + val noteEvent = item.event - noteEvent is TextNoteModificationEvent && noteEvent.isTaggedEvent(note.idHex) && !noteEvent.isExpirationBefore(time) - } + noteEvent is TextNoteModificationEvent && noteEvent.isTaggedEvent(note.idHex) && !noteEvent.isExpirationBefore(time) + }.sortedWith(compareBy({ it.createdAt() }, { it.idHex })) + + modificationCache.put(note.idHex, newNotes) + + return newNotes } fun cleanObservers() { 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 32b3b853f..fc9a92a09 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 @@ -61,6 +61,7 @@ import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.MutableState import androidx.compose.runtime.Stable +import androidx.compose.runtime.State import androidx.compose.runtime.derivedStateOf import androidx.compose.runtime.getValue import androidx.compose.runtime.livedata.observeAsState @@ -1088,10 +1089,58 @@ fun InnerNoteWithReactions( } @Stable -class EditState( - val showOriginal: MutableState = mutableStateOf(false), - val modificationsInOrder: MutableState> = mutableStateOf(emptyList()), -) +class EditState() { + private var modificationsList: List = persistentListOf() + private var modificationToShowIndex: Int = -1 + + val modificationToShow: MutableState = mutableStateOf(null) + val showingVersion: MutableState = mutableStateOf(0) + + fun hasModificationsToShow(): Boolean = modificationsList.isNotEmpty() + + fun isOriginal(): Boolean = modificationToShowIndex < 0 + + fun isLatest(): Boolean = modificationToShowIndex == modificationsList.lastIndex + + fun originalVersionId() = 0 + + fun lastVersionId() = modificationsList.size + + fun versionId() = modificationToShowIndex + 1 + + fun nextModification() { + if (modificationToShowIndex < 0) { + modificationToShowIndex = 0 + modificationToShow.value = modificationsList.getOrNull(0) + } else { + modificationToShowIndex++ + if (modificationToShowIndex >= modificationsList.size) { + modificationToShowIndex = -1 + modificationToShow.value = null + } else { + modificationToShow.value = modificationsList.getOrNull(modificationToShowIndex) + } + } + + showingVersion.value = versionId() + } + + fun updateModifications(newModifications: List) { + if (modificationsList != newModifications) { + modificationsList = newModifications + + if (newModifications.isEmpty()) { + modificationToShow.value = null + modificationToShowIndex = -1 + } else { + modificationToShowIndex = newModifications.lastIndex + modificationToShow.value = newModifications.last() + } + } + + showingVersion.value = versionId() + } +} @Composable private fun NoteBody( @@ -1105,14 +1154,7 @@ private fun NoteBody( accountViewModel: AccountViewModel, nav: (String) -> Unit, ) { - val editState by - produceState(initialValue = EditState(), key1 = baseNote) { - accountViewModel.findModificationEventsForNote(baseNote) { newModifications -> - if (value.modificationsInOrder.value != newModifications) { - value.modificationsInOrder.value = newModifications - } - } - } + val editState = observeEdits(baseNote = baseNote, accountViewModel = accountViewModel) FirstUserInfoRow( baseNote = baseNote, @@ -1168,7 +1210,7 @@ private fun RenderNoteRow( backgroundColor: MutableState, makeItShort: Boolean, canPreview: Boolean, - editState: EditState, + editState: State>, accountViewModel: AccountViewModel, nav: (String) -> Unit, ) { @@ -1358,7 +1400,7 @@ fun RenderTextEvent( makeItShort: Boolean, canPreview: Boolean, backgroundColor: MutableState, - editState: EditState, + editState: State>, accountViewModel: AccountViewModel, nav: (String) -> Unit, ) { @@ -1371,10 +1413,10 @@ fun RenderTextEvent( derivedStateOf { val subject = (note.event as? TextNoteEvent)?.subject()?.ifEmpty { null } val newBody = - if (editState.showOriginal.value || editState.modificationsInOrder.value.isEmpty()) { - body + if (editState.value is GenericLoadable.Loaded) { + (editState.value as? GenericLoadable.Loaded)?.loaded?.modificationToShow?.value?.event?.content() ?: body } else { - editState.modificationsInOrder.value.firstOrNull()?.event?.content() ?: body + body } if (!subject.isNullOrBlank() && !newBody.split("\n")[0].contains(subject)) { @@ -2822,7 +2864,7 @@ fun DisplayLocation( fun FirstUserInfoRow( baseNote: Note, showAuthorPicture: Boolean, - editState: EditState, + editState: State>, accountViewModel: AccountViewModel, nav: (String) -> Unit, ) { @@ -2857,8 +2899,10 @@ fun FirstUserInfoRow( DisplayFollowingHashtagsInPost(baseNote, accountViewModel, nav) } - if (!editState.modificationsInOrder.value.isEmpty()) { - DisplayEditStatus(editState.showOriginal) + if (editState.value is GenericLoadable.Loaded) { + (editState.value as? GenericLoadable.Loaded)?.loaded?.let { + DisplayEditStatus(it) + } } TimeAgo(baseNote) @@ -2868,15 +2912,69 @@ fun FirstUserInfoRow( } @Composable -fun DisplayEditStatus(showOriginal: MutableState) { +fun observeEdits( + baseNote: Note, + accountViewModel: AccountViewModel, +): State> { + val editState = + remember(baseNote.idHex) { + val cached = accountViewModel.cachedModificationEventsForNote(baseNote) + mutableStateOf( + if (cached != null) { + if (cached.isEmpty()) { + GenericLoadable.Empty() + } else { + val state = EditState() + state.updateModifications(cached) + GenericLoadable.Loaded(state) + } + } else { + GenericLoadable.Loading() + }, + ) + } + + val updatedNote = baseNote.live().innerModifications.observeAsState() + + LaunchedEffect(key1 = updatedNote) { + updatedNote.value?.note?.let { + accountViewModel.findModificationEventsForNote(it) { newModifications -> + if (newModifications.isEmpty()) { + if (editState.value !is GenericLoadable.Empty) { + editState.value = GenericLoadable.Empty() + } + } else { + if (editState.value is GenericLoadable.Loaded) { + (editState.value as? GenericLoadable.Loaded)?.loaded?.updateModifications(newModifications) + } else { + val state = EditState() + state.updateModifications(newModifications) + editState.value = GenericLoadable.Loaded(state) + } + } + } + } + } + + return editState +} + +@Composable +fun DisplayEditStatus(editState: EditState) { ClickableText( text = - if (showOriginal.value) { - buildAnnotatedString { append(stringResource(id = R.string.original)) } - } else { - buildAnnotatedString { append(stringResource(id = R.string.edited)) } + buildAnnotatedString { + if (editState.showingVersion.value == editState.originalVersionId()) { + append(stringResource(id = R.string.original)) + } else if (editState.showingVersion.value == editState.lastVersionId()) { + append(stringResource(id = R.string.edited)) + } else { + append(stringResource(id = R.string.edited_number, editState.versionId())) + } }, - onClick = { showOriginal.value = !showOriginal.value }, + onClick = { + editState.nextModification() + }, style = LocalTextStyle.current.copy( color = MaterialTheme.colorScheme.placeholderText, diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/ThreadFeedView.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/ThreadFeedView.kt index f65619417..c54c31228 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/ThreadFeedView.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/ThreadFeedView.kt @@ -58,7 +58,6 @@ import androidx.compose.runtime.derivedStateOf import androidx.compose.runtime.getValue import androidx.compose.runtime.livedata.observeAsState import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.produceState import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.setValue @@ -86,6 +85,7 @@ import coil.compose.AsyncImage import com.vitorpamplona.amethyst.R import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.amethyst.model.Note +import com.vitorpamplona.amethyst.ui.components.GenericLoadable import com.vitorpamplona.amethyst.ui.components.InlineCarrousel import com.vitorpamplona.amethyst.ui.components.LoadNote import com.vitorpamplona.amethyst.ui.components.ObserveDisplayNip05Status @@ -132,6 +132,7 @@ import com.vitorpamplona.amethyst.ui.note.RenderPostApproval import com.vitorpamplona.amethyst.ui.note.RenderRepost import com.vitorpamplona.amethyst.ui.note.RenderTextEvent import com.vitorpamplona.amethyst.ui.note.VideoDisplay +import com.vitorpamplona.amethyst.ui.note.observeEdits import com.vitorpamplona.amethyst.ui.note.showAmount import com.vitorpamplona.amethyst.ui.note.timeAgo import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel @@ -377,20 +378,13 @@ fun NoteMaster( onClick = { showHiddenNote = true }, ) } else { - val editState by - produceState(initialValue = EditState(), key1 = baseNote) { - accountViewModel.findModificationEventsForNote(baseNote) { newModifications -> - if (value.modificationsInOrder.value != newModifications) { - value.modificationsInOrder.value = newModifications - } - } - } - Column( modifier .fillMaxWidth() .padding(top = 10.dp), ) { + val editState = observeEdits(baseNote = baseNote, accountViewModel = accountViewModel) + Row( modifier = Modifier @@ -421,8 +415,10 @@ fun NoteMaster( DisplayFollowingHashtagsInPost(baseNote, accountViewModel, nav) } - if (!editState.modificationsInOrder.value.isEmpty()) { - DisplayEditStatus(editState.showOriginal) + if (editState.value is GenericLoadable.Loaded) { + (editState.value as? GenericLoadable.Loaded)?.loaded?.let { + DisplayEditStatus(it) + } } Text( @@ -850,9 +846,9 @@ private fun RenderWikiHeaderForThreadPreview() { val accountViewModel = mockAccountViewModel() val nav: (String) -> Unit = {} - val editState by + val editState = remember { - mutableStateOf(EditState()) + mutableStateOf(GenericLoadable.Empty()) } runBlocking { diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/AccountViewModel.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/AccountViewModel.kt index f18c22e44..88ec1d47b 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/AccountViewModel.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/AccountViewModel.kt @@ -932,6 +932,8 @@ class AccountViewModel(val account: Account, val settings: SettingsState) : View } } + fun cachedModificationEventsForNote(note: Note) = LocalCache.cachedModificationEventsForNote(note) + suspend fun findModificationEventsForNote( note: Note, onResult: (List) -> Unit, diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index f56c9324b..74e89ae60 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -51,6 +51,7 @@ Boost boosted edited + edit #%1$s original Quote Fork diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/events/TextNoteModificationEvent.kt b/quartz/src/main/java/com/vitorpamplona/quartz/events/TextNoteModificationEvent.kt index 377dbc91d..79018b85a 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/events/TextNoteModificationEvent.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/events/TextNoteModificationEvent.kt @@ -34,6 +34,8 @@ class TextNoteModificationEvent( content: String, sig: HexKey, ) : Event(id, pubKey, createdAt, KIND, tags, content, sig) { + fun editedNote() = firstTaggedEvent() + companion object { const val KIND = 1010 const val ALT = "Content Change Event" From dfec7ae7f55f7c725344c3b08f546ba24b10d073 Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Fri, 1 Mar 2024 12:28:42 -0500 Subject: [PATCH 33/98] Better aligns header elements --- .../main/java/com/vitorpamplona/amethyst/ui/note/NoteCompose.kt | 1 - app/src/main/java/com/vitorpamplona/amethyst/ui/theme/Shape.kt | 2 +- 2 files changed, 1 insertion(+), 2 deletions(-) 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 fc9a92a09..46f0a96f6 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 @@ -2978,7 +2978,6 @@ fun DisplayEditStatus(editState: EditState) { style = LocalTextStyle.current.copy( color = MaterialTheme.colorScheme.placeholderText, - fontSize = Font14SP, fontWeight = FontWeight.Bold, ), maxLines = 1, diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/theme/Shape.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/theme/Shape.kt index 29b1300ed..8b39b9e70 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/theme/Shape.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/theme/Shape.kt @@ -56,7 +56,7 @@ val EditFieldBorder = RoundedCornerShape(25.dp) val ChatBubbleShapeMe = RoundedCornerShape(15.dp, 15.dp, 3.dp, 15.dp) val ChatBubbleShapeThem = RoundedCornerShape(3.dp, 15.dp, 15.dp, 15.dp) -val StdButtonSizeModifier = Modifier.size(20.dp) +val StdButtonSizeModifier = Modifier.size(19.dp) val HalfVertSpacer = Modifier.height(2.dp) From 77d30b6e7ccff014c43b390809c36820c7ff0729 Mon Sep 17 00:00:00 2001 From: Crowdin Bot Date: Fri, 1 Mar 2024 17:30:38 +0000 Subject: [PATCH 34/98] New Crowdin translations by GitHub Action --- app/src/main/res/values-cs/strings.xml | 13 ++++++------- app/src/main/res/values-de/strings.xml | 13 ++++++------- app/src/main/res/values-pt-rBR/strings.xml | 12 +++++------- app/src/main/res/values-sv-rSE/strings.xml | 12 +++++------- 4 files changed, 22 insertions(+), 28 deletions(-) diff --git a/app/src/main/res/values-cs/strings.xml b/app/src/main/res/values-cs/strings.xml index aa267eb06..44a4af3ae 100644 --- a/app/src/main/res/values-cs/strings.xml +++ b/app/src/main/res/values-cs/strings.xml @@ -16,6 +16,7 @@ Spam Zneužívání identity Nezákonné jednání + Jiný Neznámý Ikona předávání Neznámý autor @@ -23,6 +24,9 @@ Kopírovat ID autora Kopírovat ID poznámky Vysílání + Časové razítko + Časové razítko: Čeká na potvrzení + OTS: Čeká Požadovat smazání Blockovat / Nahlásit @@ -43,6 +47,8 @@ Počet zobrazení Zvýšení zvýšeno + upraveno + originál Citovat Rozštěpení Nová částka v sats @@ -661,14 +667,7 @@ Git repositář: %1$s Internet: Klon: - Časové razítko - Časové razítko: Čeká na potvrzení - OTS: Čeká - upraveno - originál - OTS: %1$s Důkaz časového razítka Existuje důkaz, že tento příspěvek byl podepsán někdy před %1$s. Důkaz byl označen v Bitcoin blockchainu v tomto datu a čase. Upravit příspěvek - Jiný diff --git a/app/src/main/res/values-de/strings.xml b/app/src/main/res/values-de/strings.xml index 3e9cbcc58..ea20aafb5 100644 --- a/app/src/main/res/values-de/strings.xml +++ b/app/src/main/res/values-de/strings.xml @@ -16,6 +16,7 @@ Spam Vortäuschung Illegales Verhalten + Andere Unbekannt Relay-Symbol Unbekannter Autor @@ -23,6 +24,9 @@ Autor-ID kopieren Notiz-ID kopieren Übertragungen + Zeitstempel es + Zeitstempel: Ausstehende Bestätigungen + OTS: Ausstehend Löschung beantragen Blockieren / Melden @@ -43,6 +47,8 @@ Aufrufe Boost boosted + bearbeitet + Original Zitat Fork Neuer Betrag in Sats @@ -666,14 +672,7 @@ anz der Bedingungen ist erforderlich Git Repository: %1$s Internet: Klonen: - Zeitstempel es - Zeitstempel: Ausstehende Bestätigungen - OTS: Ausstehend - bearbeitet - Original - OTS: %1$s Zeitstempel Beweis Es gibt einen Beweis, dass dieser Beitrag irgendwann vor %1$s signiert wurde. Der Beweis wurde zu diesem Datum und Uhrzeit in der Bitcoin-Blockchain gestempelt. Beitrag bearbeiten - Andere diff --git a/app/src/main/res/values-pt-rBR/strings.xml b/app/src/main/res/values-pt-rBR/strings.xml index 2125caf75..c9ef64ca2 100644 --- a/app/src/main/res/values-pt-rBR/strings.xml +++ b/app/src/main/res/values-pt-rBR/strings.xml @@ -16,6 +16,7 @@ Spam Representação Comportamento ilegal + Outro Desconhecido Ícone do relay Autor desconhecido @@ -23,6 +24,9 @@ Copiar PubKey do usuário Copiar ID da Nota Transmitir + Carimbá-lo com data/hora + Carimbo de data/hora: Confirmações pendentes + OTS: Pendente Pedir para excluir Bloquear / Denunciar @@ -43,6 +47,7 @@ Contagem de visualizações Impulsionar impulsionado + editado Citar Garfo Novo Valor em Sats @@ -661,14 +666,7 @@ Repositório Git: %1$s Site: Clonar: - Carimbá-lo com data/hora - Carimbo de data/hora: Confirmações pendentes - OTS: Pendente - editado - original - OTS: %1$s Prova de Carimbo de data/hora Há prova de que esta postagem foi assinada antes de %1$s. A prova foi carimbada no blockchain do Bitcoin naquela data e hora. Editar postagem - Outro diff --git a/app/src/main/res/values-sv-rSE/strings.xml b/app/src/main/res/values-sv-rSE/strings.xml index 341c4a1fc..bbd04976d 100644 --- a/app/src/main/res/values-sv-rSE/strings.xml +++ b/app/src/main/res/values-sv-rSE/strings.xml @@ -16,6 +16,7 @@ Spam Imitation Olagligt beteende + Annat Okänd Relä ikon Okänd användare @@ -23,6 +24,9 @@ Kopiera användarens ID Kopiera anteckningens ID Sänd ut + Tidsstämpla det + Tidsstämpel: Väntar på bekräftelser + OTS: Väntar Begär radering Blockera / Rapportera @@ -43,6 +47,7 @@ Antal visningar Boosta boostad + redigerat Citera Förgrening Nytt belopp i Sats @@ -660,14 +665,7 @@ Git Repository: %1$s Webbplats: Klona: - Tidsstämpla det - Tidsstämpel: Väntar på bekräftelser - OTS: Väntar - redigerat - original - OTS: %1$s Tidsstämpel Bevis Det finns bevis på att detta inlägg signerades någon gång före %1$s. Beviset stämplades i Bitcoin-blockchainen vid det datumet och den tiden. Redigera inlägg - Annat From b1a355691a3180c9ea120b806144d879379ca22c Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Fri, 1 Mar 2024 13:41:43 -0500 Subject: [PATCH 35/98] Fixes another caching issue when multiple posts include the same poll. --- .../amethyst/ui/note/PollNoteViewModel.kt | 42 ++++++++++--------- 1 file changed, 22 insertions(+), 20 deletions(-) diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/note/PollNoteViewModel.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/PollNoteViewModel.kt index 0c03ebd7a..5f4949bc8 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/note/PollNoteViewModel.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/PollNoteViewModel.kt @@ -76,29 +76,31 @@ class PollNoteViewModel : ViewModel() { acc: Account, note: Note?, ) { - account = acc - pollNote = note - pollEvent = pollNote?.event as PollNoteEvent - pollOptions = pollEvent?.pollOptions() - valueMaximum = pollEvent?.getTagLong(VALUE_MAXIMUM) - valueMinimum = pollEvent?.getTagLong(VALUE_MINIMUM) - valueMinimumBD = valueMinimum?.let { BigDecimal(it) } - valueMaximumBD = valueMaximum?.let { BigDecimal(it) } - consensusThreshold = - pollEvent?.getTagLong(CONSENSUS_THRESHOLD)?.toFloat()?.div(100)?.toBigDecimal() - closedAt = pollEvent?.getTagLong(CLOSED_AT) + if (acc != account || pollNote != note) { + account = acc + pollNote = note + pollEvent = pollNote?.event as PollNoteEvent + pollOptions = pollEvent?.pollOptions() + valueMaximum = pollEvent?.getTagLong(VALUE_MAXIMUM) + valueMinimum = pollEvent?.getTagLong(VALUE_MINIMUM) + valueMinimumBD = valueMinimum?.let { BigDecimal(it) } + valueMaximumBD = valueMaximum?.let { BigDecimal(it) } + consensusThreshold = + pollEvent?.getTagLong(CONSENSUS_THRESHOLD)?.toFloat()?.div(100)?.toBigDecimal() + closedAt = pollEvent?.getTagLong(CLOSED_AT) - totalZapped = BigDecimal.ZERO - wasZappedByLoggedInAccount = false + totalZapped = BigDecimal.ZERO + wasZappedByLoggedInAccount = false - canZap.value = checkIfCanZap() + canZap.value = checkIfCanZap() - tallies = pollOptions?.keys?.map { option -> - PollOption( - option, - pollOptions?.get(option) ?: "", - ) - } ?: emptyList() + tallies = pollOptions?.keys?.map { option -> + PollOption( + option, + pollOptions?.get(option) ?: "", + ) + } ?: emptyList() + } } fun refreshTallies() { From af784a5bda25ad19641742444bd0baa2decde6bf Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Fri, 1 Mar 2024 14:50:28 -0500 Subject: [PATCH 36/98] Editting the current version of the post instead of only the original post. --- .../amethyst/model/LocalCache.kt | 2 +- .../amethyst/ui/actions/EditPostView.kt | 4 ++-- .../amethyst/ui/actions/EditPostViewModel.kt | 3 ++- .../amethyst/ui/note/BadgeCompose.kt | 2 +- .../amethyst/ui/note/MessageSetCompose.kt | 2 +- .../amethyst/ui/note/MultiSetCompose.kt | 5 +++-- .../amethyst/ui/note/NoteCompose.kt | 19 ++++++++++++------- .../amethyst/ui/note/UserProfilePicture.kt | 10 ++++++++++ .../amethyst/ui/screen/ThreadFeedView.kt | 2 +- .../ui/screen/loggedIn/ChannelScreen.kt | 10 +++++----- .../ui/screen/loggedIn/VideoScreen.kt | 1 + 11 files changed, 39 insertions(+), 21 deletions(-) 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 fc50dc25e..0d4852585 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/model/LocalCache.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/model/LocalCache.kt @@ -1404,7 +1404,7 @@ object LocalCache { note.loadEvent(event, author, emptyList()) event.editedNote()?.let { - getNoteIfExists(it)?.let { editedNote -> + checkGetOrCreateNote(it)?.let { editedNote -> modificationCache.remove(editedNote.idHex) editedNote.liveSet?.innerModifications?.invalidateData() } diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/EditPostView.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/EditPostView.kt index 1d35c13f0..e389b7abc 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/EditPostView.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/EditPostView.kt @@ -37,7 +37,6 @@ import androidx.compose.foundation.layout.heightIn import androidx.compose.foundation.layout.imePadding import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size -import androidx.compose.foundation.layout.width import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.itemsIndexed import androidx.compose.foundation.rememberScrollState @@ -115,6 +114,7 @@ import kotlinx.coroutines.withContext fun EditPostView( onClose: () -> Unit, edit: Note, + versionLookingAt: Note?, accountViewModel: AccountViewModel, nav: (String) -> Unit, ) { @@ -128,7 +128,7 @@ fun EditPostView( var relayList = remember { accountViewModel.account.activeWriteRelays().toImmutableList() } LaunchedEffect(Unit) { - postViewModel.load(edit, accountViewModel) + postViewModel.load(edit, versionLookingAt, accountViewModel) launch(Dispatchers.IO) { postViewModel.imageUploadingError.collect { error -> diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/EditPostViewModel.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/EditPostViewModel.kt index 99c0b6314..13c188653 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/EditPostViewModel.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/EditPostViewModel.kt @@ -82,6 +82,7 @@ open class EditPostViewModel() : ViewModel() { open fun load( edit: Note, + versionLookingAt: Note?, accountViewModel: AccountViewModel, ) { this.accountViewModel = accountViewModel @@ -90,7 +91,7 @@ open class EditPostViewModel() : ViewModel() { canAddInvoice = accountViewModel.userProfile().info?.lnAddress() != null contentToAddUrl = null - message = TextFieldValue(edit.event?.content() ?: "") + message = TextFieldValue(versionLookingAt?.event?.content() ?: edit.event?.content() ?: "") urlPreview = findUrlInMessage() editedFromNote = edit diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/note/BadgeCompose.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/BadgeCompose.kt index fe987ca47..c37701b43 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/note/BadgeCompose.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/BadgeCompose.kt @@ -167,7 +167,7 @@ fun BadgeCompose( tint = MaterialTheme.colorScheme.placeholderText, ) - NoteDropDownMenu(note, popupExpanded, accountViewModel, nav) + NoteDropDownMenu(note, popupExpanded, null, accountViewModel, nav) } } diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/note/MessageSetCompose.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/MessageSetCompose.kt index 17e95d00c..c48dc99d7 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/note/MessageSetCompose.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/MessageSetCompose.kt @@ -129,7 +129,7 @@ fun MessageSetCompose( nav = nav, ) - NoteDropDownMenu(baseNote, popupExpanded, accountViewModel, nav) + NoteDropDownMenu(baseNote, popupExpanded, null, accountViewModel, nav) } } diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/note/MultiSetCompose.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/MultiSetCompose.kt index 5a8cb21ff..15e302737 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/note/MultiSetCompose.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/MultiSetCompose.kt @@ -74,6 +74,7 @@ import com.vitorpamplona.amethyst.ui.screen.CombinedZap import com.vitorpamplona.amethyst.ui.screen.MultiSetCard import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel import com.vitorpamplona.amethyst.ui.theme.DividerThickness +import com.vitorpamplona.amethyst.ui.theme.HalfTopPadding import com.vitorpamplona.amethyst.ui.theme.NotificationIconModifier import com.vitorpamplona.amethyst.ui.theme.NotificationIconModifierSmaller import com.vitorpamplona.amethyst.ui.theme.Size10dp @@ -158,7 +159,7 @@ fun MultiSetCompose( NoteCompose( baseNote = baseNote, routeForLastRead = null, - modifier = remember { Modifier.padding(top = 5.dp) }, + modifier = HalfTopPadding, isBoostedNote = true, showHidden = showHidden, parentBackgroundColor = backgroundColor, @@ -166,7 +167,7 @@ fun MultiSetCompose( nav = nav, ) - NoteDropDownMenu(baseNote, popupExpanded, accountViewModel, nav) + NoteDropDownMenu(baseNote, popupExpanded, null, accountViewModel, nav) } Divider( 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 46f0a96f6..ea6361e6a 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 @@ -782,7 +782,7 @@ fun LongCommunityHeader( ) Spacer(DoubleHorzSpacer) NormalTimeAgo(baseNote = baseNote, Modifier.weight(1f)) - MoreOptionsButton(baseNote, accountViewModel, nav) + MoreOptionsButton(baseNote, null, accountViewModel, nav) } } @@ -2907,7 +2907,7 @@ fun FirstUserInfoRow( TimeAgo(baseNote) - MoreOptionsButton(baseNote, accountViewModel, nav) + MoreOptionsButton(baseNote, editState, accountViewModel, nav) } } @@ -2916,6 +2916,10 @@ fun observeEdits( baseNote: Note, accountViewModel: AccountViewModel, ): State> { + if (baseNote.event !is TextNoteEvent) { + return remember { mutableStateOf(GenericLoadable.Empty()) } + } + val editState = remember(baseNote.idHex) { val cached = accountViewModel.cachedModificationEventsForNote(baseNote) @@ -2934,10 +2938,10 @@ fun observeEdits( ) } - val updatedNote = baseNote.live().innerModifications.observeAsState() + val updatedNote by baseNote.live().innerModifications.observeAsState() LaunchedEffect(key1 = updatedNote) { - updatedNote.value?.note?.let { + updatedNote?.note?.let { accountViewModel.findModificationEventsForNote(it) { newModifications -> if (newModifications.isEmpty()) { if (editState.value !is GenericLoadable.Empty) { @@ -2999,6 +3003,7 @@ private fun BoostedMark() { @Composable fun MoreOptionsButton( baseNote: Note, + editState: State>? = null, accountViewModel: AccountViewModel, nav: (String) -> Unit, ) { @@ -3014,6 +3019,7 @@ fun MoreOptionsButton( NoteDropDownMenu( baseNote, popupExpanded, + editState, accountViewModel, nav, ) @@ -3834,9 +3840,8 @@ private fun RenderShortRepositoryHeader( accountViewModel: AccountViewModel, nav: (String) -> Unit, ) { - val noteState = baseNote.live().metadata.observeAsState() - val note = remember(noteState) { noteState.value?.note } ?: return - val noteEvent = note.event as? GitRepositoryEvent ?: return + val noteState by baseNote.live().metadata.observeAsState() + val noteEvent = noteState?.note?.event as? GitRepositoryEvent ?: return Column( modifier = MaterialTheme.colorScheme.replyModifier.padding(10.dp), diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/note/UserProfilePicture.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/UserProfilePicture.kt index 747b6c2e9..04c1cd08b 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/note/UserProfilePicture.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/UserProfilePicture.kt @@ -43,6 +43,7 @@ import androidx.compose.runtime.Composable import androidx.compose.runtime.Immutable 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 @@ -67,6 +68,7 @@ import com.vitorpamplona.amethyst.R import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.model.User import com.vitorpamplona.amethyst.ui.actions.EditPostView +import com.vitorpamplona.amethyst.ui.components.GenericLoadable import com.vitorpamplona.amethyst.ui.components.RobohashAsyncImage import com.vitorpamplona.amethyst.ui.components.RobohashFallbackAsyncImage import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel @@ -456,6 +458,7 @@ data class DropDownParams( fun NoteDropDownMenu( note: Note, popupExpanded: MutableState, + editState: State>? = null, accountViewModel: AccountViewModel, nav: (String) -> Unit, ) { @@ -482,12 +485,19 @@ fun NoteDropDownMenu( } if (wantsToEditPost.value) { + // avoids changing while drafting a note and a new event shows up. + val versionLookingAt = + remember { + (editState?.value as? GenericLoadable.Loaded)?.loaded?.modificationToShow?.value + } + EditPostView( onClose = { popupExpanded.value = false wantsToEditPost.value = false }, edit = note, + versionLookingAt = versionLookingAt, accountViewModel = accountViewModel, nav = nav, ) diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/ThreadFeedView.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/ThreadFeedView.kt index c54c31228..4b0c5f7e8 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/ThreadFeedView.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/ThreadFeedView.kt @@ -438,7 +438,7 @@ fun NoteMaster( tint = MaterialTheme.colorScheme.placeholderText, ) - NoteDropDownMenu(baseNote, moreActionsExpanded, accountViewModel, nav) + NoteDropDownMenu(baseNote, moreActionsExpanded, editState, accountViewModel, nav) } } diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/ChannelScreen.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/ChannelScreen.kt index f24fca756..86f9e325d 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/ChannelScreen.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/ChannelScreen.kt @@ -710,8 +710,8 @@ fun ShortChannelHeader( nav: (String) -> Unit, showFlag: Boolean, ) { - val channelState = baseChannel.live.observeAsState() - val channel = remember(channelState) { channelState.value?.channel } ?: return + val channelState by baseChannel.live.observeAsState() + val channel = channelState?.channel ?: return val automaticallyShowProfilePicture = remember { @@ -782,8 +782,8 @@ fun LongChannelHeader( accountViewModel: AccountViewModel, nav: (String) -> Unit, ) { - val channelState = baseChannel.live.observeAsState() - val channel = remember(channelState) { channelState.value?.channel } ?: return + val channelState by baseChannel.live.observeAsState() + val channel = channelState?.channel ?: return Row( lineModifier, @@ -865,7 +865,7 @@ fun LongChannelHeader( ) Spacer(DoubleHorzSpacer) NormalTimeAgo(note, remember { Modifier.weight(1f) }) - MoreOptionsButton(note, accountViewModel, nav) + MoreOptionsButton(note, null, accountViewModel, nav) } } } diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/VideoScreen.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/VideoScreen.kt index 9f3e9136e..1dface6d1 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/VideoScreen.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/VideoScreen.kt @@ -406,6 +406,7 @@ private fun VideoUserOptionAction( NoteDropDownMenu( note, popupExpanded, + null, accountViewModel, nav, ) From bc7a578cfec6a78dc2655c35b5adb7ee40d512e6 Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Fri, 1 Mar 2024 15:39:24 -0500 Subject: [PATCH 37/98] Making forks work with revisions. --- .../vitorpamplona/amethyst/ui/actions/NewPostView.kt | 3 ++- .../amethyst/ui/actions/NewPostViewModel.kt | 3 ++- .../com/vitorpamplona/amethyst/ui/note/NoteCompose.kt | 6 ++++-- .../com/vitorpamplona/amethyst/ui/note/ReactionsRow.kt | 10 +++++++++- .../vitorpamplona/amethyst/ui/screen/ThreadFeedView.kt | 2 +- 5 files changed, 18 insertions(+), 6 deletions(-) diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewPostView.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewPostView.kt index 854fa87f8..20efa29bf 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewPostView.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewPostView.kt @@ -186,6 +186,7 @@ fun NewPostView( baseReplyTo: Note? = null, quote: Note? = null, fork: Note? = null, + version: Note? = null, enableMessageInterface: Boolean = false, accountViewModel: AccountViewModel, nav: (String) -> Unit, @@ -201,7 +202,7 @@ fun NewPostView( var relayList = remember { accountViewModel.account.activeWriteRelays().toImmutableList() } LaunchedEffect(Unit) { - postViewModel.load(accountViewModel, baseReplyTo, quote, fork) + postViewModel.load(accountViewModel, baseReplyTo, quote, fork, version) launch(Dispatchers.IO) { postViewModel.imageUploadingError.collect { error -> diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewPostViewModel.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewPostViewModel.kt index 47838ecd5..510e258be 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewPostViewModel.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewPostViewModel.kt @@ -180,6 +180,7 @@ open class NewPostViewModel() : ViewModel() { replyingTo: Note?, quote: Note?, fork: Note?, + version: Note?, ) { this.accountViewModel = accountViewModel this.account = accountViewModel.account @@ -244,7 +245,7 @@ open class NewPostViewModel() : ViewModel() { } fork?.let { - message = TextFieldValue(it.event?.content() ?: "") + message = TextFieldValue(version?.event?.content() ?: it.event?.content() ?: "") urlPreview = findUrlInMessage() it.event?.isSensitive()?.let { 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 ea6361e6a..9f38ee16e 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 @@ -1025,6 +1025,7 @@ fun InnerNoteWithReactions( nav: (String) -> Unit, ) { val notBoostedNorQuote = !isBoostedNote && !isQuotedNote + val editState = observeEdits(baseNote = baseNote, accountViewModel = accountViewModel) Row( modifier = @@ -1057,6 +1058,7 @@ fun InnerNoteWithReactions( canPreview = canPreview, showSecondRow = showSecondRow, backgroundColor = backgroundColor, + editState = editState, accountViewModel = accountViewModel, nav = nav, ) @@ -1075,6 +1077,7 @@ fun InnerNoteWithReactions( ReactionsRow( baseNote = baseNote, showReactionDetail = notBoostedNorQuote, + editState = editState, accountViewModel = accountViewModel, nav = nav, ) @@ -1151,11 +1154,10 @@ private fun NoteBody( canPreview: Boolean = true, showSecondRow: Boolean, backgroundColor: MutableState, + editState: State>, accountViewModel: AccountViewModel, nav: (String) -> Unit, ) { - val editState = observeEdits(baseNote = baseNote, accountViewModel = accountViewModel) - FirstUserInfoRow( baseNote = baseNote, showAuthorPicture = showAuthorPicture, diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/note/ReactionsRow.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/ReactionsRow.kt index 794a20752..64986be17 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/note/ReactionsRow.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/ReactionsRow.kt @@ -61,6 +61,7 @@ import androidx.compose.runtime.Composable import androidx.compose.runtime.Immutable 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 @@ -99,6 +100,7 @@ import com.vitorpamplona.amethyst.R import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.service.ZapPaymentHandler import com.vitorpamplona.amethyst.ui.actions.NewPostView +import com.vitorpamplona.amethyst.ui.components.GenericLoadable import com.vitorpamplona.amethyst.ui.components.InLineIconRenderer import com.vitorpamplona.amethyst.ui.navigation.routeToMessage import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel @@ -145,6 +147,7 @@ import kotlin.math.roundToInt fun ReactionsRow( baseNote: Note, showReactionDetail: Boolean, + editState: State>, accountViewModel: AccountViewModel, nav: (String) -> Unit, ) { @@ -152,7 +155,7 @@ fun ReactionsRow( Spacer(modifier = HalfDoubleVertSpacer) - InnerReactionRow(baseNote, showReactionDetail, wantsToSeeReactions, accountViewModel, nav) + InnerReactionRow(baseNote, showReactionDetail, wantsToSeeReactions, editState, accountViewModel, nav) Spacer(modifier = HalfDoubleVertSpacer) @@ -169,6 +172,7 @@ private fun InnerReactionRow( baseNote: Note, showReactionDetail: Boolean, wantsToSeeReactions: MutableState, + editState: State>, accountViewModel: AccountViewModel, nav: (String) -> Unit, ) { @@ -190,6 +194,7 @@ private fun InnerReactionRow( three = { BoostWithDialog( baseNote, + editState, MaterialTheme.colorScheme.placeholderText, accountViewModel, nav, @@ -495,6 +500,7 @@ private fun WatchZapAndRenderGallery( @Composable private fun BoostWithDialog( baseNote: Note, + editState: State>, grayTint: Color, accountViewModel: AccountViewModel, nav: (String) -> Unit, @@ -507,6 +513,7 @@ private fun BoostWithDialog( onClose = { wantsToQuote = null }, baseReplyTo = null, quote = wantsToQuote, + version = (editState.value as? GenericLoadable.Loaded)?.loaded?.modificationToShow?.value, accountViewModel = accountViewModel, nav = nav, ) @@ -528,6 +535,7 @@ private fun BoostWithDialog( onClose = { wantsToFork = null }, baseReplyTo = replyTo, fork = wantsToFork, + version = (editState.value as? GenericLoadable.Loaded)?.loaded?.modificationToShow?.value, accountViewModel = accountViewModel, nav = nav, ) diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/ThreadFeedView.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/ThreadFeedView.kt index 4b0c5f7e8..2ff8f03c4 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/ThreadFeedView.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/ThreadFeedView.kt @@ -608,7 +608,7 @@ fun NoteMaster( DisplayZapSplits(noteEvent, false, accountViewModel, nav) } - ReactionsRow(note, true, accountViewModel, nav) + ReactionsRow(note, true, editState, accountViewModel, nav) Divider( thickness = DividerThickness, From cbce22c994b3a67022e4e74ae7e3f5758de645e2 Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Fri, 1 Mar 2024 15:46:58 -0500 Subject: [PATCH 38/98] Fixes the alt tag --- .../vitorpamplona/quartz/events/TextNoteModificationEvent.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/events/TextNoteModificationEvent.kt b/quartz/src/main/java/com/vitorpamplona/quartz/events/TextNoteModificationEvent.kt index 79018b85a..c3dd0a9ac 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/events/TextNoteModificationEvent.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/events/TextNoteModificationEvent.kt @@ -47,7 +47,7 @@ class TextNoteModificationEvent( createdAt: Long = TimeUtils.now(), onReady: (TextNoteModificationEvent) -> Unit, ) { - val tags = arrayOf(arrayOf("e", eventId), arrayOf("alt", CalendarDateSlotEvent.ALT)) + val tags = arrayOf(arrayOf("e", eventId), arrayOf("alt", ALT)) signer.sign(createdAt, KIND, tags, content, onReady) } } From 3f500ef5b322e0b22837e0b438825982f534cc10 Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Fri, 1 Mar 2024 16:04:26 -0500 Subject: [PATCH 39/98] Make sure to add the forked author to the p-tag of a note. --- app/src/main/java/com/vitorpamplona/amethyst/model/Account.kt | 2 +- .../com/vitorpamplona/amethyst/ui/actions/NewPostViewModel.kt | 4 +++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/app/src/main/java/com/vitorpamplona/amethyst/model/Account.kt b/app/src/main/java/com/vitorpamplona/amethyst/model/Account.kt index ae52d71f3..c77e1235e 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/model/Account.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/model/Account.kt @@ -1420,8 +1420,8 @@ class Account( eventId = idHex, signer = signer, ) { - Client.send(it, relayList = relayList) LocalCache.justConsume(it, null) + Client.send(it, relayList = relayList) } } diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewPostViewModel.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewPostViewModel.kt index 510e258be..21439fb7f 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewPostViewModel.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewPostViewModel.kt @@ -280,7 +280,9 @@ open class NewPostViewModel() : ViewModel() { } it.author?.let { - if (this.pTags?.contains(it) != true) { + if (this.pTags == null) { + this.pTags = listOf(it) + } else if (this.pTags?.contains(it) != true) { this.pTags = listOf(it) + (this.pTags ?: emptyList()) } } From c34811e05b9ecf93e7707b78fbf2180e0a06963d Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Fri, 1 Mar 2024 16:15:06 -0500 Subject: [PATCH 40/98] Tries to highlight that there are two states here for Compose to observe. --- .../java/com/vitorpamplona/amethyst/ui/note/NoteCompose.kt | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) 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 9f38ee16e..50394e144 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 @@ -1416,7 +1416,8 @@ fun RenderTextEvent( val subject = (note.event as? TextNoteEvent)?.subject()?.ifEmpty { null } val newBody = if (editState.value is GenericLoadable.Loaded) { - (editState.value as? GenericLoadable.Loaded)?.loaded?.modificationToShow?.value?.event?.content() ?: body + val state = (editState.value as? GenericLoadable.Loaded)?.loaded?.modificationToShow + state?.value?.event?.content() ?: body } else { body } From 48a6923537bcd7dc3e3fc57756059a36d46d5550 Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Fri, 1 Mar 2024 16:15:18 -0500 Subject: [PATCH 41/98] Forces edits to be from the same user. --- .../main/java/com/vitorpamplona/amethyst/model/LocalCache.kt | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) 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 0d4852585..7e8015912 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/model/LocalCache.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/model/LocalCache.kt @@ -1733,6 +1733,8 @@ object LocalCache { suspend fun findLatestModificationForNote(note: Note): List { checkNotInMainThread() + val originalAuthor = note.author?.pubkeyHex ?: return emptyList() + modificationCache[note.idHex]?.let { return it } @@ -1743,7 +1745,7 @@ object LocalCache { noteListCache.filter { item -> val noteEvent = item.event - noteEvent is TextNoteModificationEvent && noteEvent.isTaggedEvent(note.idHex) && !noteEvent.isExpirationBefore(time) + noteEvent is TextNoteModificationEvent && noteEvent.pubKey == originalAuthor && noteEvent.isTaggedEvent(note.idHex) && !noteEvent.isExpirationBefore(time) }.sortedWith(compareBy({ it.createdAt() }, { it.idHex })) modificationCache.put(note.idHex, newNotes) From c53fc031570960bf4cce845e8f3c68573c70a78d Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Fri, 1 Mar 2024 16:52:18 -0500 Subject: [PATCH 42/98] Cache must update before updating the screen. --- .../java/com/vitorpamplona/amethyst/model/LocalCache.kt | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) 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 7e8015912..53d38dcaa 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/model/LocalCache.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/model/LocalCache.kt @@ -1406,6 +1406,10 @@ object LocalCache { event.editedNote()?.let { checkGetOrCreateNote(it)?.let { editedNote -> modificationCache.remove(editedNote.idHex) + // if it is a new post from the user, must update list of Notes to quickly update the user. + if (relay == null) { + updateListCache() + } editedNote.liveSet?.innerModifications?.invalidateData() } } @@ -2132,8 +2136,8 @@ class LocalCacheLiveData { fun invalidateData(newNote: Note) { bundler.invalidateList(newNote) { bundledNewNotes -> - _newEventBundles.emit(bundledNewNotes) LocalCache.updateListCache() + _newEventBundles.emit(bundledNewNotes) } } } From 786802b708259813d62e9803e4269da330c0a939 Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Fri, 1 Mar 2024 17:07:19 -0500 Subject: [PATCH 43/98] Presents notifications when a person forks your content. --- .../java/com/vitorpamplona/amethyst/model/LocalCache.kt | 8 +++++--- .../amethyst/ui/dal/NotificationFeedFilter.kt | 4 +++- .../com/vitorpamplona/quartz/events/BaseTextNoteEvent.kt | 2 ++ 3 files changed, 10 insertions(+), 4 deletions(-) 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 53d38dcaa..3953baa82 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/model/LocalCache.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/model/LocalCache.kt @@ -747,7 +747,9 @@ object LocalCache { if (version.event == null) { version.loadEvent(event, author, emptyList()) - + if (version.liveSet != null) { + updateListCache() + } version.liveSet?.innerOts?.invalidateData() } @@ -1406,8 +1408,8 @@ object LocalCache { event.editedNote()?.let { checkGetOrCreateNote(it)?.let { editedNote -> modificationCache.remove(editedNote.idHex) - // if it is a new post from the user, must update list of Notes to quickly update the user. - if (relay == null) { + // must update list of Notes to quickly update the user. + if (editedNote.liveSet != null) { updateListCache() } editedNote.liveSet?.innerModifications?.invalidateData() diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/dal/NotificationFeedFilter.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/dal/NotificationFeedFilter.kt index a92a0d4a5..33d86af79 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/dal/NotificationFeedFilter.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/dal/NotificationFeedFilter.kt @@ -99,8 +99,10 @@ class NotificationFeedFilter(val account: Account) : AdditiveFeedFilter() val isAuthoredPostCited = event.findCitations().any { LocalCache.getNoteIfExists(it)?.author?.pubkeyHex == authorHex } val isAuthorDirectlyCited = event.citedUsers().contains(authorHex) + val isAuthorOfAFork = + event.isForkFromAddressWithPubkey(authorHex) || (event.forkFromVersion()?.let { LocalCache.getNoteIfExists(it)?.author?.pubkeyHex == authorHex } == true) - return isAuthoredPostCited || isAuthorDirectlyCited + return isAuthoredPostCited || isAuthorDirectlyCited || isAuthorOfAFork } if (event is ReactionEvent) { diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/events/BaseTextNoteEvent.kt b/quartz/src/main/java/com/vitorpamplona/quartz/events/BaseTextNoteEvent.kt index a6d202298..aa65eb642 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/events/BaseTextNoteEvent.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/events/BaseTextNoteEvent.kt @@ -55,6 +55,8 @@ open class BaseTextNoteEvent( fun forkFromVersion() = tags.firstOrNull { it.size > 3 && it[0] == "e" && it[3] == "fork" }?.get(1) + fun isForkFromAddressWithPubkey(authorHex: HexKey) = tags.any { it.size > 3 && it[0] == "a" && it[3] == "fork" && it[1].contains(authorHex) } + open fun replyTos(): List { val oldStylePositional = tags.filter { it.size > 1 && it.size <= 3 && it[0] == "e" }.map { it[1] } val newStyleReply = tags.lastOrNull { it.size > 3 && it[0] == "e" && it[3] == "reply" }?.get(1) From 81cc985e3b0ad559e5a828806cdcaf464ca074a6 Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Fri, 1 Mar 2024 18:09:15 -0500 Subject: [PATCH 44/98] Adds the ability to see and reply to Git Issues and Patches. --- .../vitorpamplona/amethyst/model/Account.kt | 58 +++++++++++++ .../amethyst/model/LocalCache.kt | 9 +- .../service/NostrSingleEventDataSource.kt | 2 + .../amethyst/ui/actions/NewPostViewModel.kt | 40 +++++++++ .../quartz/events/GitIssueEvent.kt | 2 +- .../quartz/events/GitReplyEvent.kt | 83 +++++++++++++++++++ .../quartz/events/TextNoteEvent.kt | 72 ++++++++-------- 7 files changed, 228 insertions(+), 38 deletions(-) diff --git a/app/src/main/java/com/vitorpamplona/amethyst/model/Account.kt b/app/src/main/java/com/vitorpamplona/amethyst/model/Account.kt index c77e1235e..2d1d84a2d 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/model/Account.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/model/Account.kt @@ -67,6 +67,7 @@ import com.vitorpamplona.quartz.events.FileStorageHeaderEvent import com.vitorpamplona.quartz.events.GeneralListEvent import com.vitorpamplona.quartz.events.GenericRepostEvent import com.vitorpamplona.quartz.events.GiftWrapEvent +import com.vitorpamplona.quartz.events.GitReplyEvent import com.vitorpamplona.quartz.events.HTTPAuthorizationEvent import com.vitorpamplona.quartz.events.LiveActivitiesChatMessageEvent import com.vitorpamplona.quartz.events.LnZapEvent @@ -1349,6 +1350,63 @@ class Account( } } + fun sendGitReply( + message: String, + replyTo: List?, + mentions: List?, + repository: ATag?, + zapReceiver: List? = null, + wantsToMarkAsSensitive: Boolean, + zapRaiserAmount: Long? = null, + replyingTo: String?, + root: String?, + directMentions: Set, + forkedFrom: Event?, + relayList: List? = null, + geohash: String? = null, + nip94attachments: List? = null, + ) { + if (!isWriteable()) return + + val repliesToHex = replyTo?.filter { it.address() == null }?.map { it.idHex } + val mentionsHex = mentions?.map { it.pubkeyHex } + val addresses = listOfNotNull(repository) + (replyTo?.mapNotNull { it.address() } ?: emptyList()) + + GitReplyEvent.create( + msg = message, + replyTos = repliesToHex, + mentions = mentionsHex, + addresses = addresses, + extraTags = null, + zapReceiver = zapReceiver, + markAsSensitive = wantsToMarkAsSensitive, + zapRaiserAmount = zapRaiserAmount, + replyingTo = replyingTo, + root = root, + directMentions = directMentions, + geohash = geohash, + nip94attachments = nip94attachments, + forkedFrom = forkedFrom, + signer = signer, + ) { + Client.send(it, relayList = relayList) + LocalCache.justConsume(it, null) + + // broadcast replied notes + replyingTo?.let { + LocalCache.getNoteIfExists(replyingTo)?.event?.let { + Client.send(it, relayList = relayList) + } + } + replyTo?.forEach { it.event?.let { Client.send(it, relayList = relayList) } } + addresses?.forEach { + LocalCache.getAddressableNoteIfExists(it.toTag())?.event?.let { + Client.send(it, relayList = relayList) + } + } + } + } + fun sendPost( message: String, replyTo: List?, 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 3953baa82..76ba4fade 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/model/LocalCache.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/model/LocalCache.kt @@ -451,14 +451,21 @@ object LocalCache { return } + val repository = event.repository()?.toTag() + val replyTo = event .tagsWithoutCitations() - .filter { it != event.repository()?.toTag() } + .filter { it != repository } .mapNotNull { checkGetOrCreateNote(it) } + // println("New GitReply ${event.id} for ${replyTo.firstOrNull()?.event?.id()} ${event.tagsWithoutCitations().filter { it != event.repository()?.toTag() }.firstOrNull()}") + note.loadEvent(event, author, replyTo) + // Counts the replies + replyTo.forEach { it.addReply(note) } + refreshObservers(note) } diff --git a/app/src/main/java/com/vitorpamplona/amethyst/service/NostrSingleEventDataSource.kt b/app/src/main/java/com/vitorpamplona/amethyst/service/NostrSingleEventDataSource.kt index 618dcbeb9..659314695 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/service/NostrSingleEventDataSource.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/service/NostrSingleEventDataSource.kt @@ -29,6 +29,7 @@ import com.vitorpamplona.amethyst.service.relays.JsonFilter import com.vitorpamplona.amethyst.service.relays.TypedFilter import com.vitorpamplona.quartz.events.CommunityPostApprovalEvent import com.vitorpamplona.quartz.events.GenericRepostEvent +import com.vitorpamplona.quartz.events.GitReplyEvent import com.vitorpamplona.quartz.events.LiveActivitiesChatMessageEvent import com.vitorpamplona.quartz.events.LnZapEvent import com.vitorpamplona.quartz.events.OtsEvent @@ -138,6 +139,7 @@ object NostrSingleEventDataSource : NostrDataSource("SingleEventFeed") { PollNoteEvent.KIND, OtsEvent.KIND, TextNoteModificationEvent.KIND, + GitReplyEvent.KIND, ), tags = mapOf("e" to it.map { it.idHex }), since = findMinimumEOSEs(it), diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewPostViewModel.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewPostViewModel.kt index 21439fb7f..0959d90b4 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewPostViewModel.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewPostViewModel.kt @@ -59,6 +59,7 @@ import com.vitorpamplona.quartz.events.Event import com.vitorpamplona.quartz.events.FileHeaderEvent import com.vitorpamplona.quartz.events.FileStorageEvent import com.vitorpamplona.quartz.events.FileStorageHeaderEvent +import com.vitorpamplona.quartz.events.GitIssueEvent import com.vitorpamplona.quartz.events.Price import com.vitorpamplona.quartz.events.PrivateDmEvent import com.vitorpamplona.quartz.events.TextNoteEvent @@ -434,6 +435,45 @@ open class NewPostViewModel() : ViewModel() { nip94attachments = usedAttachments, ) } + } else if (originalNote?.event is GitIssueEvent) { + val originalNoteEvent = originalNote?.event as GitIssueEvent + // adds markers + val rootId = + originalNoteEvent.rootIssueOrPatch() // if it has a marker as root + ?: originalNote + ?.replyTo + ?.firstOrNull { it.event != null && it.replyTo?.isEmpty() == true } + ?.idHex // if it has loaded events with zero replies in the reply list + ?: originalNote?.replyTo?.firstOrNull()?.idHex // old rules, first item is root. + ?: originalNote?.idHex + + val replyId = originalNote?.idHex + + val replyToSet = + if (forkedFromNote != null) { + (listOfNotNull(forkedFromNote) + (tagger.eTags ?: emptyList())).ifEmpty { null } + } else { + tagger.eTags + } + + val repositoryAddress = originalNoteEvent.repository() + + account?.sendGitReply( + message = tagger.message, + replyTo = replyToSet, + mentions = tagger.pTags, + repository = repositoryAddress, + zapReceiver = zapReceiver, + wantsToMarkAsSensitive = wantsToMarkAsSensitive, + zapRaiserAmount = localZapRaiserAmount, + replyingTo = replyId, + root = rootId, + directMentions = tagger.directMentions, + forkedFrom = forkedFromNote?.event as? Event, + relayList = relayList, + geohash = geoHash, + nip94attachments = usedAttachments, + ) } else { if (wantsPoll) { account?.sendPoll( diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/events/GitIssueEvent.kt b/quartz/src/main/java/com/vitorpamplona/quartz/events/GitIssueEvent.kt index ed11d2015..2c1752ffe 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/events/GitIssueEvent.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/events/GitIssueEvent.kt @@ -41,7 +41,7 @@ class GitIssueEvent( private fun repositoryHex() = innerRepository()?.getOrNull(1) - fun rootIssueOrPath() = tags.lastOrNull { it.size > 3 && it[0] == "e" && it[3] == "root" }?.get(1) + fun rootIssueOrPatch() = tags.lastOrNull { it.size > 3 && it[0] == "e" && it[3] == "root" }?.get(1) fun repository() = innerRepository()?.let { diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/events/GitReplyEvent.kt b/quartz/src/main/java/com/vitorpamplona/quartz/events/GitReplyEvent.kt index 505685b16..f083c895f 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/events/GitReplyEvent.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/events/GitReplyEvent.kt @@ -23,6 +23,7 @@ package com.vitorpamplona.quartz.events import androidx.compose.runtime.Immutable import com.vitorpamplona.quartz.encoders.ATag import com.vitorpamplona.quartz.encoders.HexKey +import com.vitorpamplona.quartz.encoders.Nip92MediaAttachments import com.vitorpamplona.quartz.signers.NostrSigner import com.vitorpamplona.quartz.utils.TimeUtils @@ -75,5 +76,87 @@ class GitReplyEvent( signer.sign(createdAt, KIND, tags.toTypedArray(), content, onReady) } + + fun create( + msg: String, + replyTos: List? = null, + mentions: List? = null, + addresses: List? = null, + extraTags: List? = null, + zapReceiver: List? = null, + markAsSensitive: Boolean = false, + zapRaiserAmount: Long? = null, + replyingTo: String? = null, + root: String? = null, + directMentions: Set = emptySet(), + geohash: String? = null, + nip94attachments: List? = null, + forkedFrom: Event? = null, + signer: NostrSigner, + createdAt: Long = TimeUtils.now(), + onReady: (GitReplyEvent) -> Unit, + ) { + val tags = mutableListOf>() + replyTos?.let { + tags.addAll( + it.positionalMarkedTags( + tagName = "e", + root = root, + replyingTo = replyingTo, + directMentions = directMentions, + forkedFrom = forkedFrom?.id, + ), + ) + } + mentions?.forEach { + if (it in directMentions) { + tags.add(arrayOf("p", it, "", "mention")) + } else { + tags.add(arrayOf("p", it)) + } + } + replyTos?.forEach { + if (it in directMentions) { + tags.add(arrayOf("q", it)) + } + } + addresses + ?.map { it.toTag() } + ?.let { + tags.addAll( + it.positionalMarkedTags( + tagName = "a", + root = root, + replyingTo = replyingTo, + directMentions = directMentions, + forkedFrom = (forkedFrom as? AddressableEvent)?.address()?.toTag(), + ), + ) + } + findHashtags(msg).forEach { + tags.add(arrayOf("t", it)) + tags.add(arrayOf("t", it.lowercase())) + } + extraTags?.forEach { tags.add(arrayOf("t", it)) } + zapReceiver?.forEach { + tags.add(arrayOf("zap", it.lnAddressOrPubKeyHex, it.relay ?: "", it.weight.toString())) + } + findURLs(msg).forEach { tags.add(arrayOf("r", it)) } + if (markAsSensitive) { + tags.add(arrayOf("content-warning", "")) + } + zapRaiserAmount?.let { tags.add(arrayOf("zapraiser", "$it")) } + geohash?.let { tags.addAll(geohashMipMap(it)) } + nip94attachments?.let { + it.forEach { + Nip92MediaAttachments().convertFromFileHeader(it)?.let { + tags.add(it) + } + } + } + tags.add(arrayOf("alt", "a git issue reply")) + + signer.sign(createdAt, KIND, tags.toTypedArray(), msg, onReady) + } } } diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/events/TextNoteEvent.kt b/quartz/src/main/java/com/vitorpamplona/quartz/events/TextNoteEvent.kt index 79a3161b2..13f81f043 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/events/TextNoteEvent.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/events/TextNoteEvent.kt @@ -123,45 +123,45 @@ class TextNoteEvent( signer.sign(createdAt, KIND, tags.toTypedArray(), msg, onReady) } - - /** - * Returns a list of NIP-10 marked tags that are also ordered at best effort to support the - * deprecated method of positional tags to maximize backwards compatibility with clients that - * support replies but have not been updated to understand tag markers. - * - * https://github.com/nostr-protocol/nips/blob/master/10.md - * - * The tag to the root of the reply chain goes first. The tag to the reply event being responded - * to goes last. The order for any other tag does not matter, so keep the relative order. - */ - private fun List.positionalMarkedTags( - tagName: String, - root: String?, - replyingTo: String?, - directMentions: Set, - forkedFrom: String?, - ) = sortedWith { o1, o2 -> - when { - o1 == o2 -> 0 - o1 == root -> -1 // root goes first - o2 == root -> 1 // root goes first - o1 == replyingTo -> 1 // reply event being responded to goes last - o2 == replyingTo -> -1 // reply event being responded to goes last - else -> 0 // keep the relative order for any other tag - } - } - .map { - when (it) { - root -> arrayOf(tagName, it, "", "root") - replyingTo -> arrayOf(tagName, it, "", "reply") - forkedFrom -> arrayOf(tagName, it, "", "fork") - in directMentions -> arrayOf(tagName, it, "", "mention") - else -> arrayOf(tagName, it) - } - } } } fun findURLs(text: String): List { return UrlDetector(text, UrlDetectorOptions.Default).detect().map { it.originalUrl } } + +/** + * Returns a list of NIP-10 marked tags that are also ordered at best effort to support the + * deprecated method of positional tags to maximize backwards compatibility with clients that + * support replies but have not been updated to understand tag markers. + * + * https://github.com/nostr-protocol/nips/blob/master/10.md + * + * The tag to the root of the reply chain goes first. The tag to the reply event being responded + * to goes last. The order for any other tag does not matter, so keep the relative order. + */ +fun List.positionalMarkedTags( + tagName: String, + root: String?, + replyingTo: String?, + directMentions: Set, + forkedFrom: String?, +) = sortedWith { o1, o2 -> + when { + o1 == o2 -> 0 + o1 == root -> -1 // root goes first + o2 == root -> 1 // root goes first + o1 == replyingTo -> 1 // reply event being responded to goes last + o2 == replyingTo -> -1 // reply event being responded to goes last + else -> 0 // keep the relative order for any other tag + } +} + .map { + when (it) { + root -> arrayOf(tagName, it, "", "root") + replyingTo -> arrayOf(tagName, it, "", "reply") + forkedFrom -> arrayOf(tagName, it, "", "fork") + in directMentions -> arrayOf(tagName, it, "", "mention") + else -> arrayOf(tagName, it) + } + } From b694ac7259ef6cd1e7f80b74e387c781772f2554 Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Fri, 1 Mar 2024 21:15:43 -0500 Subject: [PATCH 45/98] Visualizing proposed edits from other people --- .../vitorpamplona/amethyst/model/Account.kt | 4 + .../amethyst/ui/actions/EditPostView.kt | 56 +++++++ .../amethyst/ui/actions/EditPostViewModel.kt | 27 ++++ .../amethyst/ui/note/NoteCompose.kt | 140 +++++++++++++++++- .../amethyst/ui/note/UserProfilePicture.kt | 23 ++- .../amethyst/ui/screen/ThreadFeedView.kt | 12 ++ app/src/main/res/values/strings.xml | 6 + .../events/TextNoteModificationEvent.kt | 19 ++- 8 files changed, 276 insertions(+), 11 deletions(-) diff --git a/app/src/main/java/com/vitorpamplona/amethyst/model/Account.kt b/app/src/main/java/com/vitorpamplona/amethyst/model/Account.kt index 2d1d84a2d..039090ac7 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/model/Account.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/model/Account.kt @@ -1467,6 +1467,8 @@ class Account( fun sendEdit( message: String, originalNote: Note, + notify: HexKey?, + summary: String? = null, relayList: List? = null, ) { if (!isWriteable()) return @@ -1476,6 +1478,8 @@ class Account( TextNoteModificationEvent.create( content = message, eventId = idHex, + notify = notify, + summary = summary, signer = signer, ) { LocalCache.justConsume(it, null) diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/EditPostView.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/EditPostView.kt index e389b7abc..177d858cc 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/EditPostView.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/EditPostView.kt @@ -94,6 +94,7 @@ import com.vitorpamplona.amethyst.ui.components.BechLink import com.vitorpamplona.amethyst.ui.components.InvoiceRequest import com.vitorpamplona.amethyst.ui.components.LoadUrlPreview import com.vitorpamplona.amethyst.ui.components.VideoView +import com.vitorpamplona.amethyst.ui.note.NoteCompose import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel import com.vitorpamplona.amethyst.ui.screen.loggedIn.UserLine import com.vitorpamplona.amethyst.ui.theme.BitcoinOrange @@ -101,7 +102,9 @@ import com.vitorpamplona.amethyst.ui.theme.QuoteBorder import com.vitorpamplona.amethyst.ui.theme.Size10dp 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.placeholderText +import com.vitorpamplona.amethyst.ui.theme.replyModifier import com.vitorpamplona.amethyst.ui.theme.subtleBorder import kotlinx.collections.immutable.toImmutableList import kotlinx.coroutines.Dispatchers @@ -119,6 +122,7 @@ fun EditPostView( nav: (String) -> Unit, ) { val postViewModel: EditPostViewModel = viewModel() + postViewModel.prepare(edit, versionLookingAt, accountViewModel) val context = LocalContext.current @@ -257,6 +261,21 @@ fun EditPostView( .fillMaxWidth() .verticalScroll(scrollState), ) { + postViewModel.editedFromNote?.let { + Row(Modifier.heightIn(max = 200.dp)) { + NoteCompose( + baseNote = it, + makeItShort = true, + unPackReply = false, + isQuotedNote = true, + modifier = MaterialTheme.colorScheme.replyModifier, + accountViewModel = accountViewModel, + nav = nav, + ) + Spacer(modifier = StdVertSpacer) + } + } + MessageField(postViewModel) val myUrlPreview = postViewModel.urlPreview @@ -353,6 +372,43 @@ fun EditPostView( } } } + + /* + Row( + verticalAlignment = Alignment.CenterVertically, + modifier = Modifier.fillMaxWidth().padding(vertical = Size5dp, horizontal = Size10dp), + ) { + Column { + Text( + text = stringResource(R.string.message_to_author), + fontSize = 18.sp, + fontWeight = FontWeight.W500, + ) + + Divider() + + MyTextField( + value = postViewModel.subject, + onValueChange = { postViewModel.updateSubject(it) }, + modifier = Modifier.fillMaxWidth(), + placeholder = { + Text( + text = stringResource(R.string.message_to_author_placeholder), + color = MaterialTheme.colorScheme.placeholderText, + ) + }, + visualTransformation = + UrlUserTagTransformation( + MaterialTheme.colorScheme.primary, + ), + colors = + OutlinedTextFieldDefaults.colors( + unfocusedBorderColor = Color.Transparent, + focusedBorderColor = Color.Transparent, + ), + ) + } + }*/ } } diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/EditPostViewModel.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/EditPostViewModel.kt index 13c188653..b2439727d 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/EditPostViewModel.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/EditPostViewModel.kt @@ -59,6 +59,8 @@ open class EditPostViewModel() : ViewModel() { var editedFromNote: Note? = null + var subject by mutableStateOf(TextFieldValue("")) + var nip94attachments by mutableStateOf>(emptyList()) var nip95attachments by mutableStateOf>>(emptyList()) @@ -80,6 +82,16 @@ open class EditPostViewModel() : ViewModel() { var canAddInvoice by mutableStateOf(false) var wantsInvoice by mutableStateOf(false) + open fun prepare( + edit: Note, + versionLookingAt: Note?, + accountViewModel: AccountViewModel, + ) { + this.accountViewModel = accountViewModel + this.account = accountViewModel.account + this.editedFromNote = edit + } + open fun load( edit: Note, versionLookingAt: Note?, @@ -111,15 +123,29 @@ open class EditPostViewModel() : ViewModel() { account?.sendNip95(it.first, it.second, relayList) } + val notify = + if (editedFromNote?.author?.pubkeyHex == account?.userProfile()?.pubkeyHex) { + null + } else { + // notifies if it is not the logged in user + editedFromNote?.author?.pubkeyHex + } + account?.sendEdit( message = message.text, originalNote = editedFromNote!!, + notify = notify, + summary = subject.text.ifBlank { null }, relayList = relayList, ) cancel() } + open fun updateSubject(it: TextFieldValue) { + subject = it + } + fun upload( galleryUri: Uri, alt: String?, @@ -192,6 +218,7 @@ open class EditPostViewModel() : ViewModel() { open fun cancel() { message = TextFieldValue("") + subject = TextFieldValue("") editedFromNote = null 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 50394e144..9b57df413 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 @@ -48,6 +48,8 @@ import androidx.compose.foundation.shape.CircleShape import androidx.compose.foundation.shape.CutCornerShape import androidx.compose.foundation.text.ClickableText import androidx.compose.foundation.verticalScroll +import androidx.compose.material3.Button +import androidx.compose.material3.Card import androidx.compose.material3.Divider import androidx.compose.material3.HorizontalDivider import androidx.compose.material3.IconButton @@ -85,6 +87,7 @@ import androidx.compose.ui.platform.LocalUriHandler import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.AnnotatedString +import androidx.compose.ui.text.TextStyle import androidx.compose.ui.text.buildAnnotatedString import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.style.TextAlign @@ -114,6 +117,7 @@ import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.model.RelayBriefInfoCache import com.vitorpamplona.amethyst.model.User import com.vitorpamplona.amethyst.service.CachedGeoLocations +import com.vitorpamplona.amethyst.ui.actions.EditPostView import com.vitorpamplona.amethyst.ui.actions.NewRelayListView import com.vitorpamplona.amethyst.ui.components.ClickableUrl import com.vitorpamplona.amethyst.ui.components.CreateClickableTextWithEmoji @@ -183,6 +187,7 @@ import com.vitorpamplona.amethyst.ui.theme.boostedNoteModifier import com.vitorpamplona.amethyst.ui.theme.channelNotePictureModifier import com.vitorpamplona.amethyst.ui.theme.grayText import com.vitorpamplona.amethyst.ui.theme.imageModifier +import com.vitorpamplona.amethyst.ui.theme.innerPostModifier import com.vitorpamplona.amethyst.ui.theme.lessImportantLink import com.vitorpamplona.amethyst.ui.theme.mediumImportanceLink import com.vitorpamplona.amethyst.ui.theme.newItemBackgroundColor @@ -235,6 +240,7 @@ import com.vitorpamplona.quartz.events.RelaySetEvent import com.vitorpamplona.quartz.events.ReportEvent import com.vitorpamplona.quartz.events.RepostEvent import com.vitorpamplona.quartz.events.TextNoteEvent +import com.vitorpamplona.quartz.events.TextNoteModificationEvent import com.vitorpamplona.quartz.events.UserMetadata import com.vitorpamplona.quartz.events.VideoEvent import com.vitorpamplona.quartz.events.VideoHorizontalEvent @@ -291,8 +297,7 @@ fun NoteCompose( nav = nav, ) } else { - LongPressToQuickAction(baseNote = baseNote, accountViewModel = accountViewModel) { showPopup, - -> + LongPressToQuickAction(baseNote = baseNote, accountViewModel = accountViewModel) { showPopup -> BlankNote( remember { modifier.combinedClickable( @@ -1111,6 +1116,8 @@ class EditState() { fun versionId() = modificationToShowIndex + 1 + fun latest() = modificationsList.lastOrNull() + fun nextModification() { if (modificationToShowIndex < 0) { modificationToShowIndex = 0 @@ -1339,6 +1346,17 @@ private fun RenderNoteRow( nav, ) } + is TextNoteModificationEvent -> { + RenderTextModificationEvent( + baseNote, + makeItShort, + canPreview, + backgroundColor, + editState, + accountViewModel, + nav, + ) + } else -> { RenderTextEvent( baseNote, @@ -1467,6 +1485,124 @@ fun RenderTextEvent( } } +@Composable +fun RenderTextModificationEvent( + note: Note, + makeItShort: Boolean, + canPreview: Boolean, + backgroundColor: MutableState, + editStateByAuthor: State>, + accountViewModel: AccountViewModel, + nav: (String) -> Unit, +) { + val noteEvent = note.event as? TextNoteModificationEvent ?: return + val noteAuthor = note.author ?: return + + val isAuthorTheLoggedUser = remember(note.event) { accountViewModel.isLoggedUser(note.author) } + + val editState = + remember { + derivedStateOf { + val loadable = editStateByAuthor.value as? GenericLoadable.Loaded + + val state = EditState() + + val latestChangeByAuthor = + if (loadable != null && loadable.loaded.hasModificationsToShow()) { + loadable.loaded.latest() + } else { + null + } + + state.updateModifications(listOfNotNull(latestChangeByAuthor, note)) + + GenericLoadable.Loaded(state) + } + } + + val wantsToEditPost = + remember { + mutableStateOf(false) + } + + Card( + modifier = MaterialTheme.colorScheme.imageModifier, + ) { + Column(Modifier.fillMaxWidth().padding(Size10dp)) { + Text( + text = stringResource(id = R.string.proposal_to_edit), + style = + TextStyle( + fontSize = 18.sp, + fontWeight = FontWeight.Bold, + ), + ) + + Spacer(modifier = StdVertSpacer) + + noteEvent.summary()?.let { + TranslatableRichTextViewer( + content = it, + canPreview = canPreview && !makeItShort, + modifier = Modifier.fillMaxWidth(), + tags = EmptyTagList, + backgroundColor = backgroundColor, + accountViewModel = accountViewModel, + nav = nav, + ) + Spacer(modifier = StdVertSpacer) + } + + noteEvent.editedNote()?.let { + LoadNote(baseNoteHex = it, accountViewModel = accountViewModel) { baseNote -> + baseNote?.let { + Column( + modifier = + MaterialTheme.colorScheme.innerPostModifier.padding(Size10dp).clickable { + routeFor(baseNote, accountViewModel.userProfile())?.let { nav(it) } + }, + ) { + NoteBody( + baseNote = baseNote, + showAuthorPicture = true, + unPackReply = false, + makeItShort = false, + canPreview = true, + showSecondRow = false, + backgroundColor = backgroundColor, + editState = editState, + accountViewModel = accountViewModel, + nav = nav, + ) + + if (wantsToEditPost.value) { + EditPostView( + onClose = { + wantsToEditPost.value = false + }, + edit = baseNote, + versionLookingAt = note, + accountViewModel = accountViewModel, + nav = nav, + ) + } + } + } + } + } + + Spacer(modifier = StdVertSpacer) + + Button( + onClick = { wantsToEditPost.value = true }, + modifier = Modifier.fillMaxWidth(), + ) { + Text(text = stringResource(id = R.string.accept_the_suggestion)) + } + } + } +} + @Composable fun RenderPoll( note: Note, diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/note/UserProfilePicture.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/UserProfilePicture.kt index 04c1cd08b..50f77676d 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/note/UserProfilePicture.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/UserProfilePicture.kt @@ -581,13 +581,22 @@ fun NoteDropDownMenu( }, ) Divider() - if (state.isLoggedUser && note.event is TextNoteEvent) { - DropdownMenuItem( - text = { Text(stringResource(R.string.edit_post)) }, - onClick = { - wantsToEditPost.value = true - }, - ) + if (note.event is TextNoteEvent) { + if (state.isLoggedUser) { + DropdownMenuItem( + text = { Text(stringResource(R.string.edit_post)) }, + onClick = { + wantsToEditPost.value = true + }, + ) + } else { + DropdownMenuItem( + text = { Text(stringResource(R.string.propose_an_edit)) }, + onClick = { + wantsToEditPost.value = true + }, + ) + } } DropdownMenuItem( text = { Text(stringResource(R.string.broadcast)) }, diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/ThreadFeedView.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/ThreadFeedView.kt index 2ff8f03c4..97b2c8258 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/ThreadFeedView.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/ThreadFeedView.kt @@ -131,6 +131,7 @@ import com.vitorpamplona.amethyst.ui.note.RenderPoll import com.vitorpamplona.amethyst.ui.note.RenderPostApproval import com.vitorpamplona.amethyst.ui.note.RenderRepost import com.vitorpamplona.amethyst.ui.note.RenderTextEvent +import com.vitorpamplona.amethyst.ui.note.RenderTextModificationEvent import com.vitorpamplona.amethyst.ui.note.VideoDisplay import com.vitorpamplona.amethyst.ui.note.observeEdits import com.vitorpamplona.amethyst.ui.note.showAmount @@ -174,6 +175,7 @@ import com.vitorpamplona.quartz.events.PinListEvent import com.vitorpamplona.quartz.events.PollNoteEvent import com.vitorpamplona.quartz.events.RelaySetEvent import com.vitorpamplona.quartz.events.RepostEvent +import com.vitorpamplona.quartz.events.TextNoteModificationEvent import com.vitorpamplona.quartz.events.VideoEvent import com.vitorpamplona.quartz.events.WikiNoteEvent import kotlinx.collections.immutable.toImmutableList @@ -568,6 +570,16 @@ fun NoteMaster( ) } else if (noteEvent is RepostEvent || noteEvent is GenericRepostEvent) { RenderRepost(baseNote, backgroundColor, accountViewModel, nav) + } else if (noteEvent is TextNoteModificationEvent) { + RenderTextModificationEvent( + note = baseNote, + makeItShort = false, + canPreview = true, + backgroundColor, + editState, + accountViewModel, + nav, + ) } else if (noteEvent is PollNoteEvent) { val canPreview = note.author == account.userProfile() || diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 74e89ae60..845fe0b15 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -55,6 +55,7 @@ original Quote Fork + Propose an Edit New Amount in Sats Add "replying to " @@ -795,4 +796,9 @@ There\'s proof this post was signed sometime before %1$s. The proof was stamped in the Bitcoin blockchain at that date and time. Edit Post + Proposal to improve your post + Summary of changes + Quick fixes... + + Accept the Suggestion diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/events/TextNoteModificationEvent.kt b/quartz/src/main/java/com/vitorpamplona/quartz/events/TextNoteModificationEvent.kt index c3dd0a9ac..0094c5983 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/events/TextNoteModificationEvent.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/events/TextNoteModificationEvent.kt @@ -36,6 +36,8 @@ class TextNoteModificationEvent( ) : Event(id, pubKey, createdAt, KIND, tags, content, sig) { fun editedNote() = firstTaggedEvent() + fun summary() = tags.firstOrNull { it.size > 1 && it[0] == "summary" }?.get(1) + companion object { const val KIND = 1010 const val ALT = "Content Change Event" @@ -43,12 +45,25 @@ class TextNoteModificationEvent( fun create( content: String, eventId: HexKey, + notify: HexKey?, + summary: String?, signer: NostrSigner, createdAt: Long = TimeUtils.now(), onReady: (TextNoteModificationEvent) -> Unit, ) { - val tags = arrayOf(arrayOf("e", eventId), arrayOf("alt", ALT)) - signer.sign(createdAt, KIND, tags, content, onReady) + val tags = mutableListOf(arrayOf("e", eventId)) + + notify?.let { + tags.add(arrayOf("p", it)) + } + + summary?.let { + tags.add(arrayOf("summary", it)) + } + + tags.add(arrayOf("alt", ALT)) + + signer.sign(createdAt, KIND, tags.toTypedArray(), content, onReady) } } } From 861ad218c813191d835096531488094c85b9938a Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Sat, 2 Mar 2024 12:03:16 -0500 Subject: [PATCH 46/98] Fixes bug with NIP-11s with null kinds --- .../com/vitorpamplona/quartz/encoders/Nip11RelayInformation.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/encoders/Nip11RelayInformation.kt b/quartz/src/main/java/com/vitorpamplona/quartz/encoders/Nip11RelayInformation.kt index 58f43b9dc..c2258dad6 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/encoders/Nip11RelayInformation.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/encoders/Nip11RelayInformation.kt @@ -86,7 +86,7 @@ class RelayInformationLimitation( ) class RelayInformationRetentionData( - val kinds: ArrayList, + val kinds: ArrayList?, val tiem: Int?, val count: Int?, ) From 6725114b485bcd5eac8983c6b36cbfddfba93de1 Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Sat, 2 Mar 2024 12:35:04 -0500 Subject: [PATCH 47/98] Avoids changing the showhidden value all the time, triggering recompositions. --- .../java/com/vitorpamplona/amethyst/ui/screen/FeedState.kt | 6 ++++-- .../com/vitorpamplona/amethyst/ui/screen/FeedViewModel.kt | 4 +++- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/FeedState.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/FeedState.kt index ea9d5ac64..4061e144c 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/FeedState.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/FeedState.kt @@ -27,8 +27,10 @@ import kotlinx.collections.immutable.ImmutableList sealed class FeedState { object Loading : FeedState() - class Loaded(val feed: MutableState>, val showHidden: MutableState) : - FeedState() + class Loaded( + val feed: MutableState>, + val showHidden: MutableState, + ) : FeedState() object Empty : FeedState() diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/FeedViewModel.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/FeedViewModel.kt index d2610f923..b11e369ba 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/FeedViewModel.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/FeedViewModel.kt @@ -328,7 +328,9 @@ abstract class FeedViewModel(val localFilter: FeedFilter) : _feedContent.update { FeedState.Empty } } else if (currentState is FeedState.Loaded) { // updates the current list - currentState.showHidden.value = localFilter.showHiddenKey() + if (currentState.showHidden.value != localFilter.showHiddenKey()) { + currentState.showHidden.value = localFilter.showHiddenKey() + } currentState.feed.value = notes } else { _feedContent.update { From 16c171ec40a2b62428c78ed133a6458f99180115 Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Sat, 2 Mar 2024 12:37:02 -0500 Subject: [PATCH 48/98] Fixes quote-notes and reposted notes partially disappearing when they contain hidden users or words. --- .../amethyst/ui/note/BlankNote.kt | 92 +++++++++++++++++++ .../amethyst/ui/note/NoteCompose.kt | 15 ++- app/src/main/res/values/strings.xml | 1 + 3 files changed, 105 insertions(+), 3 deletions(-) diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/note/BlankNote.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/BlankNote.kt index 81823b0c9..22e42d5aa 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/note/BlankNote.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/BlankNote.kt @@ -37,15 +37,28 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.res.stringResource +import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import com.vitorpamplona.amethyst.R import com.vitorpamplona.amethyst.model.Note +import com.vitorpamplona.amethyst.ui.components.mockAccountViewModel import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel import com.vitorpamplona.amethyst.ui.theme.ButtonBorder import com.vitorpamplona.amethyst.ui.theme.ButtonPadding import com.vitorpamplona.amethyst.ui.theme.DividerThickness import com.vitorpamplona.amethyst.ui.theme.Size35dp +import com.vitorpamplona.amethyst.ui.theme.ThemeComparisonColumn import kotlinx.collections.immutable.ImmutableSet +import kotlinx.collections.immutable.persistentSetOf + +@Composable +@Preview +fun BlankNotePreview() { + ThemeComparisonColumn( + onDark = { BlankNote() }, + onLight = { BlankNote() }, + ) +} @Composable fun BlankNote( @@ -85,6 +98,32 @@ fun BlankNote( } } +@Composable +@Preview +fun HiddenNotePreview() { + val accountViewModel = mockAccountViewModel() + val nav: (String) -> Unit = {} + + ThemeComparisonColumn( + onDark = { + HiddenNote( + reports = persistentSetOf(), + isHiddenAuthor = true, + accountViewModel = accountViewModel, + nav = nav, + ) {} + }, + onLight = { + HiddenNote( + reports = persistentSetOf(), + isHiddenAuthor = true, + accountViewModel = accountViewModel, + nav = nav, + ) {} + }, + ) +} + @OptIn(ExperimentalLayoutApi::class) @Composable fun HiddenNote( @@ -149,3 +188,56 @@ fun HiddenNote( ) } } + +@Preview +@Composable +fun HiddenNoteByMePreview() { + ThemeComparisonColumn( + onDark = { HiddenNoteByMe {} }, + onLight = { HiddenNoteByMe {} }, + ) +} + +@Composable +fun HiddenNoteByMe( + modifier: Modifier = Modifier, + isQuote: Boolean = false, + onClick: () -> Unit, +) { + Column(modifier = modifier, horizontalAlignment = Alignment.CenterHorizontally) { + Row( + modifier = Modifier.padding(horizontal = 20.dp), + horizontalArrangement = Arrangement.Center, + verticalAlignment = Alignment.CenterVertically, + ) { + Column( + horizontalAlignment = Alignment.CenterHorizontally, + modifier = Modifier.padding(30.dp), + ) { + Text( + text = stringResource(R.string.post_was_hidden), + color = Color.Gray, + ) + + Button( + modifier = Modifier.padding(top = 10.dp), + onClick = onClick, + shape = ButtonBorder, + colors = + ButtonDefaults.buttonColors( + contentColor = MaterialTheme.colorScheme.primary, + ), + contentPadding = ButtonPadding, + ) { + Text(text = stringResource(R.string.show_anyway), color = Color.White) + } + } + } + + if (!isQuote) { + Divider( + thickness = DividerThickness, + ) + } + } +} 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 9b57df413..c4dc75fc9 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 @@ -359,8 +359,13 @@ fun CheckHiddenNoteCompose( } .observeAsState(accountViewModel.isNoteHidden(note)) + val showAnyway = + remember { + mutableStateOf(false) + } + Crossfade(targetState = isHidden, label = "CheckHiddenNoteCompose") { - if (!it) { + if (!it || showAnyway.value) { LoadedNoteCompose( note = note, routeForLastRead = routeForLastRead, @@ -374,6 +379,11 @@ fun CheckHiddenNoteCompose( accountViewModel = accountViewModel, nav = nav, ) + } else if (isQuotedNote || isBoostedNote) { + HiddenNoteByMe( + isQuote = true, + onClick = { showAnyway.value = true }, + ) } } } @@ -441,8 +451,7 @@ fun RenderReportState( ) { var showReportedNote by remember(note) { mutableStateOf(false) } - Crossfade(targetState = !state.isAcceptable && !showReportedNote, label = "RenderReportState") { - showHiddenNote -> + Crossfade(targetState = !state.isAcceptable && !showReportedNote, label = "RenderReportState") { showHiddenNote -> if (showHiddenNote) { HiddenNote( state.relevantReports, diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 845fe0b15..9de79d5d3 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -7,6 +7,7 @@ Your Profile Picture Scan QR Show Anyway + This post was hidden because it mentions your hidden users or words Post was muted or reported by Event is loading or can\'t be found in your relay list Channel Image From a30142191566efb862ec319073583deac70c6009 Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Sat, 2 Mar 2024 15:57:49 -0500 Subject: [PATCH 49/98] Adds Home-only empty feed screen. --- .../amethyst/ui/screen/ChatroomFeedView.kt | 2 +- .../ui/screen/ChatroomListFeedView.kt | 2 +- .../amethyst/ui/screen/FeedView.kt | 53 ++++++----------- .../amethyst/ui/screen/StringFeedView.kt | 2 +- .../amethyst/ui/screen/UserFeedView.kt | 2 +- .../ui/screen/loggedIn/DiscoverScreen.kt | 4 +- .../ui/screen/loggedIn/HiddenUsersScreen.kt | 4 +- .../amethyst/ui/screen/loggedIn/HomeScreen.kt | 59 ++++++++++++++++++- .../ui/screen/loggedIn/VideoScreen.kt | 4 +- 9 files changed, 85 insertions(+), 47 deletions(-) diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/ChatroomFeedView.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/ChatroomFeedView.kt index bf5720b83..da5cf1f55 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/ChatroomFeedView.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/ChatroomFeedView.kt @@ -54,7 +54,7 @@ fun RefreshingChatroomFeedView( scrollStateKey: String? = null, enablePullRefresh: Boolean = true, ) { - RefresheableView(viewModel, enablePullRefresh) { + RefresheableBox(viewModel, enablePullRefresh) { SaveableFeedState(viewModel, scrollStateKey) { listState -> RenderChatroomFeedView( viewModel, diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/ChatroomListFeedView.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/ChatroomListFeedView.kt index ffa1a6f7e..43fa223fc 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/ChatroomListFeedView.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/ChatroomListFeedView.kt @@ -45,7 +45,7 @@ fun ChatroomListFeedView( nav: (String) -> Unit, markAsRead: MutableState, ) { - RefresheableView(viewModel, true) { CrossFadeState(viewModel, accountViewModel, nav, markAsRead) } + RefresheableBox(viewModel, true) { CrossFadeState(viewModel, accountViewModel, nav, markAsRead) } } @Composable diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/FeedView.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/FeedView.kt index f1064990c..920977642 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/FeedView.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/FeedView.kt @@ -27,6 +27,7 @@ import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxHeight import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth @@ -56,6 +57,7 @@ import com.vitorpamplona.amethyst.R import com.vitorpamplona.amethyst.ui.note.NoteCompose import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel import com.vitorpamplona.amethyst.ui.theme.FeedPadding +import com.vitorpamplona.amethyst.ui.theme.StdVertSpacer import kotlin.time.ExperimentalTime @Composable @@ -67,13 +69,15 @@ fun RefresheableFeedView( accountViewModel: AccountViewModel, nav: (String) -> Unit, ) { - RefresheableView(viewModel, enablePullRefresh) { - SaveableFeedState(viewModel, routeForLastRead, scrollStateKey, accountViewModel, nav) + RefresheableBox(viewModel, enablePullRefresh) { + SaveableFeedState(viewModel, scrollStateKey) { listState -> + RenderFeedState(viewModel, accountViewModel, listState, nav, routeForLastRead) + } } } @Composable -fun RefresheableView( +fun RefresheableBox( viewModel: InvalidatableViewModel, enablePullRefresh: Boolean = true, content: @Composable () -> Unit, @@ -108,19 +112,6 @@ fun RefresheableView( } } -@Composable -private fun SaveableFeedState( - viewModel: FeedViewModel, - routeForLastRead: String?, - scrollStateKey: String? = null, - accountViewModel: AccountViewModel, - nav: (String) -> Unit, -) { - SaveableFeedState(viewModel, scrollStateKey) { listState -> - RenderFeed(viewModel, accountViewModel, listState, nav, routeForLastRead) - } -} - @Composable fun SaveableFeedState( viewModel: FeedViewModel, @@ -158,12 +149,16 @@ fun SaveableGridFeedState( } @Composable -private fun RenderFeed( +fun RenderFeedState( viewModel: FeedViewModel, accountViewModel: AccountViewModel, listState: LazyListState, nav: (String) -> Unit, routeForLastRead: String?, + onLoaded: @Composable (FeedState.Loaded) -> Unit = { FeedLoaded(it, listState, routeForLastRead, accountViewModel, nav) }, + onEmpty: @Composable () -> Unit = { FeedEmpty { viewModel.invalidateData() } }, + onError: @Composable (String) -> Unit = { FeedError(it) { viewModel.invalidateData() } }, + onLoading: @Composable () -> Unit = { LoadingFeed() }, ) { val feedState by viewModel.feedContent.collectAsStateWithLifecycle() @@ -172,24 +167,10 @@ private fun RenderFeed( animationSpec = tween(durationMillis = 100), ) { state -> when (state) { - is FeedState.Empty -> { - FeedEmpty { viewModel.invalidateData() } - } - is FeedState.FeedError -> { - FeedError(state.errorMessage) { viewModel.invalidateData() } - } - is FeedState.Loaded -> { - FeedLoaded( - state = state, - listState = listState, - routeForLastRead = routeForLastRead, - accountViewModel = accountViewModel, - nav = nav, - ) - } - is FeedState.Loading -> { - LoadingFeed() - } + is FeedState.Empty -> onEmpty() + is FeedState.FeedError -> onError(state.errorMessage) + is FeedState.Loaded -> onLoaded(state) + is FeedState.Loading -> onLoading() } } } @@ -277,6 +258,7 @@ fun FeedError( verticalArrangement = Arrangement.Center, ) { Text("${stringResource(R.string.error_loading_replies)} $errorMessage") + Spacer(modifier = StdVertSpacer) Button( modifier = Modifier.align(Alignment.CenterHorizontally), onClick = onRefresh, @@ -294,6 +276,7 @@ fun FeedEmpty(onRefresh: () -> Unit) { verticalArrangement = Arrangement.Center, ) { Text(stringResource(R.string.feed_is_empty)) + Spacer(modifier = StdVertSpacer) OutlinedButton(onClick = onRefresh) { Text(text = stringResource(R.string.refresh)) } } } diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/StringFeedView.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/StringFeedView.kt index f4c355fdc..e1f3a41d6 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/StringFeedView.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/StringFeedView.kt @@ -45,7 +45,7 @@ fun RefreshingFeedStringFeedView( enablePullRefresh: Boolean = true, inner: @Composable (String) -> Unit, ) { - RefresheableView(viewModel, enablePullRefresh) { StringFeedView(viewModel, inner = inner) } + RefresheableBox(viewModel, enablePullRefresh) { StringFeedView(viewModel, inner = inner) } } @Composable diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/UserFeedView.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/UserFeedView.kt index 6fc453d92..bc52f5c4a 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/UserFeedView.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/UserFeedView.kt @@ -39,7 +39,7 @@ fun RefreshingFeedUserFeedView( nav: (String) -> Unit, enablePullRefresh: Boolean = true, ) { - RefresheableView(viewModel, enablePullRefresh) { UserFeedView(viewModel, accountViewModel, nav) } + RefresheableBox(viewModel, enablePullRefresh) { UserFeedView(viewModel, accountViewModel, nav) } } @Composable diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/DiscoverScreen.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/DiscoverScreen.kt index 10ff2a720..a5f134cf1 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/DiscoverScreen.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/DiscoverScreen.kt @@ -69,7 +69,7 @@ import com.vitorpamplona.amethyst.ui.screen.NostrDiscoverCommunityFeedViewModel import com.vitorpamplona.amethyst.ui.screen.NostrDiscoverLiveFeedViewModel import com.vitorpamplona.amethyst.ui.screen.NostrDiscoverMarketplaceFeedViewModel import com.vitorpamplona.amethyst.ui.screen.PagerStateKeys -import com.vitorpamplona.amethyst.ui.screen.RefresheableView +import com.vitorpamplona.amethyst.ui.screen.RefresheableBox import com.vitorpamplona.amethyst.ui.screen.SaveableFeedState import com.vitorpamplona.amethyst.ui.screen.SaveableGridFeedState import com.vitorpamplona.amethyst.ui.screen.ScrollStateKeys @@ -196,7 +196,7 @@ private fun DiscoverPages( } HorizontalPager(state = pagerState) { page -> - RefresheableView(tabs[page].viewModel, true) { + RefresheableBox(tabs[page].viewModel, true) { if (tabs[page].viewModel is NostrDiscoverMarketplaceFeedViewModel) { SaveableGridFeedState(tabs[page].viewModel, scrollStateKey = tabs[page].scrollStateKey) { listState -> diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/HiddenUsersScreen.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/HiddenUsersScreen.kt index 1381ee22b..73346dd8d 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/HiddenUsersScreen.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/HiddenUsersScreen.kt @@ -71,7 +71,7 @@ import com.vitorpamplona.amethyst.ui.elements.AddButton import com.vitorpamplona.amethyst.ui.screen.NostrHiddenAccountsFeedViewModel import com.vitorpamplona.amethyst.ui.screen.NostrHiddenWordsFeedViewModel import com.vitorpamplona.amethyst.ui.screen.NostrSpammerAccountsFeedViewModel -import com.vitorpamplona.amethyst.ui.screen.RefresheableView +import com.vitorpamplona.amethyst.ui.screen.RefresheableBox import com.vitorpamplona.amethyst.ui.screen.RefreshingFeedUserFeedView import com.vitorpamplona.amethyst.ui.screen.StringFeedView import com.vitorpamplona.amethyst.ui.screen.UserFeedViewModel @@ -216,7 +216,7 @@ private fun HiddenWordsFeed( hiddenWordsViewModel: NostrHiddenWordsFeedViewModel, accountViewModel: AccountViewModel, ) { - RefresheableView(hiddenWordsViewModel, false) { + RefresheableBox(hiddenWordsViewModel, false) { StringFeedView( hiddenWordsViewModel, post = { AddMuteWordTextField(accountViewModel) }, diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/HomeScreen.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/HomeScreen.kt index 7773764f6..24c5a7ede 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/HomeScreen.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/HomeScreen.kt @@ -22,11 +22,15 @@ package com.vitorpamplona.amethyst.ui.screen.loggedIn import androidx.compose.animation.Crossfade import androidx.compose.foundation.ExperimentalFoundationApi +import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxHeight +import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.pager.HorizontalPager import androidx.compose.foundation.pager.PagerState import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.OutlinedButton import androidx.compose.material3.Tab import androidx.compose.material3.TabRow import androidx.compose.material3.Text @@ -39,9 +43,11 @@ import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalLifecycleOwner import androidx.compose.ui.res.stringResource +import androidx.compose.ui.tooling.preview.Preview import androidx.lifecycle.Lifecycle import androidx.lifecycle.LifecycleEventObserver import androidx.lifecycle.compose.collectAsStateWithLifecycle @@ -54,10 +60,14 @@ import com.vitorpamplona.amethyst.ui.screen.FeedViewModel import com.vitorpamplona.amethyst.ui.screen.NostrHomeFeedViewModel import com.vitorpamplona.amethyst.ui.screen.NostrHomeRepliesFeedViewModel import com.vitorpamplona.amethyst.ui.screen.PagerStateKeys -import com.vitorpamplona.amethyst.ui.screen.RefresheableFeedView +import com.vitorpamplona.amethyst.ui.screen.RefresheableBox +import com.vitorpamplona.amethyst.ui.screen.RenderFeedState +import com.vitorpamplona.amethyst.ui.screen.SaveableFeedState import com.vitorpamplona.amethyst.ui.screen.ScrollStateKeys import com.vitorpamplona.amethyst.ui.screen.rememberForeverPagerState +import com.vitorpamplona.amethyst.ui.theme.StdVertSpacer import com.vitorpamplona.amethyst.ui.theme.TabRowHeight +import com.vitorpamplona.amethyst.ui.theme.ThemeComparisonRow import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.toImmutableList import kotlinx.coroutines.launch @@ -181,7 +191,7 @@ private fun HomePages( } HorizontalPager(state = pagerState, userScrollEnabled = false) { page -> - RefresheableFeedView( + HomeFeeds( viewModel = tabs[page].viewModel, routeForLastRead = tabs[page].routeForLastRead, scrollStateKey = tabs[page].scrollStateKey, @@ -191,6 +201,51 @@ private fun HomePages( } } +@Composable +fun HomeFeeds( + viewModel: FeedViewModel, + routeForLastRead: String?, + enablePullRefresh: Boolean = true, + scrollStateKey: String? = null, + accountViewModel: AccountViewModel, + nav: (String) -> Unit, +) { + RefresheableBox(viewModel, enablePullRefresh) { + SaveableFeedState(viewModel, scrollStateKey) { listState -> + RenderFeedState( + viewModel = viewModel, + accountViewModel = accountViewModel, + listState = listState, + nav = nav, + routeForLastRead = routeForLastRead, + onEmpty = { HomeFeedEmpty { viewModel.invalidateData() } }, + ) + } + } +} + +@Preview +@Composable +fun HomeFeedEmptyPreview() { + ThemeComparisonRow( + onDark = { HomeFeedEmpty {} }, + onLight = { HomeFeedEmpty {} }, + ) +} + +@Composable +fun HomeFeedEmpty(onRefresh: () -> Unit) { + Column( + Modifier.fillMaxSize(), + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.Center, + ) { + Text(stringResource(R.string.feed_is_empty)) + Spacer(modifier = StdVertSpacer) + OutlinedButton(onClick = onRefresh) { Text(text = stringResource(R.string.refresh)) } + } +} + @Composable fun CheckIfUrlIsOnline( url: String, diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/VideoScreen.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/VideoScreen.kt index 1dface6d1..0e5b4eeb4 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/VideoScreen.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/VideoScreen.kt @@ -86,7 +86,7 @@ import com.vitorpamplona.amethyst.ui.screen.FeedState import com.vitorpamplona.amethyst.ui.screen.FeedViewModel import com.vitorpamplona.amethyst.ui.screen.LoadingFeed import com.vitorpamplona.amethyst.ui.screen.NostrVideoFeedViewModel -import com.vitorpamplona.amethyst.ui.screen.RefresheableView +import com.vitorpamplona.amethyst.ui.screen.RefresheableBox import com.vitorpamplona.amethyst.ui.screen.ScrollStateKeys import com.vitorpamplona.amethyst.ui.screen.rememberForeverPagerState import com.vitorpamplona.amethyst.ui.theme.Size35Modifier @@ -214,7 +214,7 @@ private fun LoadedState( WatchScrollToTop(videoFeedView, pagerState) - RefresheableView(viewModel = videoFeedView) { + RefresheableBox(viewModel = videoFeedView) { SlidingCarousel( state.feed, pagerState, From 4fb68dd0149ad47c5b443638994dedec5bcab14e Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Sat, 2 Mar 2024 16:11:01 -0500 Subject: [PATCH 50/98] Fixes content title for the video playback notification --- .../vitorpamplona/amethyst/ui/components/RichTextViewer.kt | 6 +++++- .../java/com/vitorpamplona/amethyst/ui/note/NoteCompose.kt | 4 ++-- .../amethyst/benchmark/RichTextParserBenchmark.kt | 1 + .../com/vitorpamplona/amethyst/commons/RichTextParser.kt | 7 ++++--- 4 files changed, 12 insertions(+), 6 deletions(-) diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/components/RichTextViewer.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/components/RichTextViewer.kt index bd3ffa1ea..97f90b808 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/components/RichTextViewer.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/components/RichTextViewer.kt @@ -497,7 +497,11 @@ private fun RenderContentAsMarkdown( ZoomableContentView( content = remember(destination, tags) { - RichTextParser().parseMediaUrl(destination, tags ?: EmptyTagList) ?: MediaUrlImage(url = destination) + RichTextParser().parseMediaUrl( + destination, + tags ?: EmptyTagList, + title.ifEmpty { null } ?: content, + ) ?: MediaUrlImage(url = destination, description = title.ifEmpty { null } ?: content) }, roundedCorner = true, accountViewModel = accountViewModel, 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 c4dc75fc9..ca4517c3e 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 @@ -3575,7 +3575,7 @@ fun FileHeaderDisplay( val blurHash = event.blurhash() val hash = event.hash() val dimensions = event.dimensions() - val description = event.alt() ?: event.content + val description = event.content.ifEmpty { null } ?: event.alt() val isImage = RichTextParser.isImageUrl(fullUrl) val uri = note.toNostrUri() @@ -3634,7 +3634,7 @@ fun VideoDisplay( val blurHash = event.blurhash() val hash = event.hash() val dimensions = event.dimensions() - val description = event.alt() ?: event.content + val description = event.content.ifBlank { null } ?: event.alt() val isImage = RichTextParser.isImageUrl(fullUrl) val uri = note.toNostrUri() diff --git a/benchmark/src/androidTest/java/com/vitorpamplona/amethyst/benchmark/RichTextParserBenchmark.kt b/benchmark/src/androidTest/java/com/vitorpamplona/amethyst/benchmark/RichTextParserBenchmark.kt index c3408f582..5bf80dafb 100644 --- a/benchmark/src/androidTest/java/com/vitorpamplona/amethyst/benchmark/RichTextParserBenchmark.kt +++ b/benchmark/src/androidTest/java/com/vitorpamplona/amethyst/benchmark/RichTextParserBenchmark.kt @@ -49,6 +49,7 @@ class RichTextParserBenchmark { RichTextParser().parseMediaUrl( "https://github.com/vitorpamplona/amethyst/releases/download/v0.83.10/amethyst-googleplay-universal-v0.83.10.apk", EmptyTagList, + null, ), ) } 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 bb205711b..dcd6b6eb4 100644 --- a/commons/src/main/java/com/vitorpamplona/amethyst/commons/RichTextParser.kt +++ b/commons/src/main/java/com/vitorpamplona/amethyst/commons/RichTextParser.kt @@ -45,6 +45,7 @@ class RichTextParser() { fun parseMediaUrl( fullUrl: String, eventTags: ImmutableListOfLists, + description: String?, ): MediaUrlContent? { val removedParamsFromUrl = removeQueryParamsForExtensionComparison(fullUrl) return if (imageExtensions.any { removedParamsFromUrl.endsWith(it) }) { @@ -53,7 +54,7 @@ class RichTextParser() { MediaUrlImage( url = fullUrl, - description = frags[FileHeaderEvent.ALT] ?: tags[FileHeaderEvent.ALT], + description = description ?: frags[FileHeaderEvent.ALT] ?: tags[FileHeaderEvent.ALT], hash = frags[FileHeaderEvent.HASH] ?: tags[FileHeaderEvent.HASH], blurhash = frags[FileHeaderEvent.BLUR_HASH] ?: tags[FileHeaderEvent.BLUR_HASH], dim = frags[FileHeaderEvent.DIMENSION] ?: tags[FileHeaderEvent.DIMENSION], @@ -64,7 +65,7 @@ class RichTextParser() { val tags = Nip92MediaAttachments().parse(fullUrl, eventTags.lists) MediaUrlVideo( url = fullUrl, - description = frags[FileHeaderEvent.ALT] ?: tags[FileHeaderEvent.ALT], + description = description ?: frags[FileHeaderEvent.ALT] ?: tags[FileHeaderEvent.ALT], hash = frags[FileHeaderEvent.HASH] ?: tags[FileHeaderEvent.HASH], blurhash = frags[FileHeaderEvent.BLUR_HASH] ?: tags[FileHeaderEvent.BLUR_HASH], dim = frags[FileHeaderEvent.DIMENSION] ?: tags[FileHeaderEvent.DIMENSION], @@ -106,7 +107,7 @@ class RichTextParser() { val urlSet = parseValidUrls(content) val imagesForPager = - urlSet.mapNotNull { fullUrl -> parseMediaUrl(fullUrl, tags) }.associateBy { it.url } + urlSet.mapNotNull { fullUrl -> parseMediaUrl(fullUrl, tags, content) }.associateBy { it.url } val imageList = imagesForPager.values.toList() val emojiMap = Nip30CustomEmoji.createEmojiMap(tags) From 3ddd06fafcf8edccfd8cdd44fcc3ff08da7dd0e0 Mon Sep 17 00:00:00 2001 From: Crowdin Bot Date: Sat, 2 Mar 2024 21:13:28 +0000 Subject: [PATCH 51/98] New Crowdin translations by GitHub Action --- app/src/main/res/values-fr/strings.xml | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/app/src/main/res/values-fr/strings.xml b/app/src/main/res/values-fr/strings.xml index 07ed62499..f5f0ae9d2 100644 --- a/app/src/main/res/values-fr/strings.xml +++ b/app/src/main/res/values-fr/strings.xml @@ -48,9 +48,11 @@ Boost boosté modifié + éditer #%1$s original Citation Fork + Proposer une Modification Nouveau montant en Sats Ajouter "répondre à " @@ -673,4 +675,8 @@ Preuve d\'horodatage Il y a une preuve que ce message a été signé avant %1$s. La preuve a été estampillée dans la blockchain Bitcoin à cette date et à cette heure. Modifier le Message + Proposition pour améliorer votre publication + Récapitulatif des modifications + Corrections rapides ... + Accepter la Suggestion From 5342c4cfba7adc796c9ae7696486b5efcfe28360 Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Sat, 2 Mar 2024 16:18:11 -0500 Subject: [PATCH 52/98] updates mockk --- gradle/libs.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 88a75dab3..ea4f8fd15 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -29,7 +29,7 @@ lazysodiumAndroid = "5.1.0" lightcompressor = "1.3.2" markdown = "48702a8ced" media3 = "1.2.1" -mockk = "1.13.9" +mockk = "1.13.10" navigationCompose = "2.7.7" okhttp = "5.0.0-alpha.12" runner = "1.5.2" From fcd61d87be1c98de7e82883ecc3eca9d522eb5f7 Mon Sep 17 00:00:00 2001 From: David Kaspar Date: Sun, 3 Mar 2024 09:28:10 +0000 Subject: [PATCH 53/98] Updated translations. DE, SV, PT --- app/src/main/res/values-cs/strings.xml | 7 +++++++ app/src/main/res/values-de/strings.xml | 7 +++++++ app/src/main/res/values-pt-rBR/strings.xml | 7 +++++++ app/src/main/res/values-sv-rSE/strings.xml | 7 +++++++ app/src/main/res/values/strings.xml | 2 +- 5 files changed, 29 insertions(+), 1 deletion(-) diff --git a/app/src/main/res/values-cs/strings.xml b/app/src/main/res/values-cs/strings.xml index 44a4af3ae..69576a81f 100644 --- a/app/src/main/res/values-cs/strings.xml +++ b/app/src/main/res/values-cs/strings.xml @@ -670,4 +670,11 @@ Důkaz časového razítka Existuje důkaz, že tento příspěvek byl podepsán někdy před %1$s. Důkaz byl označen v Bitcoin blockchainu v tomto datu a čase. Upravit příspěvek + Návrh na vylepšení vašeho příspěvku + Souhrn změn + Rychlé opravy… + Přijmout návrhy + Navrhnout úpravu + Tento příspěvek byl skryt, protože zmiňuje vaše skryté uživatele nebo slova + úprava #%1$s diff --git a/app/src/main/res/values-de/strings.xml b/app/src/main/res/values-de/strings.xml index ea20aafb5..553ed6b73 100644 --- a/app/src/main/res/values-de/strings.xml +++ b/app/src/main/res/values-de/strings.xml @@ -675,4 +675,11 @@ anz der Bedingungen ist erforderlich Zeitstempel Beweis Es gibt einen Beweis, dass dieser Beitrag irgendwann vor %1$s signiert wurde. Der Beweis wurde zu diesem Datum und Uhrzeit in der Bitcoin-Blockchain gestempelt. Beitrag bearbeiten + Vorschlag zur Verbesserung Ihres Beitrags + Zusammenfassung der Änderungen + Schnelle Korrekturen… + Den Vorschlag annehmen + Eine Bearbeitung vorschlagen + Dieser Beitrag wurde ausgeblendet, weil er Ihre ausgeblendeten Benutzer oder Wörter erwähnt + Bearbeitung #%1$s diff --git a/app/src/main/res/values-pt-rBR/strings.xml b/app/src/main/res/values-pt-rBR/strings.xml index c9ef64ca2..b4a2da339 100644 --- a/app/src/main/res/values-pt-rBR/strings.xml +++ b/app/src/main/res/values-pt-rBR/strings.xml @@ -669,4 +669,11 @@ Prova de Carimbo de data/hora Há prova de que esta postagem foi assinada antes de %1$s. A prova foi carimbada no blockchain do Bitcoin naquela data e hora. Editar postagem + Proposta para melhorar sua postagem + Resumo das alterações + Correções rápidas… + Aceitar a Sugestão + Propor uma Edição + Esta postagem foi ocultada porque menciona seus usuários ou palavras ocultas + edição #%1$s diff --git a/app/src/main/res/values-sv-rSE/strings.xml b/app/src/main/res/values-sv-rSE/strings.xml index bbd04976d..5ec0edc08 100644 --- a/app/src/main/res/values-sv-rSE/strings.xml +++ b/app/src/main/res/values-sv-rSE/strings.xml @@ -668,4 +668,11 @@ Tidsstämpel Bevis Det finns bevis på att detta inlägg signerades någon gång före %1$s. Beviset stämplades i Bitcoin-blockchainen vid det datumet och den tiden. Redigera inlägg + Förslag till att förbättra ditt inlägg + Sammanfattning av ändringar + Snabba fixar… + Acceptera förslaget + Föreslå en redigering + Detta inlägg doldes eftersom det nämner dina dolda användare eller ord + redigering #%1$s diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 9de79d5d3..d21e60ae0 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -799,7 +799,7 @@ Edit Post Proposal to improve your post Summary of changes - Quick fixes... + Quick fixes… Accept the Suggestion From 5f53fc6aa9c4dd510502f59734f6ced4dcc7de9d Mon Sep 17 00:00:00 2001 From: Crowdin Bot Date: Sun, 3 Mar 2024 16:07:20 +0000 Subject: [PATCH 54/98] New Crowdin translations by GitHub Action --- app/src/main/res/values-cs/strings.xml | 6 +++--- app/src/main/res/values-de/strings.xml | 6 +++--- app/src/main/res/values-fr/strings.xml | 5 +++-- app/src/main/res/values-pt-rBR/strings.xml | 6 +++--- app/src/main/res/values-sv-rSE/strings.xml | 6 +++--- 5 files changed, 15 insertions(+), 14 deletions(-) diff --git a/app/src/main/res/values-cs/strings.xml b/app/src/main/res/values-cs/strings.xml index 69576a81f..af9e6a1cb 100644 --- a/app/src/main/res/values-cs/strings.xml +++ b/app/src/main/res/values-cs/strings.xml @@ -6,6 +6,7 @@ Profilový obrázek Skenovat QR kód Zobrazit přesto + Tento příspěvek byl skryt, protože zmiňuje vaše skryté uživatele nebo slova Příspěvek byl označen jako nevhodný uživatelem Příspěvek nenalezen Obrázek kanálu @@ -48,9 +49,11 @@ Zvýšení zvýšeno upraveno + úprava #%1$s originál Citovat Rozštěpení + Navrhnout úpravu Nová částka v sats Přidat "odpovídá na " @@ -674,7 +677,4 @@ Souhrn změn Rychlé opravy… Přijmout návrhy - Navrhnout úpravu - Tento příspěvek byl skryt, protože zmiňuje vaše skryté uživatele nebo slova - úprava #%1$s diff --git a/app/src/main/res/values-de/strings.xml b/app/src/main/res/values-de/strings.xml index 553ed6b73..9105a7ebe 100644 --- a/app/src/main/res/values-de/strings.xml +++ b/app/src/main/res/values-de/strings.xml @@ -6,6 +6,7 @@ Dein Profilbild QR Code scannen Trotzdem anzeigen + Dieser Beitrag wurde ausgeblendet, weil er Ihre ausgeblendeten Benutzer oder Wörter erwähnt Der Beitrag wurde als unangemessen gekennzeichnet von Ereignis wird geladen oder kann nicht in deiner Relay-Liste gefunden werden Kanalbild @@ -48,9 +49,11 @@ Boost boosted bearbeitet + Bearbeitung #%1$s Original Zitat Fork + Eine Bearbeitung vorschlagen Neuer Betrag in Sats Hinzufügen "Antworten auf " @@ -679,7 +682,4 @@ anz der Bedingungen ist erforderlich Zusammenfassung der Änderungen Schnelle Korrekturen… Den Vorschlag annehmen - Eine Bearbeitung vorschlagen - Dieser Beitrag wurde ausgeblendet, weil er Ihre ausgeblendeten Benutzer oder Wörter erwähnt - Bearbeitung #%1$s diff --git a/app/src/main/res/values-fr/strings.xml b/app/src/main/res/values-fr/strings.xml index f5f0ae9d2..5e72a863f 100644 --- a/app/src/main/res/values-fr/strings.xml +++ b/app/src/main/res/values-fr/strings.xml @@ -6,6 +6,7 @@ Votre Photo de Profil Scanner le QR code Montrer quand même + Ce message a été masqué car il mentionne vos utilisateurs ou mots cachés Post signalé inapproprié par Post non trouvé Image du canal @@ -422,8 +423,8 @@ DÉCONNECTÉ TERMINÉ PLANIFIÉ - Livestream est déconnecté - Livestream terminé + La diffusion en direct est déconnectée + La diffusion en direct est terminée Se déconnecter supprime toutes vos informations locales. Assurez-vous d\'avoir vos clés privées sauvegardées pour éviter de perdre votre compte. Voulez-vous continuer ? Tags suivis Relais diff --git a/app/src/main/res/values-pt-rBR/strings.xml b/app/src/main/res/values-pt-rBR/strings.xml index b4a2da339..07b8500ce 100644 --- a/app/src/main/res/values-pt-rBR/strings.xml +++ b/app/src/main/res/values-pt-rBR/strings.xml @@ -6,6 +6,7 @@ Sua Foto de Perfil Ler QR Mostrar de qualquer maneira + Esta postagem foi ocultada porque menciona seus usuários ou palavras ocultas A postagem foi sinalizada como imprópria por postagem não encontrada Imagem do Canal @@ -48,8 +49,10 @@ Impulsionar impulsionado editado + edição #%1$s Citar Garfo + Propor uma Edição Novo Valor em Sats Adicionar "respondendo para " @@ -673,7 +676,4 @@ Resumo das alterações Correções rápidas… Aceitar a Sugestão - Propor uma Edição - Esta postagem foi ocultada porque menciona seus usuários ou palavras ocultas - edição #%1$s diff --git a/app/src/main/res/values-sv-rSE/strings.xml b/app/src/main/res/values-sv-rSE/strings.xml index 5ec0edc08..7ec253d25 100644 --- a/app/src/main/res/values-sv-rSE/strings.xml +++ b/app/src/main/res/values-sv-rSE/strings.xml @@ -6,6 +6,7 @@ Din profilbild Scanna QR Visa ändå + Detta inlägg doldes eftersom det nämner dina dolda användare eller ord Inlägget flaggades som olämpligt av Inlägg hittades inte Kanal bild @@ -48,8 +49,10 @@ Boosta boostad redigerat + redigering #%1$s Citera Förgrening + Föreslå en redigering Nytt belopp i Sats Lägg till "Svarar till " @@ -672,7 +675,4 @@ Sammanfattning av ändringar Snabba fixar… Acceptera förslaget - Föreslå en redigering - Detta inlägg doldes eftersom det nämner dina dolda användare eller ord - redigering #%1$s From a92ef7a3755352170c380a58280d2586e07f515b Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Mon, 4 Mar 2024 09:08:06 -0500 Subject: [PATCH 55/98] v0.85.0 --- app/build.gradle | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/app/build.gradle b/app/build.gradle index 25f1ed849..10e391f2c 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -12,9 +12,9 @@ android { applicationId "com.vitorpamplona.amethyst" minSdk 26 targetSdk 34 - versionCode 358 - versionName "0.84.3" - buildConfigField "String", "RELEASE_NOTES_ID", "\"4d5a05aec61d8798f30f76b2efab81b98d75a03f935fb82823a1080bd56473cd\"" + versionCode 359 + versionName "0.85.0" + buildConfigField "String", "RELEASE_NOTES_ID", "\"d8da33fd13d129d86c53564aedefafbe3716f007c520431be4a8e488d3925afb\"" testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" vectorDrawables { From 249ac4e0398634eaf0adfc3d647c9a9efee969a5 Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Mon, 4 Mar 2024 09:08:20 -0500 Subject: [PATCH 56/98] Revert "v0.85.0" This reverts commit a92ef7a3755352170c380a58280d2586e07f515b. --- app/build.gradle | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/app/build.gradle b/app/build.gradle index 10e391f2c..25f1ed849 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -12,9 +12,9 @@ android { applicationId "com.vitorpamplona.amethyst" minSdk 26 targetSdk 34 - versionCode 359 - versionName "0.85.0" - buildConfigField "String", "RELEASE_NOTES_ID", "\"d8da33fd13d129d86c53564aedefafbe3716f007c520431be4a8e488d3925afb\"" + versionCode 358 + versionName "0.84.3" + buildConfigField "String", "RELEASE_NOTES_ID", "\"4d5a05aec61d8798f30f76b2efab81b98d75a03f935fb82823a1080bd56473cd\"" testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" vectorDrawables { From fa6adcde751c9a3b1a8b0dc7859c7e51a4092d77 Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Mon, 4 Mar 2024 09:10:11 -0500 Subject: [PATCH 57/98] v0.85.0 --- app/build.gradle | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/app/build.gradle b/app/build.gradle index 25f1ed849..10e391f2c 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -12,9 +12,9 @@ android { applicationId "com.vitorpamplona.amethyst" minSdk 26 targetSdk 34 - versionCode 358 - versionName "0.84.3" - buildConfigField "String", "RELEASE_NOTES_ID", "\"4d5a05aec61d8798f30f76b2efab81b98d75a03f935fb82823a1080bd56473cd\"" + versionCode 359 + versionName "0.85.0" + buildConfigField "String", "RELEASE_NOTES_ID", "\"d8da33fd13d129d86c53564aedefafbe3716f007c520431be4a8e488d3925afb\"" testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" vectorDrawables { From b3211cbb3558d6072d6c2a57313e5f3c3ff34bf4 Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Mon, 4 Mar 2024 09:37:32 -0500 Subject: [PATCH 58/98] Removes the release drafter plugin on actions. Too buggy --- .github/workflows/create-release.yml | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/.github/workflows/create-release.yml b/.github/workflows/create-release.yml index 65829028f..13c453f10 100644 --- a/.github/workflows/create-release.yml +++ b/.github/workflows/create-release.yml @@ -228,8 +228,4 @@ jobs: asset_path: app/build/outputs/bundle/fdroidRelease/app-fdroid-release.aab asset_name: amethyst-fdroid-${{ github.ref_name }}.aab asset_content_type: application/zip - - - name: Drafts a description for the release - uses: release-drafter/release-drafter@v5 - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} \ No newline at end of file + \ No newline at end of file From 4897c28ed31f4b1d34242f60824ca7c2caf16908 Mon Sep 17 00:00:00 2001 From: Crowdin Bot Date: Mon, 4 Mar 2024 14:38:48 +0000 Subject: [PATCH 59/98] New Crowdin translations by GitHub Action --- app/src/main/res/values-cs/strings.xml | 1 + app/src/main/res/values-de/strings.xml | 1 + app/src/main/res/values-pt-rBR/strings.xml | 2 ++ app/src/main/res/values-sv-rSE/strings.xml | 2 ++ 4 files changed, 6 insertions(+) diff --git a/app/src/main/res/values-cs/strings.xml b/app/src/main/res/values-cs/strings.xml index af9e6a1cb..a69d7c860 100644 --- a/app/src/main/res/values-cs/strings.xml +++ b/app/src/main/res/values-cs/strings.xml @@ -670,6 +670,7 @@ Git repositář: %1$s Internet: Klon: + OTS: %1$s Důkaz časového razítka Existuje důkaz, že tento příspěvek byl podepsán někdy před %1$s. Důkaz byl označen v Bitcoin blockchainu v tomto datu a čase. Upravit příspěvek diff --git a/app/src/main/res/values-de/strings.xml b/app/src/main/res/values-de/strings.xml index 9105a7ebe..b25d451b9 100644 --- a/app/src/main/res/values-de/strings.xml +++ b/app/src/main/res/values-de/strings.xml @@ -675,6 +675,7 @@ anz der Bedingungen ist erforderlich Git Repository: %1$s Internet: Klonen: + OTS: %1$s Zeitstempel Beweis Es gibt einen Beweis, dass dieser Beitrag irgendwann vor %1$s signiert wurde. Der Beweis wurde zu diesem Datum und Uhrzeit in der Bitcoin-Blockchain gestempelt. Beitrag bearbeiten diff --git a/app/src/main/res/values-pt-rBR/strings.xml b/app/src/main/res/values-pt-rBR/strings.xml index 07b8500ce..86b394ff3 100644 --- a/app/src/main/res/values-pt-rBR/strings.xml +++ b/app/src/main/res/values-pt-rBR/strings.xml @@ -50,6 +50,7 @@ impulsionado editado edição #%1$s + original Citar Garfo Propor uma Edição @@ -669,6 +670,7 @@ Repositório Git: %1$s Site: Clonar: + OTS: %1$s Prova de Carimbo de data/hora Há prova de que esta postagem foi assinada antes de %1$s. A prova foi carimbada no blockchain do Bitcoin naquela data e hora. Editar postagem diff --git a/app/src/main/res/values-sv-rSE/strings.xml b/app/src/main/res/values-sv-rSE/strings.xml index 7ec253d25..5e2aa6db5 100644 --- a/app/src/main/res/values-sv-rSE/strings.xml +++ b/app/src/main/res/values-sv-rSE/strings.xml @@ -50,6 +50,7 @@ boostad redigerat redigering #%1$s + original Citera Förgrening Föreslå en redigering @@ -668,6 +669,7 @@ Git Repository: %1$s Webbplats: Klona: + OTS: %1$s Tidsstämpel Bevis Det finns bevis på att detta inlägg signerades någon gång före %1$s. Beviset stämplades i Bitcoin-blockchainen vid det datumet och den tiden. Redigera inlägg From 08a103d332a6123ee83ca19ce2489edcd563e6be Mon Sep 17 00:00:00 2001 From: David Kaspar Date: Sat, 2 Mar 2024 13:05:26 +0000 Subject: [PATCH 60/98] updated some icons with content descriptions --- .../com/vitorpamplona/amethyst/ui/note/Icons.kt | 14 +++++++------- app/src/main/res/values/strings.xml | 4 ++++ 2 files changed, 11 insertions(+), 7 deletions(-) diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/note/Icons.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/Icons.kt index 1e0616bd3..1168f4de8 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/note/Icons.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/Icons.kt @@ -22,7 +22,7 @@ package com.vitorpamplona.amethyst.ui.note import androidx.compose.foundation.layout.size import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.filled.ArrowBack +import androidx.compose.material.icons.automirrored.filled.ArrowBack import androidx.compose.material.icons.filled.Bolt import androidx.compose.material.icons.filled.Cancel import androidx.compose.material.icons.filled.Clear @@ -81,7 +81,7 @@ fun FollowingIcon(iconSize: Dp) { @Composable fun ArrowBackIcon() { Icon( - imageVector = Icons.Default.ArrowBack, + imageVector = Icons.AutoMirrored.Filled.ArrowBack, contentDescription = stringResource(R.string.back), tint = MaterialTheme.colorScheme.grayText, ) @@ -104,7 +104,7 @@ fun DownloadForOfflineIcon( ) { Icon( imageVector = Icons.Default.DownloadForOffline, - null, + contentDescription = stringResource(id = R.string.accessibility_download_for_offline), modifier = remember(iconSize) { Modifier.size(iconSize) }, tint = tint, ) @@ -320,7 +320,7 @@ fun RegularPostIcon() { fun CancelIcon() { Icon( imageVector = Icons.Default.Cancel, - null, + contentDescription = stringResource(id = R.string.cancel), modifier = Size30Modifier, tint = MaterialTheme.colorScheme.placeholderText, ) @@ -375,7 +375,7 @@ fun PlayIcon( ) { Icon( imageVector = Icons.Outlined.PlayCircle, - contentDescription = null, + contentDescription = "Play", modifier = modifier, tint = tint, ) @@ -401,7 +401,7 @@ fun LyricsIcon( ) { Icon( painter = painterResource(id = R.drawable.lyrics_on), - contentDescription = null, + contentDescription = stringResource(id = R.string.accessibility_lyrics_on), modifier = modifier, tint = tint, ) @@ -414,7 +414,7 @@ fun LyricsOffIcon( ) { Icon( painter = painterResource(id = R.drawable.lyrics_off), - contentDescription = null, + contentDescription = stringResource(id = R.string.accessibility_lyrics_off), modifier = modifier, tint = tint, ) diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index d21e60ae0..bb391ed93 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -802,4 +802,8 @@ Quick fixes… Accept the Suggestion + + Download + Lyrics on + Lyrics off From 2158e36032329f0cc699264960df5d9b87def2b6 Mon Sep 17 00:00:00 2001 From: David Kaspar Date: Sat, 2 Mar 2024 13:08:28 +0000 Subject: [PATCH 61/98] Correct spelling --- .../java/com/vitorpamplona/amethyst/ui/screen/CardFeedView.kt | 2 +- .../amethyst/ui/screen/loggedIn/NotificationScreen.kt | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/CardFeedView.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/CardFeedView.kt index 2d24b634c..8bea5062e 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/CardFeedView.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/CardFeedView.kt @@ -58,7 +58,7 @@ import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel import com.vitorpamplona.amethyst.ui.theme.FeedPadding @Composable -fun RefresheableCardView( +fun RefreshableCardView( viewModel: CardFeedViewModel, accountViewModel: AccountViewModel, nav: (String) -> Unit, diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/NotificationScreen.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/NotificationScreen.kt index 99269225e..6e6e5bc81 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/NotificationScreen.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/NotificationScreen.kt @@ -78,7 +78,7 @@ import com.vitorpamplona.amethyst.ui.note.UserReactionsViewModel import com.vitorpamplona.amethyst.ui.note.showAmount import com.vitorpamplona.amethyst.ui.note.showCount import com.vitorpamplona.amethyst.ui.screen.NotificationViewModel -import com.vitorpamplona.amethyst.ui.screen.RefresheableCardView +import com.vitorpamplona.amethyst.ui.screen.RefreshableCardView import com.vitorpamplona.amethyst.ui.screen.ScrollStateKeys import com.vitorpamplona.amethyst.ui.screen.SharedPreferencesViewModel import com.vitorpamplona.amethyst.ui.theme.BitcoinOrange @@ -121,7 +121,7 @@ fun NotificationScreen( model = userReactionsStatsModel, ) - RefresheableCardView( + RefreshableCardView( viewModel = notifFeedViewModel, accountViewModel = accountViewModel, nav = nav, From 708bd00d0db4e97c214e9045dec6e766173a36d2 Mon Sep 17 00:00:00 2001 From: David Kaspar Date: Sat, 2 Mar 2024 14:00:46 +0000 Subject: [PATCH 62/98] updated vertical dots with content descriptions --- .../java/com/vitorpamplona/amethyst/ui/note/BadgeCompose.kt | 2 +- app/src/main/java/com/vitorpamplona/amethyst/ui/note/Icons.kt | 4 ++-- .../com/vitorpamplona/amethyst/ui/screen/ThreadFeedView.kt | 2 +- .../amethyst/ui/screen/loggedIn/ChatroomListScreen.kt | 2 +- .../vitorpamplona/amethyst/ui/screen/loggedIn/VideoScreen.kt | 4 +++- 5 files changed, 8 insertions(+), 6 deletions(-) diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/note/BadgeCompose.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/BadgeCompose.kt index c37701b43..30e2c7759 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/note/BadgeCompose.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/BadgeCompose.kt @@ -162,7 +162,7 @@ fun BadgeCompose( ) { Icon( imageVector = Icons.Default.MoreVert, - null, + contentDescription = stringResource(id = R.string.more_options), modifier = Size15Modifier, tint = MaterialTheme.colorScheme.placeholderText, ) diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/note/Icons.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/Icons.kt index 1168f4de8..32bfb7439 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/note/Icons.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/Icons.kt @@ -23,6 +23,7 @@ package com.vitorpamplona.amethyst.ui.note import androidx.compose.foundation.layout.size import androidx.compose.material.icons.Icons import androidx.compose.material.icons.automirrored.filled.ArrowBack +import androidx.compose.material.icons.automirrored.filled.OpenInNew import androidx.compose.material.icons.filled.Bolt import androidx.compose.material.icons.filled.Cancel import androidx.compose.material.icons.filled.Clear @@ -33,7 +34,6 @@ import androidx.compose.material.icons.filled.ExpandLess import androidx.compose.material.icons.filled.ExpandMore import androidx.compose.material.icons.filled.Link import androidx.compose.material.icons.filled.MoreVert -import androidx.compose.material.icons.filled.OpenInNew import androidx.compose.material.icons.filled.PushPin import androidx.compose.material.icons.filled.Report import androidx.compose.material.icons.filled.VolumeOff @@ -237,7 +237,7 @@ fun OpenInNewIcon( tint: Color = Color.Unspecified, ) { Icon( - imageVector = Icons.Default.OpenInNew, + imageVector = Icons.AutoMirrored.Filled.OpenInNew, stringResource(id = R.string.copy_to_clipboard), tint = tint, modifier = modifier, diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/ThreadFeedView.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/ThreadFeedView.kt index 97b2c8258..a4d829f0a 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/ThreadFeedView.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/ThreadFeedView.kt @@ -435,7 +435,7 @@ fun NoteMaster( ) { Icon( imageVector = Icons.Default.MoreVert, - null, + contentDescription = stringResource(id = R.string.more_options), modifier = Size15Modifier, tint = MaterialTheme.colorScheme.placeholderText, ) diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/ChatroomListScreen.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/ChatroomListScreen.kt index 94d4fa306..d5c0b5985 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/ChatroomListScreen.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/ChatroomListScreen.kt @@ -269,7 +269,7 @@ fun ChatroomListScreenOnlyList( ) { Icon( imageVector = Icons.Default.MoreVert, - contentDescription = null, + contentDescription = stringResource(id = R.string.more_options), tint = MaterialTheme.colorScheme.placeholderText, ) diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/VideoScreen.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/VideoScreen.kt index 0e5b4eeb4..06fb614be 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/VideoScreen.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/VideoScreen.kt @@ -57,11 +57,13 @@ import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalLifecycleOwner +import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import androidx.lifecycle.Lifecycle import androidx.lifecycle.LifecycleEventObserver import androidx.lifecycle.compose.collectAsStateWithLifecycle +import com.vitorpamplona.amethyst.R import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.service.NostrVideoDataSource import com.vitorpamplona.amethyst.ui.actions.NewPostView @@ -398,7 +400,7 @@ private fun VideoUserOptionAction( ) { Icon( imageVector = Icons.Default.MoreVert, - null, + contentDescription = stringResource(id = R.string.more_options), modifier = remember { Modifier.size(20.dp) }, tint = MaterialTheme.colorScheme.placeholderText, ) From 2bef5f015de0b59f7c15624e6bad252a1a029473 Mon Sep 17 00:00:00 2001 From: David Kaspar Date: Sat, 2 Mar 2024 15:09:51 +0000 Subject: [PATCH 63/98] change ArrowForwardIos to AutoMirrored --- .../amethyst/ui/actions/NewMediaView.kt | 4 ++-- .../amethyst/ui/actions/NewPostView.kt | 14 +++++++------- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewMediaView.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewMediaView.kt index dcb217e38..f68a9fd3d 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewMediaView.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewMediaView.kt @@ -88,7 +88,7 @@ fun NewMediaView( val resolver = LocalContext.current.contentResolver val context = LocalContext.current - val scroolState = rememberScrollState() + val scrollState = rememberScrollState() LaunchedEffect(uri) { val mediaType = resolver.getType(uri) ?: "" @@ -173,7 +173,7 @@ fun NewMediaView( modifier = Modifier.fillMaxWidth().weight(1f), ) { Column( - modifier = Modifier.fillMaxWidth().verticalScroll(scroolState), + modifier = Modifier.fillMaxWidth().verticalScroll(scrollState), ) { ImageVideoPost(postViewModel, accountViewModel) } diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewPostView.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewPostView.kt index 20efa29bf..1518064a0 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewPostView.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewPostView.kt @@ -56,7 +56,8 @@ import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.text.KeyboardOptions import androidx.compose.foundation.verticalScroll import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.filled.ArrowForwardIos +import androidx.compose.material.icons.automirrored.filled.ArrowForwardIos +import androidx.compose.material.icons.automirrored.outlined.ArrowForwardIos import androidx.compose.material.icons.filled.Bolt import androidx.compose.material.icons.filled.CurrencyBitcoin import androidx.compose.material.icons.filled.LocationOff @@ -65,7 +66,6 @@ import androidx.compose.material.icons.filled.Sell import androidx.compose.material.icons.filled.ShowChart import androidx.compose.material.icons.filled.Visibility import androidx.compose.material.icons.filled.VisibilityOff -import androidx.compose.material.icons.outlined.ArrowForwardIos import androidx.compose.material.icons.outlined.Bolt import androidx.compose.material.icons.rounded.Warning import androidx.compose.material3.Button @@ -1087,7 +1087,7 @@ fun FowardZapTo( tint = BitcoinOrange, ) Icon( - imageVector = Icons.Outlined.ArrowForwardIos, + imageVector = Icons.AutoMirrored.Outlined.ArrowForwardIos, contentDescription = stringResource(id = R.string.zaps), modifier = Modifier @@ -1451,7 +1451,7 @@ private fun ForwardZapTo( tint = MaterialTheme.colorScheme.onBackground, ) Icon( - imageVector = Icons.Default.ArrowForwardIos, + imageVector = Icons.AutoMirrored.Filled.ArrowForwardIos, contentDescription = null, modifier = Modifier @@ -1470,7 +1470,7 @@ private fun ForwardZapTo( tint = BitcoinOrange, ) Icon( - imageVector = Icons.Outlined.ArrowForwardIos, + imageVector = Icons.AutoMirrored.Outlined.ArrowForwardIos, contentDescription = null, modifier = Modifier @@ -1534,7 +1534,7 @@ private fun MarkAsSensitive( ) Icon( imageVector = Icons.Rounded.Warning, - contentDescription = null, + contentDescription = stringResource(R.string.add_content_warning), modifier = Modifier .size(10.dp) @@ -1553,7 +1553,7 @@ private fun MarkAsSensitive( ) Icon( imageVector = Icons.Rounded.Warning, - contentDescription = null, + contentDescription = stringResource(id = R.string.remove_content_warning), modifier = Modifier .size(10.dp) From bbf0d36cdf6cb26be6dd2810b088339e87963512 Mon Sep 17 00:00:00 2001 From: David Kaspar Date: Sat, 2 Mar 2024 15:42:55 +0000 Subject: [PATCH 64/98] Sealed message button on send message --- .../vitorpamplona/amethyst/ui/note/Icons.kt | 28 +++++++++++++++++++ .../ui/screen/loggedIn/ChatroomScreen.kt | 17 +++-------- app/src/main/res/values/strings.xml | 2 ++ 3 files changed, 34 insertions(+), 13 deletions(-) diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/note/Icons.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/Icons.kt index 32bfb7439..397a18db9 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/note/Icons.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/Icons.kt @@ -20,6 +20,7 @@ */ package com.vitorpamplona.amethyst.ui.note +import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.material.icons.Icons import androidx.compose.material.icons.automirrored.filled.ArrowBack @@ -49,6 +50,7 @@ import androidx.compose.ui.graphics.Color import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.dp import com.vitorpamplona.amethyst.R import com.vitorpamplona.amethyst.ui.theme.BitcoinOrange import com.vitorpamplona.amethyst.ui.theme.Size18Modifier @@ -480,3 +482,29 @@ fun NIP05FailedVerification(modifier: Modifier) { tint = Color.Red, ) } + +@Composable +fun IncognitoIconOn() { + Icon( + painter = painterResource(id = R.drawable.incognito), + contentDescription = stringResource(id = R.string.accessibility_turn_off_sealed_message), + modifier = + Modifier + .padding(top = 2.dp) + .size(18.dp), + tint = MaterialTheme.colorScheme.primary, + ) +} + +@Composable +fun IncognitoIconOff() { + Icon( + painter = painterResource(id = R.drawable.incognito_off), + contentDescription = stringResource(id = R.string.accessibility_turn_on_sealed_message), + modifier = + Modifier + .padding(top = 2.dp) + .size(18.dp), + tint = MaterialTheme.colorScheme.placeholderText, + ) +} diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/ChatroomScreen.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/ChatroomScreen.kt index c7537ddba..5850c5cba 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/ChatroomScreen.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/ChatroomScreen.kt @@ -68,7 +68,6 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalLifecycleOwner -import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.input.KeyboardCapitalization @@ -98,6 +97,8 @@ import com.vitorpamplona.amethyst.ui.components.ObserveDisplayNip05Status import com.vitorpamplona.amethyst.ui.note.ClickableUserPicture import com.vitorpamplona.amethyst.ui.note.DisplayRoomSubject import com.vitorpamplona.amethyst.ui.note.DisplayUserSetAsSubject +import com.vitorpamplona.amethyst.ui.note.IncognitoIconOff +import com.vitorpamplona.amethyst.ui.note.IncognitoIconOn import com.vitorpamplona.amethyst.ui.note.LoadUser import com.vitorpamplona.amethyst.ui.note.NonClickableUserPictures import com.vitorpamplona.amethyst.ui.note.QuickActionAlertDialog @@ -441,19 +442,9 @@ fun PrivateMessageEditFieldRow( }, ) { if (channelScreenModel.nip24) { - Icon( - painter = painterResource(id = R.drawable.incognito), - null, - modifier = Modifier.padding(top = 2.dp).size(18.dp), - tint = MaterialTheme.colorScheme.primary, - ) + IncognitoIconOn() } else { - Icon( - painter = painterResource(id = R.drawable.incognito_off), - null, - modifier = Modifier.padding(top = 2.dp).size(18.dp), - tint = MaterialTheme.colorScheme.placeholderText, - ) + IncognitoIconOff() } } } diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index bb391ed93..16c97a3d4 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -806,4 +806,6 @@ Download Lyrics on Lyrics off + Sealed message off. Click to turn on sealed message + Sealed message on. Click to turn off sealed message From 4cbdf11ecb7b0133c6e23f8ddf045822363e0667 Mon Sep 17 00:00:00 2001 From: David Kaspar Date: Sat, 2 Mar 2024 16:34:09 +0000 Subject: [PATCH 65/98] Send message button --- .../vitorpamplona/amethyst/ui/screen/loggedIn/ChatroomScreen.kt | 2 +- app/src/main/res/values/strings.xml | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/ChatroomScreen.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/ChatroomScreen.kt index 5850c5cba..59f052a0d 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/ChatroomScreen.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/ChatroomScreen.kt @@ -525,7 +525,7 @@ fun ThinSendButton( ) { Icon( imageVector = Icons.Default.Send, - null, + contentDescription = stringResource(id = R.string.accessibility_send), modifier = Size20Modifier, ) } diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 16c97a3d4..8cebec4ad 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -808,4 +808,5 @@ Lyrics off Sealed message off. Click to turn on sealed message Sealed message on. Click to turn off sealed message + Send From 75934b5b98027782332923632418a493241a1e36 Mon Sep 17 00:00:00 2001 From: David Kaspar Date: Sat, 2 Mar 2024 17:03:45 +0000 Subject: [PATCH 66/98] Relay list screeen --- .../amethyst/ui/actions/NewRelayListView.kt | 16 ++++++---------- 1 file changed, 6 insertions(+), 10 deletions(-) diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewRelayListView.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewRelayListView.kt index 2e809ae02..f1ae54813 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewRelayListView.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewRelayListView.kt @@ -45,8 +45,8 @@ import androidx.compose.material.icons.filled.SyncProblem import androidx.compose.material.icons.filled.Upload import androidx.compose.material3.Button import androidx.compose.material3.ButtonDefaults -import androidx.compose.material3.Divider import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.HorizontalDivider import androidx.compose.material3.Icon import androidx.compose.material3.IconButton import androidx.compose.material3.MaterialTheme @@ -272,9 +272,7 @@ fun ServerConfigHeader() { } } - Divider( - thickness = DividerThickness, - ) + HorizontalDivider(thickness = DividerThickness) } } @@ -459,9 +457,7 @@ fun ServerConfigClickableLine( } } - Divider( - thickness = DividerThickness, - ) + HorizontalDivider(thickness = DividerThickness) } } @@ -839,7 +835,7 @@ private fun FirstLine( ) { Icon( imageVector = Icons.Default.Cancel, - null, + contentDescription = stringResource(id = R.string.remove), modifier = Modifier.padding(start = 10.dp).size(15.dp), tint = WarningColor, ) @@ -875,7 +871,7 @@ fun EditableServerConfig( IconButton(onClick = { read = !read }) { Icon( imageVector = Icons.Default.Download, - null, + contentDescription = stringResource(id = R.string.read_from_relay), modifier = Modifier.size(Size35dp).padding(horizontal = 5.dp), tint = if (read) { @@ -889,7 +885,7 @@ fun EditableServerConfig( IconButton(onClick = { write = !write }) { Icon( imageVector = Icons.Default.Upload, - null, + contentDescription = stringResource(id = R.string.write_to_relay), modifier = Modifier.size(Size35dp).padding(horizontal = 5.dp), tint = if (write) { From 2f3df616f37a42f1f36aa50d63580031dcccc0f2 Mon Sep 17 00:00:00 2001 From: David Kaspar Date: Sat, 2 Mar 2024 17:19:47 +0000 Subject: [PATCH 67/98] QR code icon --- .../com/vitorpamplona/amethyst/ui/navigation/DrawerContent.kt | 2 +- .../vitorpamplona/amethyst/ui/note/UpdateZapAmountDialog.kt | 4 ++-- app/src/main/res/values/strings.xml | 2 ++ 3 files changed, 5 insertions(+), 3 deletions(-) diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/navigation/DrawerContent.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/navigation/DrawerContent.kt index 4471144f1..3062595c4 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/navigation/DrawerContent.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/navigation/DrawerContent.kt @@ -768,7 +768,7 @@ fun BottomContent( ) { Icon( painter = painterResource(R.drawable.ic_qrcode), - null, + contentDescription = stringResource(id = R.string.show_npub_as_a_qr_code), modifier = Modifier.size(24.dp), tint = MaterialTheme.colorScheme.primary, ) diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/note/UpdateZapAmountDialog.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/UpdateZapAmountDialog.kt index e81d9bee4..a38762655 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/note/UpdateZapAmountDialog.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/UpdateZapAmountDialog.kt @@ -445,7 +445,7 @@ fun UpdateZapAmountDialog( ) { Icon( painter = painterResource(R.drawable.alby), - null, + contentDescription = stringResource(id = R.string.accessibility_navigate_to_alby), modifier = Modifier.size(24.dp), tint = Color.Unspecified, ) @@ -454,7 +454,7 @@ fun UpdateZapAmountDialog( IconButton(onClick = { qrScanning = true }) { Icon( painter = painterResource(R.drawable.ic_qrcode), - null, + contentDescription = stringResource(id = R.string.accessibility_scan_qr_code), modifier = Modifier.size(24.dp), tint = MaterialTheme.colorScheme.primary, ) diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 8cebec4ad..ff396b1d3 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -809,4 +809,6 @@ Sealed message off. Click to turn on sealed message Sealed message on. Click to turn off sealed message Send + Scan QR code + Navigate to the third-party wallet provider Alby From e507e0bb6a07ed077c2f38d6ac17d55a084d0151 Mon Sep 17 00:00:00 2001 From: David Kaspar Date: Sat, 2 Mar 2024 19:01:47 +0000 Subject: [PATCH 68/98] update Play text --- app/src/main/java/com/vitorpamplona/amethyst/ui/note/Icons.kt | 2 +- app/src/main/res/values/strings.xml | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/note/Icons.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/Icons.kt index 397a18db9..9d2d4cb5a 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/note/Icons.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/Icons.kt @@ -377,7 +377,7 @@ fun PlayIcon( ) { Icon( imageVector = Icons.Outlined.PlayCircle, - contentDescription = "Play", + contentDescription = stringResource(id = R.string.accessibility_play_username), modifier = modifier, tint = tint, ) diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index ff396b1d3..296eb4049 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -809,6 +809,7 @@ Sealed message off. Click to turn on sealed message Sealed message on. Click to turn off sealed message Send + Play username as audio Scan QR code Navigate to the third-party wallet provider Alby From 8c5aea46f1a532aea2f5ccdf6defe023c6247d0e Mon Sep 17 00:00:00 2001 From: David Kaspar Date: Sat, 2 Mar 2024 19:10:42 +0000 Subject: [PATCH 69/98] refactor incognito button icon --- .../vitorpamplona/amethyst/ui/note/Icons.kt | 34 +++++++++---------- .../ui/screen/loggedIn/ChatroomScreen.kt | 16 +++++++-- 2 files changed, 30 insertions(+), 20 deletions(-) diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/note/Icons.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/Icons.kt index 9d2d4cb5a..2dc80c964 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/note/Icons.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/Icons.kt @@ -20,11 +20,12 @@ */ package com.vitorpamplona.amethyst.ui.note -import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.material.icons.Icons import androidx.compose.material.icons.automirrored.filled.ArrowBack import androidx.compose.material.icons.automirrored.filled.OpenInNew +import androidx.compose.material.icons.automirrored.filled.VolumeOff +import androidx.compose.material.icons.automirrored.filled.VolumeUp import androidx.compose.material.icons.filled.Bolt import androidx.compose.material.icons.filled.Cancel import androidx.compose.material.icons.filled.Clear @@ -37,8 +38,6 @@ import androidx.compose.material.icons.filled.Link import androidx.compose.material.icons.filled.MoreVert import androidx.compose.material.icons.filled.PushPin import androidx.compose.material.icons.filled.Report -import androidx.compose.material.icons.filled.VolumeOff -import androidx.compose.material.icons.filled.VolumeUp import androidx.compose.material.icons.outlined.BarChart import androidx.compose.material.icons.outlined.PlayCircle import androidx.compose.material3.Icon @@ -50,7 +49,6 @@ import androidx.compose.ui.graphics.Color import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.Dp -import androidx.compose.ui.unit.dp import com.vitorpamplona.amethyst.R import com.vitorpamplona.amethyst.ui.theme.BitcoinOrange import com.vitorpamplona.amethyst.ui.theme.Size18Modifier @@ -340,7 +338,7 @@ fun CloseIcon() { @Composable fun MutedIcon() { Icon( - imageVector = Icons.Default.VolumeOff, + imageVector = Icons.AutoMirrored.Filled.VolumeOff, contentDescription = stringResource(id = R.string.muted_button), tint = MaterialTheme.colorScheme.onBackground, modifier = Size30Modifier, @@ -350,7 +348,7 @@ fun MutedIcon() { @Composable fun MuteIcon() { Icon( - imageVector = Icons.Default.VolumeUp, + imageVector = Icons.AutoMirrored.Filled.VolumeUp, contentDescription = stringResource(id = R.string.mute_button), tint = MaterialTheme.colorScheme.onBackground, modifier = Size30Modifier, @@ -484,27 +482,27 @@ fun NIP05FailedVerification(modifier: Modifier) { } @Composable -fun IncognitoIconOn() { +fun IncognitoIconOn( + modifier: Modifier, + tint: Color, +) { Icon( painter = painterResource(id = R.drawable.incognito), contentDescription = stringResource(id = R.string.accessibility_turn_off_sealed_message), - modifier = - Modifier - .padding(top = 2.dp) - .size(18.dp), - tint = MaterialTheme.colorScheme.primary, + modifier = modifier, + tint = tint, ) } @Composable -fun IncognitoIconOff() { +fun IncognitoIconOff( + modifier: Modifier, + tint: Color, +) { Icon( painter = painterResource(id = R.drawable.incognito_off), contentDescription = stringResource(id = R.string.accessibility_turn_on_sealed_message), - modifier = - Modifier - .padding(top = 2.dp) - .size(18.dp), - tint = MaterialTheme.colorScheme.placeholderText, + modifier = modifier, + tint = tint, ) } diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/ChatroomScreen.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/ChatroomScreen.kt index 59f052a0d..1e621e109 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/ChatroomScreen.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/ChatroomScreen.kt @@ -442,9 +442,21 @@ fun PrivateMessageEditFieldRow( }, ) { if (channelScreenModel.nip24) { - IncognitoIconOn() + IncognitoIconOn( + modifier = + Modifier + .padding(top = 2.dp) + .size(18.dp), + tint = MaterialTheme.colorScheme.primary, + ) } else { - IncognitoIconOff() + IncognitoIconOff( + modifier = + Modifier + .padding(top = 2.dp) + .size(18.dp), + tint = MaterialTheme.colorScheme.placeholderText, + ) } } } From f0c53efebd280d6f49a7d75e912941bd8c570e06 Mon Sep 17 00:00:00 2001 From: David Kaspar Date: Sun, 3 Mar 2024 09:38:13 +0000 Subject: [PATCH 70/98] app logo --- app/src/main/java/com/vitorpamplona/amethyst/ui/note/Icons.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/note/Icons.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/Icons.kt index 2dc80c964..4f714d59d 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/note/Icons.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/Icons.kt @@ -62,7 +62,7 @@ import com.vitorpamplona.amethyst.ui.theme.subtleButton fun AmethystIcon(iconSize: Dp) { Icon( painter = painterResource(R.drawable.amethyst), - null, + contentDescription = stringResource(id = R.string.app_logo), modifier = Modifier.size(iconSize), tint = Color.Unspecified, ) From 875d5385818ec728da372fe3ac0d55ab0443f750 Mon Sep 17 00:00:00 2001 From: David Kaspar Date: Mon, 4 Mar 2024 15:03:36 +0000 Subject: [PATCH 71/98] added accessibility translations for CS, DE, SV, PT --- app/src/main/res/values-cs/strings.xml | 9 +++++++++ app/src/main/res/values-de/strings.xml | 9 +++++++++ app/src/main/res/values-pt-rBR/strings.xml | 9 +++++++++ app/src/main/res/values-sv-rSE/strings.xml | 9 +++++++++ 4 files changed, 36 insertions(+) diff --git a/app/src/main/res/values-cs/strings.xml b/app/src/main/res/values-cs/strings.xml index a69d7c860..1fbfee90c 100644 --- a/app/src/main/res/values-cs/strings.xml +++ b/app/src/main/res/values-cs/strings.xml @@ -678,4 +678,13 @@ Souhrn změn Rychlé opravy… Přijmout návrhy + Stáhnout + Text písně zapnuto + Text písně vypnuto + Zapečetěná zpráva vypnuta. Klikněte pro zapnutí zapečetěné zprávy + Zapečetěná zpráva zapnuta. Klikněte pro vypnutí zapečetěné zprávy + Odeslat + Přehrát uživatelské jméno jako audio + Skenovat QR kód + Přejít na poskytovatele peněženky třetí strany Alby diff --git a/app/src/main/res/values-de/strings.xml b/app/src/main/res/values-de/strings.xml index b25d451b9..cd528fecc 100644 --- a/app/src/main/res/values-de/strings.xml +++ b/app/src/main/res/values-de/strings.xml @@ -683,4 +683,13 @@ anz der Bedingungen ist erforderlich Zusammenfassung der Änderungen Schnelle Korrekturen… Den Vorschlag annehmen + Herunterladen + Liedtext an + Liedtext aus + Versiegelte Nachricht aus. Klicken Sie, um die versiegelte Nachricht einzuschalten + Versiegelte Nachricht an. Klicken Sie, um die versiegelte Nachricht auszuschalten + Senden + Benutzernamen als Audio abspielen + QR-Code scannen + Navigieren Sie zum Drittanbieter-Wallet-Anbieter Alby diff --git a/app/src/main/res/values-pt-rBR/strings.xml b/app/src/main/res/values-pt-rBR/strings.xml index 86b394ff3..d7484c17d 100644 --- a/app/src/main/res/values-pt-rBR/strings.xml +++ b/app/src/main/res/values-pt-rBR/strings.xml @@ -678,4 +678,13 @@ Resumo das alterações Correções rápidas… Aceitar a Sugestão + Baixar + Letras ligadas + Letras desligadas + Mensagem selada desligada. Clique para ligar a mensagem selada + Mensagem selada ligada. Clique para desligar a mensagem selada + Enviar + Reproduzir nome de usuário como áudio + Escanear código QR + Navegar para o provedor de carteira de terceiros Alby diff --git a/app/src/main/res/values-sv-rSE/strings.xml b/app/src/main/res/values-sv-rSE/strings.xml index 5e2aa6db5..0378413f2 100644 --- a/app/src/main/res/values-sv-rSE/strings.xml +++ b/app/src/main/res/values-sv-rSE/strings.xml @@ -677,4 +677,13 @@ Sammanfattning av ändringar Snabba fixar… Acceptera förslaget + Ladda ner + Undertexter på + Undertexter av + Förseglat meddelande av. Klicka för att slå på förseglat meddelande + Förseglat meddelande på. Klicka för att stänga av förseglat meddelande + Skicka + Spela upp användarnamn som ljud + Skanna QR-kod + Navigera till tredjeparts plånboksleverantören Alby From 64c4f7dc95cffe63e513b4e59d49464305adfb59 Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Mon, 4 Mar 2024 10:29:19 -0500 Subject: [PATCH 72/98] Removes extra new line --- .github/workflows/create-release.yml | 1 - 1 file changed, 1 deletion(-) diff --git a/.github/workflows/create-release.yml b/.github/workflows/create-release.yml index 13c453f10..77c10ab3f 100644 --- a/.github/workflows/create-release.yml +++ b/.github/workflows/create-release.yml @@ -228,4 +228,3 @@ jobs: asset_path: app/build/outputs/bundle/fdroidRelease/app-fdroid-release.aab asset_name: amethyst-fdroid-${{ github.ref_name }}.aab asset_content_type: application/zip - \ No newline at end of file From 779ce110ba706af414773d09b4acbacea1d265e7 Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Mon, 4 Mar 2024 11:26:22 -0500 Subject: [PATCH 73/98] Adding the most recent release features to the Readme. --- README.md | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index 51a530e1d..635556388 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,5 @@
- + Amethyst Logo @@ -40,7 +40,7 @@ height="70">](https://github.com/vitorpamplona/amethyst/releases) - [x] Events / Relay Subscriptions (NIP-01) - [x] Follow List (NIP-02) -- [ ] OpenTimestamps Attestations (NIP-03) +- [x] OpenTimestamps Attestations (NIP-03) - [x] Private Messages (NIP-04) - [x] DNS Address (NIP-05) - [ ] Mnemonic seed phrase (NIP-06) @@ -70,7 +70,9 @@ height="70">](https://github.com/vitorpamplona/amethyst/releases) - [x] Event kind summaries (NIP-31) - [ ] Labeling (NIP-32) - [x] Parameterized Replaceable Events (NIP-33) +- [x] Git Stuff (NIP-34/Draft) - [x] Sensitive Content (NIP-36) +- [x] Note Edits (NIP-37/Draft) - [x] User Status Event (NIP-38) - [x] External Identities (NIP-39) - [x] Expiration Support (NIP-40) @@ -109,7 +111,7 @@ height="70">](https://github.com/vitorpamplona/amethyst/releases) - [x] Classifieds (NIP-99) - [x] Private Messages and Small Groups (NIP-24/Draft) - [x] Versioned Encrypted Payloads (NIP-44/Draft) -- [x] Audio Tracks (zapstr.live) (Kind:31337) +- [x] Audio Tracks (zapstr.live) (kind:31337) - [x] Push Notifications (Google and Unified Push) - [x] In-Device Automatic Translations - [x] Hashtag Following and Custom Hashtags @@ -118,6 +120,9 @@ height="70">](https://github.com/vitorpamplona/amethyst/releases) - [x] De-googled F-Droid flavor - [x] Multiple Accounts - [x] Markdown Support +- [x] FHIR Payloads (kind:82) +- [ ] Decentralized Wiki (kind:30818) +- [ ] Embed events - [ ] Image/Video Capture in the app - [ ] Local Database - [ ] Workspaces @@ -135,16 +140,16 @@ Information shared on Nostr can be re-broadcasted to other servers and should be # Development Overview -This repository is split between Amethyst and Quartz: +This repository is split between Amethyst and Quartz: - Amethyst is a native Android app made with Kotlin and Jetpack Compose. -- Quartz is our own Nostr-commons library to host classes that are of interest to other Nostr Clients. +- Quartz is our own Nostr-commons library to host classes that are of interest to other Nostr Clients. The app architecture consists of the UI, which uses the usual State/ViewModel/Composition, the service layer that connects with Nostr relays, and the model/repository layer, which keeps all Nostr objects in memory, in a full OO graph. The repository layer stores Nostr Events as Notes and Users separately. Those classes use LiveData and Flow objects to allow the UI and other parts of the app to subscribe to each Note/User and receive updates when they happen. -They are also responsible for updating viewModels when needed. As the user scrolls through Events, the Datasource classes +They are also responsible for updating viewModels when needed. As the user scrolls through Events, the Datasource classes are updated to receive more information about those particular Events. Most of the UI is reactive to changes in the repository classes. The service layer assembles Nostr filters for each need of the app, From 5a680beabbb7a71ffc30264919e667516bb2ef72 Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Mon, 4 Mar 2024 11:58:24 -0500 Subject: [PATCH 74/98] Fixes missing Fhir Classes on Release --- .../amethyst/{ui/note => model}/MiniFhir.kt | 2 +- .../com/vitorpamplona/amethyst/ui/note/NoteCompose.kt | 9 +++++++++ 2 files changed, 10 insertions(+), 1 deletion(-) rename app/src/main/java/com/vitorpamplona/amethyst/{ui/note => model}/MiniFhir.kt (99%) diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/note/MiniFhir.kt b/app/src/main/java/com/vitorpamplona/amethyst/model/MiniFhir.kt similarity index 99% rename from app/src/main/java/com/vitorpamplona/amethyst/ui/note/MiniFhir.kt rename to app/src/main/java/com/vitorpamplona/amethyst/model/MiniFhir.kt index 7f4b59514..6bf60d156 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/note/MiniFhir.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/model/MiniFhir.kt @@ -18,7 +18,7 @@ * 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.model import android.util.Log import com.fasterxml.jackson.annotation.JsonSubTypes 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 ca4517c3e..c4077a767 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 @@ -112,10 +112,19 @@ import com.vitorpamplona.amethyst.commons.MediaUrlImage import com.vitorpamplona.amethyst.commons.MediaUrlVideo import com.vitorpamplona.amethyst.commons.RichTextParser import com.vitorpamplona.amethyst.model.AddressableNote +import com.vitorpamplona.amethyst.model.Bundle import com.vitorpamplona.amethyst.model.Channel +import com.vitorpamplona.amethyst.model.FhirElementDatabase +import com.vitorpamplona.amethyst.model.LensSpecification import com.vitorpamplona.amethyst.model.Note +import com.vitorpamplona.amethyst.model.Patient +import com.vitorpamplona.amethyst.model.Practitioner import com.vitorpamplona.amethyst.model.RelayBriefInfoCache +import com.vitorpamplona.amethyst.model.Resource import com.vitorpamplona.amethyst.model.User +import com.vitorpamplona.amethyst.model.VisionPrescription +import com.vitorpamplona.amethyst.model.findReferenceInDb +import com.vitorpamplona.amethyst.model.parseResourceBundleOrNull import com.vitorpamplona.amethyst.service.CachedGeoLocations import com.vitorpamplona.amethyst.ui.actions.EditPostView import com.vitorpamplona.amethyst.ui.actions.NewRelayListView From 2f45067949fe0352d723cda9c82646e34851042f Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Mon, 4 Mar 2024 12:00:02 -0500 Subject: [PATCH 75/98] v0.85.1 --- app/build.gradle | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/build.gradle b/app/build.gradle index 10e391f2c..9d536f33a 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -12,8 +12,8 @@ android { applicationId "com.vitorpamplona.amethyst" minSdk 26 targetSdk 34 - versionCode 359 - versionName "0.85.0" + versionCode 360 + versionName "0.85.1" buildConfigField "String", "RELEASE_NOTES_ID", "\"d8da33fd13d129d86c53564aedefafbe3716f007c520431be4a8e488d3925afb\"" testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" From cd2b5d78a182d386d6197c2040e3b6da53a1aa99 Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Mon, 4 Mar 2024 13:16:34 -0500 Subject: [PATCH 76/98] Don't show button to edit the post if the author of the original post is not the logged in user --- .../amethyst/ui/note/NoteCompose.kt | 33 +++++++++++++++---- app/src/main/res/values/strings.xml | 2 +- 2 files changed, 27 insertions(+), 8 deletions(-) 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 c4077a767..e1922634a 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 @@ -1516,7 +1516,7 @@ fun RenderTextModificationEvent( val noteEvent = note.event as? TextNoteModificationEvent ?: return val noteAuthor = note.author ?: return - val isAuthorTheLoggedUser = remember(note.event) { accountViewModel.isLoggedUser(note.author) } + // val isAuthorTheLoggedUser = remember(note.event) { accountViewModel.isLoggedUser(note.author) } val editState = remember { @@ -1543,6 +1543,13 @@ fun RenderTextModificationEvent( mutableStateOf(false) } + val isAuthorTheLoggedUser = + remember { + val authorOfTheOriginalNote = noteEvent.editedNote()?.let { accountViewModel.getNoteIfExists(it)?.author } + + mutableStateOf(accountViewModel.isLoggedUser(authorOfTheOriginalNote)) + } + Card( modifier = MaterialTheme.colorScheme.imageModifier, ) { @@ -1574,6 +1581,16 @@ fun RenderTextModificationEvent( noteEvent.editedNote()?.let { LoadNote(baseNoteHex = it, accountViewModel = accountViewModel) { baseNote -> baseNote?.let { + val noteState by baseNote.live().metadata.observeAsState() + + LaunchedEffect(key1 = noteState) { + val newAuthor = accountViewModel.isLoggedUser(noteState?.note?.author) + + if (isAuthorTheLoggedUser.value != newAuthor) { + isAuthorTheLoggedUser.value = newAuthor + } + } + Column( modifier = MaterialTheme.colorScheme.innerPostModifier.padding(Size10dp).clickable { @@ -1609,13 +1626,15 @@ fun RenderTextModificationEvent( } } - Spacer(modifier = StdVertSpacer) + if (isAuthorTheLoggedUser.value) { + Spacer(modifier = StdVertSpacer) - Button( - onClick = { wantsToEditPost.value = true }, - modifier = Modifier.fillMaxWidth(), - ) { - Text(text = stringResource(id = R.string.accept_the_suggestion)) + Button( + onClick = { wantsToEditPost.value = true }, + modifier = Modifier.fillMaxWidth(), + ) { + Text(text = stringResource(id = R.string.accept_the_suggestion)) + } } } } diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 296eb4049..5963977bc 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -797,7 +797,7 @@ There\'s proof this post was signed sometime before %1$s. The proof was stamped in the Bitcoin blockchain at that date and time. Edit Post - Proposal to improve your post + Proposal to improve a post Summary of changes Quick fixes… From 1f66ef717b4636c6d83d167eb70384ccac7b31c8 Mon Sep 17 00:00:00 2001 From: Crowdin Bot Date: Mon, 4 Mar 2024 18:18:38 +0000 Subject: [PATCH 77/98] New Crowdin translations by GitHub Action --- app/src/main/res/values-th/strings.xml | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/app/src/main/res/values-th/strings.xml b/app/src/main/res/values-th/strings.xml index 55de47ddb..16eb4016f 100644 --- a/app/src/main/res/values-th/strings.xml +++ b/app/src/main/res/values-th/strings.xml @@ -3,8 +3,10 @@ สแกน Qr Code โชว์ QR รูปโปรไฟล์ + รูปโปรไฟล์ แสกน QR แสดง + โน๊ตนี้ถูกซ่อนเนื่องจากมีเนื้อหา หรือผู้ใช้ ที่ท่านไม่อยากเห็น โพสต์ถูกรายงานว่าไม่เหมาะสมโดย ไม่พบโพสต์นี้ รูปของ channel @@ -15,6 +17,7 @@ สแปม เลียนแบบ พฤติกรรมที่ผิดกฎหมาย + อื่นๆ ไม่ทราบ รีเลย์ไอคอน ไม่ทราบผู้เขียน @@ -22,6 +25,8 @@ คัดลอก ID ผู้เขียน คัดลอก ID โน้ต เพยแพร่ + ประทับเวลา + กำลังรอยืนยันการประทับเวลา ส่งคำขอให้ลบ บล๊อก / รายงาน From 9081d5a54b9f847e4683de3268681e4a15d2eb4c Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Mon, 4 Mar 2024 14:13:23 -0500 Subject: [PATCH 78/98] Adds nostr git for issue management software. --- README.md | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 635556388..9149eb6dd 100644 --- a/README.md +++ b/README.md @@ -241,12 +241,14 @@ dependencyResolutionManagement { Add the dependency ```gradle -implementation('com.github.vitorpamplona.amethyst:quartz:v0.84.3') +implementation('com.github.vitorpamplona.amethyst:quartz:v0.85.1') ``` ## Contributing -[Issues](https://github.com/vitorpamplona/amethyst/issues) and [pull requests](https://github.com/vitorpamplona/amethyst/pulls) here are very welcome. Translations can be provided via [Crowdin](https://crowdin.com/project/amethyst-social) +Issues can be logged on: [https://gitworkshop.dev/repo/amethyst](https://gitworkshop.dev/repo/amethyst) + +[GitHub issues](https://github.com/vitorpamplona/amethyst/issues) and [pull requests](https://github.com/vitorpamplona/amethyst/pulls) here are also welcome. Translations can be provided via [Crowdin](https://crowdin.com/project/amethyst-social) You can also send patches through Nostr using [GitStr](https://github.com/fiatjaf/gitstr) to [this nostr address](https://patch34.pages.dev/naddr1qqyxzmt9w358jum5qyg8v6t5daezumn0wd68yvfwvdhk6qg7waehxw309ahx7um5wgkhqatz9emk2mrvdaexgetj9ehx2ap0qy2hwumn8ghj7un9d3shjtnwdaehgu3wvfnj7q3qgcxzte5zlkncx26j68ez60fzkvtkm9e0vrwdcvsjakxf9mu9qewqxpqqqpmej720gac) From 23718f51dd203020035933af88d54e39c415c215 Mon Sep 17 00:00:00 2001 From: greenart7c3 Date: Mon, 4 Mar 2024 17:11:01 -0300 Subject: [PATCH 79/98] fix crash parsing multiple results from amber --- .../vitorpamplona/quartz/signers/ExternalSignerLauncher.kt | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/signers/ExternalSignerLauncher.kt b/quartz/src/main/java/com/vitorpamplona/quartz/signers/ExternalSignerLauncher.kt index bff1e2eb8..0d9c1f252 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/signers/ExternalSignerLauncher.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/signers/ExternalSignerLauncher.kt @@ -33,8 +33,8 @@ import com.fasterxml.jackson.databind.JsonNode import com.fasterxml.jackson.databind.deser.std.StdDeserializer import com.fasterxml.jackson.databind.module.SimpleModule import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper +import com.fasterxml.jackson.module.kotlin.readValue import com.vitorpamplona.quartz.encoders.HexKey -import com.vitorpamplona.quartz.events.Event import com.vitorpamplona.quartz.events.EventInterface import com.vitorpamplona.quartz.events.LnZapRequestEvent @@ -90,9 +90,7 @@ class Result( * Parses the json with a string of events to an Array of Event objects. */ fun fromJsonArray(json: String): Array { - return Event.mapper.readTree(json).map { - fromJson(it.asText()) - }.toTypedArray() + return mapper.readValue(json) } } } From 7bc393143c5e590951f8e499b0caed442d274655 Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Mon, 4 Mar 2024 15:12:17 -0500 Subject: [PATCH 80/98] Adds link to the version notes from the drawer --- .../amethyst/ui/navigation/DrawerContent.kt | 41 +++++++++---------- 1 file changed, 20 insertions(+), 21 deletions(-) diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/navigation/DrawerContent.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/navigation/DrawerContent.kt index 3062595c4..9a72230aa 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/navigation/DrawerContent.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/navigation/DrawerContent.kt @@ -72,10 +72,13 @@ import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalFocusManager import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.SpanStyle +import androidx.compose.ui.text.buildAnnotatedString import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.input.ImeAction import androidx.compose.ui.text.input.KeyboardCapitalization import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.text.withStyle import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import androidx.lifecycle.compose.collectAsStateWithLifecycle @@ -86,6 +89,7 @@ import com.vitorpamplona.amethyst.model.User import com.vitorpamplona.amethyst.service.relays.RelayPool import com.vitorpamplona.amethyst.service.relays.RelayPoolStatus import com.vitorpamplona.amethyst.ui.actions.NewRelayListView +import com.vitorpamplona.amethyst.ui.components.ClickableText import com.vitorpamplona.amethyst.ui.components.CreateTextWithEmoji import com.vitorpamplona.amethyst.ui.components.RobohashFallbackAsyncImage import com.vitorpamplona.amethyst.ui.note.LoadStatuses @@ -736,29 +740,24 @@ fun BottomContent( modifier = Modifier.fillMaxWidth().padding(horizontal = 15.dp), verticalAlignment = Alignment.CenterVertically, ) { - Text( + ClickableText( + text = + buildAnnotatedString { + withStyle( + SpanStyle( + fontSize = 12.sp, + fontWeight = FontWeight.Bold, + ), + ) { + append("v" + BuildConfig.VERSION_NAME + "-" + BuildConfig.FLAVOR.uppercase()) + } + }, + onClick = { + nav("Note/${BuildConfig.RELEASE_NOTES_ID}") + coroutineScope.launch { drawerState.close() } + }, modifier = Modifier.padding(start = 16.dp), - text = "v" + BuildConfig.VERSION_NAME + "-" + BuildConfig.FLAVOR.uppercase(), - fontSize = 12.sp, - fontWeight = FontWeight.Bold, ) - /* - IconButton( - onClick = { - when (AppCompatDelegate.getDefaultNightMode()) { - AppCompatDelegate.MODE_NIGHT_NO -> AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_YES) - AppCompatDelegate.MODE_NIGHT_YES -> AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_NO) - else -> AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_YES) - } - } - ) { - Icon( - painter = painterResource(R.drawable.ic_theme), - null, - modifier = Modifier.size(24.dp), - tint = MaterialTheme.colorScheme.primary - ) - }*/ Box(modifier = Modifier.weight(1F)) IconButton( onClick = { From e9fd62dc26bfa66a59b3da613df522fa21b6a10d Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Mon, 4 Mar 2024 15:37:29 -0500 Subject: [PATCH 81/98] Displaying correct edits on the new edit proposal --- .../amethyst/ui/note/NoteCompose.kt | 58 +++++++++++-------- .../amethyst/ui/screen/ThreadFeedView.kt | 1 - 2 files changed, 34 insertions(+), 25 deletions(-) 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 e1922634a..70cd1cb2c 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 @@ -1136,6 +1136,11 @@ class EditState() { fun latest() = modificationsList.lastOrNull() + fun latestBefore(createdAt: Long?): Note? { + if (createdAt == null) return latest() + return modificationsList.lastOrNull { (it.createdAt() ?: Long.MAX_VALUE) < createdAt } + } + fun nextModification() { if (modificationToShowIndex < 0) { modificationToShowIndex = 0 @@ -1370,7 +1375,6 @@ private fun RenderNoteRow( makeItShort, canPreview, backgroundColor, - editState, accountViewModel, nav, ) @@ -1509,35 +1513,12 @@ fun RenderTextModificationEvent( makeItShort: Boolean, canPreview: Boolean, backgroundColor: MutableState, - editStateByAuthor: State>, accountViewModel: AccountViewModel, nav: (String) -> Unit, ) { val noteEvent = note.event as? TextNoteModificationEvent ?: return val noteAuthor = note.author ?: return - // val isAuthorTheLoggedUser = remember(note.event) { accountViewModel.isLoggedUser(note.author) } - - val editState = - remember { - derivedStateOf { - val loadable = editStateByAuthor.value as? GenericLoadable.Loaded - - val state = EditState() - - val latestChangeByAuthor = - if (loadable != null && loadable.loaded.hasModificationsToShow()) { - loadable.loaded.latest() - } else { - null - } - - state.updateModifications(listOfNotNull(latestChangeByAuthor, note)) - - GenericLoadable.Loaded(state) - } - } - val wantsToEditPost = remember { mutableStateOf(false) @@ -1550,6 +1531,13 @@ fun RenderTextModificationEvent( mutableStateOf(accountViewModel.isLoggedUser(authorOfTheOriginalNote)) } + noteEvent.editedNote()?.let { + LoadNote(baseNoteHex = it, accountViewModel = accountViewModel) { baseOriginalNote -> + baseOriginalNote?.let { + } + } + } + Card( modifier = MaterialTheme.colorScheme.imageModifier, ) { @@ -1583,6 +1571,28 @@ fun RenderTextModificationEvent( baseNote?.let { val noteState by baseNote.live().metadata.observeAsState() + val editStateOriginalNote = observeEdits(baseNote = baseNote, accountViewModel = accountViewModel) + + val editState = + remember(note) { + derivedStateOf { + val loadable = editStateOriginalNote.value as? GenericLoadable.Loaded + + val state = EditState() + + val latestChangeByAuthor = + if (loadable != null && loadable.loaded.hasModificationsToShow()) { + loadable.loaded.latestBefore(note.createdAt()) + } else { + null + } + + state.updateModifications(listOfNotNull(latestChangeByAuthor, note)) + + GenericLoadable.Loaded(state) + } + } + LaunchedEffect(key1 = noteState) { val newAuthor = accountViewModel.isLoggedUser(noteState?.note?.author) diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/ThreadFeedView.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/ThreadFeedView.kt index a4d829f0a..5480f6ee9 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/ThreadFeedView.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/ThreadFeedView.kt @@ -576,7 +576,6 @@ fun NoteMaster( makeItShort = false, canPreview = true, backgroundColor, - editState, accountViewModel, nav, ) From 432b1e49021b7aacd3c558c433fb428eb4ceaf2c Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Mon, 4 Mar 2024 16:15:09 -0500 Subject: [PATCH 82/98] Fixes forking information kept from a previous post. --- .../com/vitorpamplona/amethyst/ui/actions/NewPostViewModel.kt | 2 ++ 1 file changed, 2 insertions(+) diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewPostViewModel.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewPostViewModel.kt index 0959d90b4..aa3c72900 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewPostViewModel.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewPostViewModel.kt @@ -289,6 +289,8 @@ open class NewPostViewModel() : ViewModel() { } forkedFromNote = it + } ?: run { + forkedFromNote = null } if (!forwardZapTo.items.isEmpty()) { From 8299f4cfca41333d3ff212ef723bf86e12e6c080 Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Mon, 4 Mar 2024 16:20:59 -0500 Subject: [PATCH 83/98] Cleans up the fork information on new posts has been canceled or posted. --- .../com/vitorpamplona/amethyst/ui/actions/NewPostViewModel.kt | 2 ++ 1 file changed, 2 insertions(+) diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewPostViewModel.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewPostViewModel.kt index aa3c72900..7643e8e80 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewPostViewModel.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewPostViewModel.kt @@ -629,6 +629,8 @@ open class NewPostViewModel() : ViewModel() { toUsers = TextFieldValue("") subject = TextFieldValue("") + forkedFromNote = null + contentToAddUrl = null urlPreview = null isUploadingImage = false From d12e886e9e3cce8902a5dfddc0656022f2ae8f87 Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Mon, 4 Mar 2024 16:52:38 -0500 Subject: [PATCH 84/98] Bringing GitIssues in the Notification --- .../service/NostrAccountDataSource.kt | 59 +++++++---- .../amethyst/ui/note/NoteCompose.kt | 97 +++++++++++++++++++ .../amethyst/ui/screen/ThreadFeedView.kt | 4 + 3 files changed, 143 insertions(+), 17 deletions(-) diff --git a/app/src/main/java/com/vitorpamplona/amethyst/service/NostrAccountDataSource.kt b/app/src/main/java/com/vitorpamplona/amethyst/service/NostrAccountDataSource.kt index cedf0bd4f..0e49e30bf 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/service/NostrAccountDataSource.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/service/NostrAccountDataSource.kt @@ -35,6 +35,9 @@ import com.vitorpamplona.quartz.events.AdvertisedRelayListEvent import com.vitorpamplona.quartz.events.BadgeAwardEvent import com.vitorpamplona.quartz.events.BadgeProfilesEvent import com.vitorpamplona.quartz.events.BookmarkListEvent +import com.vitorpamplona.quartz.events.CalendarDateSlotEvent +import com.vitorpamplona.quartz.events.CalendarRSVPEvent +import com.vitorpamplona.quartz.events.CalendarTimeSlotEvent import com.vitorpamplona.quartz.events.ChannelMessageEvent import com.vitorpamplona.quartz.events.ContactListEvent import com.vitorpamplona.quartz.events.EmojiPackSelectionEvent @@ -42,6 +45,10 @@ import com.vitorpamplona.quartz.events.Event import com.vitorpamplona.quartz.events.EventInterface import com.vitorpamplona.quartz.events.GenericRepostEvent import com.vitorpamplona.quartz.events.GiftWrapEvent +import com.vitorpamplona.quartz.events.GitIssueEvent +import com.vitorpamplona.quartz.events.GitPatchEvent +import com.vitorpamplona.quartz.events.GitReplyEvent +import com.vitorpamplona.quartz.events.HighlightEvent import com.vitorpamplona.quartz.events.LnZapEvent import com.vitorpamplona.quartz.events.LnZapPaymentResponseEvent import com.vitorpamplona.quartz.events.MetadataEvent @@ -120,24 +127,12 @@ object NostrAccountDataSource : NostrDataSource("AccountData") { ) } - fun createAccountAcceptedAwardsFilter(): TypedFilter { + fun createAccountSettingsFilter(): TypedFilter { return TypedFilter( types = COMMON_FEED_TYPES, filter = JsonFilter( - kinds = listOf(BadgeProfilesEvent.KIND, EmojiPackSelectionEvent.KIND), - authors = listOf(account.userProfile().pubkeyHex), - limit = 10, - ), - ) - } - - fun createAccountBookmarkListFilter(): TypedFilter { - return TypedFilter( - types = COMMON_FEED_TYPES, - filter = - JsonFilter( - kinds = listOf(BookmarkListEvent.KIND, PeopleListEvent.KIND, MuteListEvent.KIND), + kinds = listOf(BookmarkListEvent.KIND, PeopleListEvent.KIND, MuteListEvent.KIND, BadgeProfilesEvent.KIND, EmojiPackSelectionEvent.KIND), authors = listOf(account.userProfile().pubkeyHex), limit = 100, ), @@ -204,6 +199,36 @@ object NostrAccountDataSource : NostrDataSource("AccountData") { ) } + fun createNotificationFilter2(): TypedFilter { + val since = + latestEOSEs.users[account.userProfile()] + ?.followList + ?.get(account.defaultNotificationFollowList.value) + ?.relayList + ?: account.activeRelays()?.associate { it.url to EOSETime(TimeUtils.oneWeekAgo()) } + ?: account.convertLocalRelays().associate { it.url to EOSETime(TimeUtils.oneWeekAgo()) } + + return TypedFilter( + types = COMMON_FEED_TYPES, + filter = + JsonFilter( + kinds = + listOf( + GitReplyEvent.KIND, + GitIssueEvent.KIND, + GitPatchEvent.KIND, + HighlightEvent.KIND, + CalendarDateSlotEvent.KIND, + CalendarTimeSlotEvent.KIND, + CalendarRSVPEvent.KIND, + ), + tags = mapOf("p" to listOf(account.userProfile().pubkeyHex)), + limit = 400, + since = since, + ), + ) + } + fun createGiftWrapsToMeFilter() = TypedFilter( types = COMMON_FEED_TYPES, @@ -297,10 +322,10 @@ object NostrAccountDataSource : NostrDataSource("AccountData") { createAccountContactListFilter(), createAccountRelayListFilter(), createNotificationFilter(), + createNotificationFilter2(), createGiftWrapsToMeFilter(), createAccountReportsFilter(), - createAccountAcceptedAwardsFilter(), - createAccountBookmarkListFilter(), + createAccountSettingsFilter(), createAccountLastPostsListFilter(), createOtherAccountsBaseFilter(), ) @@ -312,7 +337,7 @@ object NostrAccountDataSource : NostrDataSource("AccountData") { createAccountMetadataFilter(), createAccountContactListFilter(), createAccountRelayListFilter(), - createAccountBookmarkListFilter(), + createAccountSettingsFilter(), ) .ifEmpty { null } } 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 70cd1cb2c..295462b07 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 @@ -230,6 +230,7 @@ import com.vitorpamplona.quartz.events.FhirResourceEvent import com.vitorpamplona.quartz.events.FileHeaderEvent import com.vitorpamplona.quartz.events.FileStorageHeaderEvent import com.vitorpamplona.quartz.events.GenericRepostEvent +import com.vitorpamplona.quartz.events.GitIssueEvent import com.vitorpamplona.quartz.events.GitPatchEvent import com.vitorpamplona.quartz.events.GitRepositoryEvent import com.vitorpamplona.quartz.events.HighlightEvent @@ -1309,6 +1310,16 @@ private fun RenderNoteRow( nav, ) } + is GitIssueEvent -> { + RenderGitIssueEvent( + baseNote, + makeItShort, + canPreview, + backgroundColor, + accountViewModel, + nav, + ) + } is PrivateDmEvent -> { RenderPrivateMessage( baseNote, @@ -4124,6 +4135,92 @@ private fun RenderGitPatchEvent( } } +@Composable +fun RenderGitIssueEvent( + baseNote: Note, + makeItShort: Boolean, + canPreview: Boolean, + backgroundColor: MutableState, + accountViewModel: AccountViewModel, + nav: (String) -> Unit, +) { + val event = baseNote.event as? GitIssueEvent ?: return + + RenderGitIssueEvent(event, baseNote, makeItShort, canPreview, backgroundColor, accountViewModel, nav) +} + +@Composable +private fun RenderGitIssueEvent( + noteEvent: GitIssueEvent, + note: Note, + makeItShort: Boolean, + canPreview: Boolean, + backgroundColor: MutableState, + accountViewModel: AccountViewModel, + nav: (String) -> Unit, +) { + val repository = remember(noteEvent) { noteEvent.repository() } + + if (repository != null) { + LoadAddressableNote(aTag = repository, accountViewModel = accountViewModel) { + if (it != null) { + RenderShortRepositoryHeader(it, accountViewModel, nav) + Spacer(modifier = DoubleVertSpacer) + } + } + } + + LoadDecryptedContent(note, accountViewModel) { body -> + val eventContent by + remember(note.event) { + derivedStateOf { + val subject = (note.event as? TextNoteEvent)?.subject()?.ifEmpty { null } + + if (!subject.isNullOrBlank() && !body.split("\n")[0].contains(subject)) { + "### $subject\n$body" + } else { + body + } + } + } + + val isAuthorTheLoggedUser = remember(note.event) { accountViewModel.isLoggedUser(note.author) } + + if (makeItShort && isAuthorTheLoggedUser) { + Text( + text = eventContent, + color = MaterialTheme.colorScheme.placeholderText, + maxLines = 2, + overflow = TextOverflow.Ellipsis, + ) + } else { + SensitivityWarning( + note = note, + accountViewModel = accountViewModel, + ) { + val modifier = remember(note) { Modifier.fillMaxWidth() } + val tags = remember(note) { note.event?.tags()?.toImmutableListOfLists() ?: EmptyTagList } + + TranslatableRichTextViewer( + content = eventContent, + canPreview = canPreview && !makeItShort, + modifier = modifier, + tags = tags, + backgroundColor = backgroundColor, + accountViewModel = accountViewModel, + nav = nav, + ) + } + + if (note.event?.hasHashtags() == true) { + val hashtags = + remember(note.event) { note.event?.hashtags()?.toImmutableList() ?: persistentListOf() } + DisplayUncitedHashtags(hashtags, eventContent, nav) + } + } + } +} + @Composable fun RenderGitRepositoryEvent( baseNote: Note, diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/ThreadFeedView.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/ThreadFeedView.kt index 5480f6ee9..9822f1d67 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/ThreadFeedView.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/ThreadFeedView.kt @@ -124,6 +124,7 @@ import com.vitorpamplona.amethyst.ui.note.ReactionsRow import com.vitorpamplona.amethyst.ui.note.RenderAppDefinition import com.vitorpamplona.amethyst.ui.note.RenderEmojiPack import com.vitorpamplona.amethyst.ui.note.RenderFhirResource +import com.vitorpamplona.amethyst.ui.note.RenderGitIssueEvent import com.vitorpamplona.amethyst.ui.note.RenderGitPatchEvent import com.vitorpamplona.amethyst.ui.note.RenderGitRepositoryEvent import com.vitorpamplona.amethyst.ui.note.RenderPinListEvent @@ -166,6 +167,7 @@ import com.vitorpamplona.quartz.events.FhirResourceEvent import com.vitorpamplona.quartz.events.FileHeaderEvent import com.vitorpamplona.quartz.events.FileStorageHeaderEvent import com.vitorpamplona.quartz.events.GenericRepostEvent +import com.vitorpamplona.quartz.events.GitIssueEvent import com.vitorpamplona.quartz.events.GitPatchEvent import com.vitorpamplona.quartz.events.GitRepositoryEvent import com.vitorpamplona.quartz.events.HighlightEvent @@ -554,6 +556,8 @@ fun NoteMaster( RenderGitRepositoryEvent(baseNote, accountViewModel, nav) } else if (noteEvent is GitPatchEvent) { RenderGitPatchEvent(baseNote, false, true, backgroundColor, accountViewModel, nav) + } else if (noteEvent is GitIssueEvent) { + RenderGitIssueEvent(baseNote, false, true, backgroundColor, accountViewModel, nav) } else if (noteEvent is AppDefinitionEvent) { RenderAppDefinition(baseNote, accountViewModel, nav) } else if (noteEvent is HighlightEvent) { From c6977d97d327481b58ad85c8a7adbfdbb05e979c Mon Sep 17 00:00:00 2001 From: Crowdin Bot Date: Mon, 4 Mar 2024 21:54:12 +0000 Subject: [PATCH 85/98] New Crowdin translations by GitHub Action --- app/src/main/res/values-es-rES/strings.xml | 19 +++++++++++++++++++ app/src/main/res/values-es-rMX/strings.xml | 19 +++++++++++++++++++ app/src/main/res/values-es-rUS/strings.xml | 19 +++++++++++++++++++ 3 files changed, 57 insertions(+) diff --git a/app/src/main/res/values-es-rES/strings.xml b/app/src/main/res/values-es-rES/strings.xml index 62a2a8e82..67effe4b5 100644 --- a/app/src/main/res/values-es-rES/strings.xml +++ b/app/src/main/res/values-es-rES/strings.xml @@ -6,6 +6,7 @@ Tu imagen de perfil Escanear el QR Mostrar de todos modos + Esta publicación se ocultó porque menciona tus usuarios o palabras ocultas Post marcado como inapropiado por El evento se está cargando o no se puede encontrar en la lista de relés Imagen del canal @@ -47,8 +48,12 @@ Total de visualizaciones Impulsar Impulsada + editada + edición #%1$s + original Citar Bifurcar + Proponer una edición Nueva cantidad en sats Añadir "respondiendo a " @@ -670,4 +675,18 @@ OTS: %1$s Prueba de marca de tiempo Hay prueba de que esta publicación se firmó en algún momento antes de %1$s. La prueba se marcó en la cadena de bloques de Bitcoin en esa fecha y hora. + Editar publicación + Propuesta para mejorar una publicación + Resumen de cambios + Correcciones rápidas… + Aceptar la sugerencia + Descargar + Letra activada + Letra desactivada + Mensaje sellado desactivado. Haz clic para activar el mensaje sellado. + Mensaje sellado activado. Haz clic para desactivar el mensaje sellado. + Enviar + Reproducir nombre de usuario como audio + Escanear código QR + Ir al proveedor externo de monederos Alby diff --git a/app/src/main/res/values-es-rMX/strings.xml b/app/src/main/res/values-es-rMX/strings.xml index 2852fb8e8..437043ab6 100644 --- a/app/src/main/res/values-es-rMX/strings.xml +++ b/app/src/main/res/values-es-rMX/strings.xml @@ -6,6 +6,7 @@ Tu imagen de perfil Escanear código QR Mostrar de todos modos + Esta publicación se ocultó porque menciona tus usuarios o palabras ocultas La publicación fue reportada por El evento se está cargando o no se puede encontrar en la lista de relés Imagen del canal @@ -47,8 +48,12 @@ Total de visualizaciones Impulsar Impulsada + editada + edición #%1$s + original Cita Bifurcar + Proponer una edición Nueva cantidad en sats Agregar "respondiendo a " @@ -670,4 +675,18 @@ OTS: %1$s Prueba de marca de tiempo Hay prueba de que esta publicación se firmó en algún momento antes de %1$s. La prueba se marcó en la cadena de bloques de Bitcoin en esa fecha y hora. + Editar publicación + Propuesta para mejorar una publicación + Resumen de cambios + Correcciones rápidas… + Aceptar la sugerencia + Descargar + Letra activada + Letra desactivada + Mensaje sellado desactivado. Haz clic para activar el mensaje sellado. + Mensaje sellado activado. Haz clic para desactivar el mensaje sellado. + Enviar + Reproducir nombre de usuario como audio + Escanear código QR + Ir al proveedor externo de billeteras Alby diff --git a/app/src/main/res/values-es-rUS/strings.xml b/app/src/main/res/values-es-rUS/strings.xml index 09a021900..d7f0391f6 100644 --- a/app/src/main/res/values-es-rUS/strings.xml +++ b/app/src/main/res/values-es-rUS/strings.xml @@ -6,6 +6,7 @@ Tu imagen de perfil Escanear código QR Mostrar de todos modos + Esta publicación se ocultó porque menciona tus usuarios o palabras ocultas La publicación fue reportada por El evento se está cargando o no se puede encontrar en la lista de relés Imagen del canal @@ -47,8 +48,12 @@ Total vistas Impulsar Impulsada + editada + edición #%1$s + original Cita Bifurcar + Proponer una edición Nueva cantidad en sats Agregar "respondiendo a " @@ -670,4 +675,18 @@ OTS: %1$s Prueba de marca de tiempo Hay prueba de que esta publicación se firmó en algún momento antes de %1$s. La prueba se marcó en la cadena de bloques de Bitcoin en esa fecha y hora. + Editar publicación + Propuesta para mejorar una publicación + Resumen de cambios + Correcciones rápidas… + Aceptar la sugerencia + Descargar + Letra activada + Letra desactivada + Mensaje sellado desactivado. Haz clic para activar el mensaje sellado. + Mensaje sellado activado. Haz clic para desactivar el mensaje sellado. + Enviar + Reproducir nombre de usuario como audio + Escanear código QR + Ir al proveedor externo de billeteras Alby From 1cf828b165ae898c2203c3bc96768f4c3cf85d8d Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Mon, 4 Mar 2024 17:52:08 -0500 Subject: [PATCH 86/98] v0.85.2 --- app/build.gradle | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/build.gradle b/app/build.gradle index 9d536f33a..9d622dfff 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -12,8 +12,8 @@ android { applicationId "com.vitorpamplona.amethyst" minSdk 26 targetSdk 34 - versionCode 360 - versionName "0.85.1" + versionCode 361 + versionName "0.85.2" buildConfigField "String", "RELEASE_NOTES_ID", "\"d8da33fd13d129d86c53564aedefafbe3716f007c520431be4a8e488d3925afb\"" testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" From 5848255e72fae583057da73a1a2f2d295d4e1d04 Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Mon, 4 Mar 2024 18:01:12 -0500 Subject: [PATCH 87/98] Displaying issues and Patches in the Notification --- .../amethyst/ui/dal/NotificationFeedFilter.kt | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/dal/NotificationFeedFilter.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/dal/NotificationFeedFilter.kt index 33d86af79..5e49b616e 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/dal/NotificationFeedFilter.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/dal/NotificationFeedFilter.kt @@ -32,6 +32,9 @@ import com.vitorpamplona.quartz.events.ChannelCreateEvent import com.vitorpamplona.quartz.events.ChannelMetadataEvent import com.vitorpamplona.quartz.events.GenericRepostEvent import com.vitorpamplona.quartz.events.GiftWrapEvent +import com.vitorpamplona.quartz.events.GitIssueEvent +import com.vitorpamplona.quartz.events.GitPatchEvent +import com.vitorpamplona.quartz.events.HighlightEvent import com.vitorpamplona.quartz.events.LnZapEvent import com.vitorpamplona.quartz.events.LnZapRequestEvent import com.vitorpamplona.quartz.events.MuteListEvent @@ -94,6 +97,14 @@ class NotificationFeedFilter(val account: Account) : AdditiveFeedFilter() ): Boolean { val event = note.event + if (event is GitIssueEvent || event is GitPatchEvent) { + return true + } + + if (event is HighlightEvent) { + return true + } + if (event is BaseTextNoteEvent) { if (note.replyTo?.any { it.author?.pubkeyHex == authorHex } == true) return true From f73c9b5773a886e0afcd893254084a6bc4cbbc97 Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Mon, 4 Mar 2024 18:09:03 -0500 Subject: [PATCH 88/98] v0.85.3 --- app/build.gradle | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/build.gradle b/app/build.gradle index 9d622dfff..e266a4e6e 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -12,8 +12,8 @@ android { applicationId "com.vitorpamplona.amethyst" minSdk 26 targetSdk 34 - versionCode 361 - versionName "0.85.2" + versionCode 362 + versionName "0.85.3" buildConfigField "String", "RELEASE_NOTES_ID", "\"d8da33fd13d129d86c53564aedefafbe3716f007c520431be4a8e488d3925afb\"" testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" From 3b3ca06c1cb0dcf12d629d9dddb0f8366611b247 Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Tue, 5 Mar 2024 09:30:47 -0500 Subject: [PATCH 89/98] Massive refactoring to reduce the size of NoteCompose --- .../amethyst/ui/actions/EditPostView.kt | 2 +- .../ui/actions/JoinUserOrChannelView.kt | 28 +- .../amethyst/ui/actions/NewPostView.kt | 25 +- .../amethyst/ui/components/CashuRedeem.kt | 5 +- .../amethyst/ui/components/InvoicePreview.kt | 5 +- .../amethyst/ui/components/InvoiceRequest.kt | 5 +- .../ui/components/SelectTextDialog.kt | 5 +- .../amethyst/ui/components/TextSpinner.kt | 6 +- .../ui/components/ZapRaiserRequest.kt | 5 +- .../amethyst/ui/layouts/ChatHeaderLayout.kt | 8 +- .../amethyst/ui/navigation/AppBottomBar.kt | 4 +- .../amethyst/ui/navigation/AppTopBar.kt | 28 +- .../amethyst/ui/navigation/DrawerContent.kt | 6 +- .../amethyst/ui/note/BadgeCompose.kt | 5 +- .../amethyst/ui/note/BlankNote.kt | 5 +- .../amethyst/ui/note/ChannelCardCompose.kt | 4 +- .../vitorpamplona/amethyst/ui/note/Loaders.kt | 243 ++ .../amethyst/ui/note/MessageSetCompose.kt | 5 +- .../amethyst/ui/note/MultiSetCompose.kt | 5 +- .../amethyst/ui/note/NoteCompose.kt | 3778 +---------------- .../amethyst/ui/note/NoteQuickActionMenu.kt | 26 +- .../amethyst/ui/note/ReactionsRow.kt | 3 +- .../amethyst/ui/note/RelayCompose.kt | 4 +- .../ui/note/UpdateReactionTypeDialog.kt | 1 + .../amethyst/ui/note/UpdateZapAmountDialog.kt | 4 +- .../amethyst/ui/note/UserCompose.kt | 4 +- .../amethyst/ui/note/UserProfilePicture.kt | 331 -- .../amethyst/ui/note/ZapNoteCompose.kt | 4 +- .../amethyst/ui/note/ZapTheDevsCard.kt | 2 +- .../amethyst/ui/note/ZapUserSetCompose.kt | 4 +- .../{ => note}/elements/AddRemoveButtons.kt | 2 +- .../amethyst/ui/note/elements/BoostedMark.kt | 41 + .../ui/note/elements/DefaultImageHeader.kt | 70 + .../{ => note}/elements/DisplayCommunity.kt | 2 +- .../ui/note/elements/DisplayEditStatus.kt | 59 + .../ui/{ => note}/elements/DisplayHashtags.kt | 2 +- .../ui/note/elements/DisplayLocation.kt | 53 + .../amethyst/ui/note/elements/DisplayOts.kt | 102 + .../ui/{ => note}/elements/DisplayPoW.kt | 2 +- .../ui/{ => note}/elements/DisplayReward.kt | 2 +- .../elements/DisplayUncitedHashtags.kt | 2 +- .../{ => note}/elements/DisplayZapSplits.kt | 2 +- .../amethyst/ui/note/elements/DropDownMenu.kt | 393 ++ .../amethyst/ui/note/elements/ForkInfo.kt | 155 + .../amethyst/ui/note/elements/TimeAgo.kt | 50 + .../amethyst/ui/note/types/AppDefinition.kt | 245 ++ .../amethyst/ui/note/types/AudioTrack.kt | 235 + .../amethyst/ui/note/types/Badge.kt | 233 + .../amethyst/ui/note/types/Classifieds.kt | 161 + .../amethyst/ui/note/types/CommunityHeader.kt | 393 ++ .../amethyst/ui/note/types/Emoji.kt | 309 ++ .../amethyst/ui/note/types/FileHeader.kt | 85 + .../amethyst/ui/note/types/FileStorage.kt | 118 + .../amethyst/ui/note/types/Git.kt | 384 ++ .../amethyst/ui/note/types/Highlight.kt | 201 + .../amethyst/ui/note/types/LiveActivity.kt | 217 + .../amethyst/ui/note/types/LongForm.kt | 133 + .../amethyst/ui/note/types/MedicalData.kt | 265 ++ .../amethyst/ui/note/types/PeopleList.kt | 126 + .../amethyst/ui/note/types/PinList.kt | 131 + .../amethyst/ui/note/types/Poll.kt | 94 + .../amethyst/ui/note/types/PrivateMessage.kt | 118 + .../amethyst/ui/note/types/Reaction.kt | 58 + .../amethyst/ui/note/types/RelayList.kt | 178 + .../ui/note/types/RenderPostApproval.kt | 97 + .../amethyst/ui/note/types/Report.kt | 108 + .../amethyst/ui/note/types/Text.kt | 123 + .../ui/note/types/TextModification.kt | 279 ++ .../amethyst/ui/note/types/Video.kt | 187 + .../amethyst/ui/screen/ChatroomFeedView.kt | 6 +- .../amethyst/ui/screen/ThreadFeedView.kt | 94 +- .../ui/screen/loggedIn/ChannelScreen.kt | 8 +- .../ui/screen/loggedIn/ChatroomListScreen.kt | 4 +- .../ui/screen/loggedIn/ChatroomScreen.kt | 6 +- .../ui/screen/loggedIn/GeoHashScreen.kt | 4 +- .../ui/screen/loggedIn/HashtagScreen.kt | 4 +- .../ui/screen/loggedIn/HiddenUsersScreen.kt | 8 +- .../ui/screen/loggedIn/NotificationScreen.kt | 4 +- .../ui/screen/loggedIn/ProfileScreen.kt | 10 +- .../ui/screen/loggedIn/ReportNoteDialog.kt | 5 +- .../ui/screen/loggedIn/SearchScreen.kt | 6 +- .../ui/screen/loggedIn/VideoScreen.kt | 6 +- .../vitorpamplona/amethyst/ui/theme/Shape.kt | 4 + .../components/TranslatableRichTextViewer.kt | 7 +- 84 files changed, 5886 insertions(+), 4265 deletions(-) create mode 100644 app/src/main/java/com/vitorpamplona/amethyst/ui/note/Loaders.kt rename app/src/main/java/com/vitorpamplona/amethyst/ui/{ => note}/elements/AddRemoveButtons.kt (98%) create mode 100644 app/src/main/java/com/vitorpamplona/amethyst/ui/note/elements/BoostedMark.kt create mode 100644 app/src/main/java/com/vitorpamplona/amethyst/ui/note/elements/DefaultImageHeader.kt rename app/src/main/java/com/vitorpamplona/amethyst/ui/{ => note}/elements/DisplayCommunity.kt (98%) create mode 100644 app/src/main/java/com/vitorpamplona/amethyst/ui/note/elements/DisplayEditStatus.kt rename app/src/main/java/com/vitorpamplona/amethyst/ui/{ => note}/elements/DisplayHashtags.kt (98%) create mode 100644 app/src/main/java/com/vitorpamplona/amethyst/ui/note/elements/DisplayLocation.kt create mode 100644 app/src/main/java/com/vitorpamplona/amethyst/ui/note/elements/DisplayOts.kt rename app/src/main/java/com/vitorpamplona/amethyst/ui/{ => note}/elements/DisplayPoW.kt (97%) rename app/src/main/java/com/vitorpamplona/amethyst/ui/{ => note}/elements/DisplayReward.kt (99%) rename app/src/main/java/com/vitorpamplona/amethyst/ui/{ => note}/elements/DisplayUncitedHashtags.kt (97%) rename app/src/main/java/com/vitorpamplona/amethyst/ui/{ => note}/elements/DisplayZapSplits.kt (98%) create mode 100644 app/src/main/java/com/vitorpamplona/amethyst/ui/note/elements/DropDownMenu.kt create mode 100644 app/src/main/java/com/vitorpamplona/amethyst/ui/note/elements/ForkInfo.kt create mode 100644 app/src/main/java/com/vitorpamplona/amethyst/ui/note/elements/TimeAgo.kt create mode 100644 app/src/main/java/com/vitorpamplona/amethyst/ui/note/types/AppDefinition.kt create mode 100644 app/src/main/java/com/vitorpamplona/amethyst/ui/note/types/AudioTrack.kt create mode 100644 app/src/main/java/com/vitorpamplona/amethyst/ui/note/types/Badge.kt create mode 100644 app/src/main/java/com/vitorpamplona/amethyst/ui/note/types/Classifieds.kt create mode 100644 app/src/main/java/com/vitorpamplona/amethyst/ui/note/types/CommunityHeader.kt create mode 100644 app/src/main/java/com/vitorpamplona/amethyst/ui/note/types/Emoji.kt create mode 100644 app/src/main/java/com/vitorpamplona/amethyst/ui/note/types/FileHeader.kt create mode 100644 app/src/main/java/com/vitorpamplona/amethyst/ui/note/types/FileStorage.kt create mode 100644 app/src/main/java/com/vitorpamplona/amethyst/ui/note/types/Git.kt create mode 100644 app/src/main/java/com/vitorpamplona/amethyst/ui/note/types/Highlight.kt create mode 100644 app/src/main/java/com/vitorpamplona/amethyst/ui/note/types/LiveActivity.kt create mode 100644 app/src/main/java/com/vitorpamplona/amethyst/ui/note/types/LongForm.kt create mode 100644 app/src/main/java/com/vitorpamplona/amethyst/ui/note/types/MedicalData.kt create mode 100644 app/src/main/java/com/vitorpamplona/amethyst/ui/note/types/PeopleList.kt create mode 100644 app/src/main/java/com/vitorpamplona/amethyst/ui/note/types/PinList.kt create mode 100644 app/src/main/java/com/vitorpamplona/amethyst/ui/note/types/Poll.kt create mode 100644 app/src/main/java/com/vitorpamplona/amethyst/ui/note/types/PrivateMessage.kt create mode 100644 app/src/main/java/com/vitorpamplona/amethyst/ui/note/types/Reaction.kt create mode 100644 app/src/main/java/com/vitorpamplona/amethyst/ui/note/types/RelayList.kt create mode 100644 app/src/main/java/com/vitorpamplona/amethyst/ui/note/types/RenderPostApproval.kt create mode 100644 app/src/main/java/com/vitorpamplona/amethyst/ui/note/types/Report.kt create mode 100644 app/src/main/java/com/vitorpamplona/amethyst/ui/note/types/Text.kt create mode 100644 app/src/main/java/com/vitorpamplona/amethyst/ui/note/types/TextModification.kt create mode 100644 app/src/main/java/com/vitorpamplona/amethyst/ui/note/types/Video.kt diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/EditPostView.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/EditPostView.kt index 177d858cc..70d6db904 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/EditPostView.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/EditPostView.kt @@ -385,7 +385,7 @@ fun EditPostView( fontWeight = FontWeight.W500, ) - Divider() + HorizontalDivider(thickness = DividerThickness) MyTextField( value = postViewModel.subject, diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/JoinUserOrChannelView.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/JoinUserOrChannelView.kt index 8406c09bd..d6bf6f367 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/JoinUserOrChannelView.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/JoinUserOrChannelView.kt @@ -37,7 +37,7 @@ import androidx.compose.foundation.lazy.itemsIndexed import androidx.compose.foundation.lazy.rememberLazyListState import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.Clear -import androidx.compose.material3.Divider +import androidx.compose.material3.HorizontalDivider import androidx.compose.material3.Icon import androidx.compose.material3.IconButton import androidx.compose.material3.MaterialTheme @@ -142,7 +142,10 @@ fun JoinUserOrChannelView( ) { Surface { Column( - modifier = Modifier.padding(10.dp).heightIn(min = 500.dp), + modifier = + Modifier + .padding(10.dp) + .heightIn(min = 500.dp), ) { Row( modifier = Modifier.fillMaxWidth(), @@ -267,7 +270,10 @@ private fun SearchEditTextForJoin( } Row( - modifier = Modifier.padding(horizontal = 10.dp).fillMaxWidth(), + modifier = + Modifier + .padding(horizontal = 10.dp) + .fillMaxWidth(), horizontalArrangement = Arrangement.SpaceBetween, verticalAlignment = Alignment.CenterVertically, ) { @@ -280,7 +286,8 @@ private fun SearchEditTextForJoin( }, leadingIcon = { SearchIcon(modifier = Size20Modifier, Color.Unspecified) }, modifier = - Modifier.weight(1f, true) + Modifier + .weight(1f, true) .defaultMinSize(minHeight = 20.dp) .focusRequester(focusRequester) .onFocusChanged { @@ -330,7 +337,11 @@ private fun RenderSearchResults( } Row( - modifier = Modifier.fillMaxWidth().fillMaxHeight().padding(vertical = 10.dp), + modifier = + Modifier + .fillMaxWidth() + .fillMaxHeight() + .padding(vertical = 10.dp), ) { LazyColumn( modifier = Modifier.fillMaxHeight(), @@ -411,7 +422,10 @@ fun UserComposeForChat( ClickableUserPicture(baseUser, Size55dp, accountViewModel) Column( - modifier = Modifier.padding(start = 10.dp).weight(1f), + modifier = + Modifier + .padding(start = 10.dp) + .weight(1f), ) { Row(verticalAlignment = Alignment.CenterVertically) { UsernameDisplay(baseUser) } @@ -419,7 +433,7 @@ fun UserComposeForChat( } } - Divider( + HorizontalDivider( modifier = Modifier.padding(top = 10.dp), thickness = DividerThickness, ) diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewPostView.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewPostView.kt index 1518064a0..563fde46d 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewPostView.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewPostView.kt @@ -70,8 +70,8 @@ import androidx.compose.material.icons.outlined.Bolt import androidx.compose.material.icons.rounded.Warning import androidx.compose.material3.Button import androidx.compose.material3.ButtonDefaults -import androidx.compose.material3.Divider import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.HorizontalDivider import androidx.compose.material3.Icon import androidx.compose.material3.IconButton import androidx.compose.material3.LocalTextStyle @@ -154,6 +154,7 @@ import com.vitorpamplona.amethyst.ui.screen.loggedIn.TextSpinner import com.vitorpamplona.amethyst.ui.screen.loggedIn.TitleExplainer import com.vitorpamplona.amethyst.ui.theme.BitcoinOrange import com.vitorpamplona.amethyst.ui.theme.ButtonBorder +import com.vitorpamplona.amethyst.ui.theme.DividerThickness import com.vitorpamplona.amethyst.ui.theme.DoubleHorzSpacer import com.vitorpamplona.amethyst.ui.theme.Font14SP import com.vitorpamplona.amethyst.ui.theme.QuoteBorder @@ -725,7 +726,7 @@ fun ContentSensitivityExplainer(postViewModel: NewPostViewModel) { ) } - Divider() + HorizontalDivider(thickness = DividerThickness) Text( text = stringResource(R.string.add_sensitive_content_explainer), @@ -772,7 +773,7 @@ fun SendDirectMessageTo(postViewModel: NewPostViewModel) { ) } - Divider() + HorizontalDivider(thickness = DividerThickness) Row( verticalAlignment = Alignment.CenterVertically, @@ -806,7 +807,7 @@ fun SendDirectMessageTo(postViewModel: NewPostViewModel) { ) } - Divider() + HorizontalDivider(thickness = DividerThickness) } } @@ -847,7 +848,7 @@ fun SellProduct(postViewModel: NewPostViewModel) { ) } - Divider() + HorizontalDivider(thickness = DividerThickness) Row( verticalAlignment = Alignment.CenterVertically, @@ -890,7 +891,7 @@ fun SellProduct(postViewModel: NewPostViewModel) { ) } - Divider() + HorizontalDivider(thickness = DividerThickness) Row( verticalAlignment = Alignment.CenterVertically, @@ -955,7 +956,7 @@ fun SellProduct(postViewModel: NewPostViewModel) { } } - Divider() + HorizontalDivider(thickness = DividerThickness) Row( verticalAlignment = Alignment.CenterVertically, @@ -1019,7 +1020,7 @@ fun SellProduct(postViewModel: NewPostViewModel) { } } - Divider() + HorizontalDivider(thickness = DividerThickness) Row( verticalAlignment = Alignment.CenterVertically, @@ -1053,7 +1054,7 @@ fun SellProduct(postViewModel: NewPostViewModel) { ) } - Divider() + HorizontalDivider(thickness = DividerThickness) } } @@ -1105,7 +1106,7 @@ fun FowardZapTo( ) } - Divider() + HorizontalDivider(thickness = DividerThickness) Text( text = stringResource(R.string.zap_split_explainer), @@ -1209,7 +1210,7 @@ fun LocationAsHash(postViewModel: NewPostViewModel) { DisplayLocationObserver(postViewModel) } - Divider() + HorizontalDivider(thickness = DividerThickness) Text( text = stringResource(R.string.geohash_explainer), @@ -1722,7 +1723,7 @@ fun ImageVideoDescription( } } - Divider() + HorizontalDivider(thickness = DividerThickness) Row( verticalAlignment = Alignment.CenterVertically, diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/components/CashuRedeem.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/components/CashuRedeem.kt index f7c1009ba..739a470fd 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/components/CashuRedeem.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/components/CashuRedeem.kt @@ -35,8 +35,8 @@ import androidx.compose.foundation.layout.size import androidx.compose.material3.Button import androidx.compose.material3.ButtonDefaults import androidx.compose.material3.Card -import androidx.compose.material3.Divider import androidx.compose.material3.FilledTonalButton +import androidx.compose.material3.HorizontalDivider import androidx.compose.material3.Icon import androidx.compose.material3.LocalTextStyle import androidx.compose.material3.MaterialTheme @@ -76,6 +76,7 @@ import com.vitorpamplona.amethyst.ui.note.ZapIcon import com.vitorpamplona.amethyst.ui.screen.SharedPreferencesViewModel import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel import com.vitorpamplona.amethyst.ui.theme.AmethystTheme +import com.vitorpamplona.amethyst.ui.theme.DividerThickness import com.vitorpamplona.amethyst.ui.theme.DoubleHorzSpacer import com.vitorpamplona.amethyst.ui.theme.QuoteBorder import com.vitorpamplona.amethyst.ui.theme.Size18Modifier @@ -195,7 +196,7 @@ fun CashuPreview( ) } - Divider() + HorizontalDivider(thickness = DividerThickness) Text( text = "${token.totalAmount} ${stringResource(id = R.string.sats)}", diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/components/InvoicePreview.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/components/InvoicePreview.kt index e3afdfa93..1b2a8efd1 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/components/InvoicePreview.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/components/InvoicePreview.kt @@ -28,7 +28,7 @@ import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding import androidx.compose.material3.Button import androidx.compose.material3.ButtonDefaults -import androidx.compose.material3.Divider +import androidx.compose.material3.HorizontalDivider import androidx.compose.material3.Icon import androidx.compose.material3.LocalTextStyle import androidx.compose.material3.MaterialTheme @@ -55,6 +55,7 @@ import com.vitorpamplona.amethyst.service.lnurl.CachedLnInvoiceParser import com.vitorpamplona.amethyst.service.lnurl.InvoiceAmount import com.vitorpamplona.amethyst.ui.note.ErrorMessageDialog import com.vitorpamplona.amethyst.ui.note.payViaIntent +import com.vitorpamplona.amethyst.ui.theme.DividerThickness import com.vitorpamplona.amethyst.ui.theme.QuoteBorder import com.vitorpamplona.amethyst.ui.theme.Size20Modifier import com.vitorpamplona.amethyst.ui.theme.subtleBorder @@ -148,7 +149,7 @@ fun InvoicePreview( ) } - Divider() + HorizontalDivider(thickness = DividerThickness) amount?.let { Text( diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/components/InvoiceRequest.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/components/InvoiceRequest.kt index 12a91502a..22b913d43 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/components/InvoiceRequest.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/components/InvoiceRequest.kt @@ -28,7 +28,7 @@ import androidx.compose.foundation.layout.padding import androidx.compose.foundation.text.KeyboardOptions import androidx.compose.material3.Button import androidx.compose.material3.ButtonDefaults -import androidx.compose.material3.Divider +import androidx.compose.material3.HorizontalDivider import androidx.compose.material3.Icon import androidx.compose.material3.MaterialTheme import androidx.compose.material3.OutlinedTextField @@ -55,6 +55,7 @@ import com.vitorpamplona.amethyst.R import com.vitorpamplona.amethyst.model.Account import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.amethyst.service.lnurl.LightningAddressResolver +import com.vitorpamplona.amethyst.ui.theme.DividerThickness import com.vitorpamplona.amethyst.ui.theme.QuoteBorder import com.vitorpamplona.amethyst.ui.theme.Size20Modifier import com.vitorpamplona.amethyst.ui.theme.placeholderText @@ -131,7 +132,7 @@ fun InvoiceRequest( ) } - Divider() + HorizontalDivider(thickness = DividerThickness) var message by remember { mutableStateOf("") } var amount by remember { mutableStateOf(1000L) } diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/components/SelectTextDialog.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/components/SelectTextDialog.kt index afda98789..52b3844c7 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/components/SelectTextDialog.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/components/SelectTextDialog.kt @@ -30,7 +30,7 @@ import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.text.selection.SelectionContainer import androidx.compose.foundation.verticalScroll import androidx.compose.material3.Card -import androidx.compose.material3.Divider +import androidx.compose.material3.HorizontalDivider import androidx.compose.material3.IconButton import androidx.compose.material3.Text import androidx.compose.runtime.Composable @@ -42,6 +42,7 @@ import androidx.compose.ui.unit.dp import androidx.compose.ui.window.Dialog import com.vitorpamplona.amethyst.R import com.vitorpamplona.amethyst.ui.note.ArrowBackIcon +import com.vitorpamplona.amethyst.ui.theme.DividerThickness import com.vitorpamplona.amethyst.ui.theme.Size24dp @Composable @@ -75,7 +76,7 @@ fun SelectTextDialog( } Text(text = stringResource(R.string.select_text_dialog_top)) } - Divider() + HorizontalDivider(thickness = DividerThickness) Column( modifier = Modifier.verticalScroll(rememberScrollState()), ) { diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/components/TextSpinner.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/components/TextSpinner.kt index 8c2141b05..aceca0ecf 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/components/TextSpinner.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/components/TextSpinner.kt @@ -34,7 +34,7 @@ import androidx.compose.foundation.layout.padding import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.itemsIndexed import androidx.compose.foundation.shape.RoundedCornerShape -import androidx.compose.material3.Divider +import androidx.compose.material3.HorizontalDivider import androidx.compose.material3.MaterialTheme import androidx.compose.material3.OutlinedTextField import androidx.compose.material3.Surface @@ -182,7 +182,7 @@ fun SpinnerSelectionDialog( fontWeight = FontWeight.Bold, ) } - Divider(color = Color.LightGray, thickness = DividerThickness) + HorizontalDivider(color = Color.LightGray, thickness = DividerThickness) } } itemsIndexed(options) { index, item -> @@ -192,7 +192,7 @@ fun SpinnerSelectionDialog( Column { onRenderItem(item) } } if (index < options.lastIndex) { - Divider(color = Color.LightGray, thickness = DividerThickness) + HorizontalDivider(color = Color.LightGray, thickness = DividerThickness) } } } diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/components/ZapRaiserRequest.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/components/ZapRaiserRequest.kt index 428e1086f..8d59c824c 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/components/ZapRaiserRequest.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/components/ZapRaiserRequest.kt @@ -25,7 +25,7 @@ import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding import androidx.compose.foundation.text.KeyboardOptions -import androidx.compose.material3.Divider +import androidx.compose.material3.HorizontalDivider import androidx.compose.material3.Icon import androidx.compose.material3.MaterialTheme import androidx.compose.material3.OutlinedTextField @@ -42,6 +42,7 @@ import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import com.vitorpamplona.amethyst.R import com.vitorpamplona.amethyst.ui.actions.NewPostViewModel +import com.vitorpamplona.amethyst.ui.theme.DividerThickness import com.vitorpamplona.amethyst.ui.theme.Size20Modifier import com.vitorpamplona.amethyst.ui.theme.placeholderText @@ -72,7 +73,7 @@ fun ZapRaiserRequest( ) } - Divider() + HorizontalDivider(thickness = DividerThickness) Text( text = stringResource(R.string.zapraiser_explainer), diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/layouts/ChatHeaderLayout.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/layouts/ChatHeaderLayout.kt index d40a92afe..ec810f28f 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/layouts/ChatHeaderLayout.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/layouts/ChatHeaderLayout.kt @@ -28,7 +28,7 @@ import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.RowScope import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.material3.Divider +import androidx.compose.material3.HorizontalDivider import androidx.compose.material3.ListItem import androidx.compose.material3.Text import androidx.compose.runtime.Composable @@ -41,7 +41,7 @@ import androidx.compose.ui.res.stringResource import androidx.compose.ui.tooling.preview.Preview import com.vitorpamplona.amethyst.R import com.vitorpamplona.amethyst.ui.note.NewItemsBubble -import com.vitorpamplona.amethyst.ui.note.TimeAgo +import com.vitorpamplona.amethyst.ui.note.elements.TimeAgo import com.vitorpamplona.amethyst.ui.theme.ChatHeadlineBorders import com.vitorpamplona.amethyst.ui.theme.DividerThickness import com.vitorpamplona.amethyst.ui.theme.DoubleHorzSpacer @@ -73,7 +73,7 @@ fun ChannelNamePreview() { onClick = {}, ) - Divider() + HorizontalDivider(thickness = DividerThickness) ListItem( headlineContent = { @@ -135,7 +135,7 @@ fun ChatHeaderLayout( } } - Divider( + HorizontalDivider( modifier = StdTopPadding, thickness = DividerThickness, ) diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/navigation/AppBottomBar.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/navigation/AppBottomBar.kt index 102a143eb..008800784 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/navigation/AppBottomBar.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/navigation/AppBottomBar.kt @@ -27,7 +27,7 @@ import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.RowScope import androidx.compose.foundation.layout.size -import androidx.compose.material3.Divider +import androidx.compose.material3.HorizontalDivider import androidx.compose.material3.Icon import androidx.compose.material3.MaterialTheme import androidx.compose.material3.NavigationBar @@ -132,7 +132,7 @@ private fun RenderBottomMenu( nav: (Route, Boolean) -> Unit, ) { Column(modifier = BottomTopHeight) { - Divider( + HorizontalDivider( thickness = DividerThickness, ) NavigationBar(tonalElevation = Size0dp) { diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/navigation/AppTopBar.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/navigation/AppTopBar.kt index ca5e7ff4c..e661a20e9 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/navigation/AppTopBar.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/navigation/AppTopBar.kt @@ -39,9 +39,9 @@ import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.verticalScroll import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.ExpandMore -import androidx.compose.material3.Divider import androidx.compose.material3.DrawerState import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.HorizontalDivider import androidx.compose.material3.Icon import androidx.compose.material3.IconButton import androidx.compose.material3.MaterialTheme @@ -107,12 +107,12 @@ import com.vitorpamplona.amethyst.ui.note.LoadAddressableNote import com.vitorpamplona.amethyst.ui.note.LoadChannel import com.vitorpamplona.amethyst.ui.note.LoadCityName import com.vitorpamplona.amethyst.ui.note.LoadUser -import com.vitorpamplona.amethyst.ui.note.LongCommunityHeader import com.vitorpamplona.amethyst.ui.note.NonClickableUserPictures import com.vitorpamplona.amethyst.ui.note.SearchIcon -import com.vitorpamplona.amethyst.ui.note.ShortCommunityHeader import com.vitorpamplona.amethyst.ui.note.UserCompose import com.vitorpamplona.amethyst.ui.note.UsernameDisplay +import com.vitorpamplona.amethyst.ui.note.types.LongCommunityHeader +import com.vitorpamplona.amethyst.ui.note.types.ShortCommunityHeader import com.vitorpamplona.amethyst.ui.screen.equalImmutableLists import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel import com.vitorpamplona.amethyst.ui.screen.loggedIn.DislayGeoTagHeader @@ -394,8 +394,6 @@ private fun ChannelTopBar( } } -@Composable fun NoTopBar() {} - @Composable fun StoriesTopBar( followLists: FollowListViewModel, @@ -403,7 +401,7 @@ fun StoriesTopBar( accountViewModel: AccountViewModel, nav: (String) -> Unit, ) { - GenericMainTopBar(drawerState, accountViewModel, nav) { accountViewModel -> + GenericMainTopBar(drawerState, accountViewModel, nav) { val list by accountViewModel.account.defaultStoriesFollowList.collectAsStateWithLifecycle() FollowListWithRoutes( @@ -422,7 +420,7 @@ fun HomeTopBar( accountViewModel: AccountViewModel, nav: (String) -> Unit, ) { - GenericMainTopBar(drawerState, accountViewModel, nav) { accountViewModel -> + GenericMainTopBar(drawerState, accountViewModel, nav) { val list by accountViewModel.account.defaultHomeFollowList.collectAsStateWithLifecycle() FollowListWithRoutes( @@ -445,7 +443,7 @@ fun NotificationTopBar( accountViewModel: AccountViewModel, nav: (String) -> Unit, ) { - GenericMainTopBar(drawerState, accountViewModel, nav) { accountViewModel -> + GenericMainTopBar(drawerState, accountViewModel, nav) { val list by accountViewModel.account.defaultNotificationFollowList.collectAsStateWithLifecycle() FollowListWithoutRoutes( @@ -464,7 +462,7 @@ fun DiscoveryTopBar( accountViewModel: AccountViewModel, nav: (String) -> Unit, ) { - GenericMainTopBar(drawerState, accountViewModel, nav) { accountViewModel -> + GenericMainTopBar(drawerState, accountViewModel, nav) { val list by accountViewModel.account.defaultDiscoveryFollowList.collectAsStateWithLifecycle() FollowListWithoutRoutes( @@ -491,7 +489,7 @@ fun GenericMainTopBar( drawerState: DrawerState, accountViewModel: AccountViewModel, nav: (String) -> Unit, - content: @Composable (AccountViewModel) -> Unit, + content: @Composable () -> Unit, ) { Column(modifier = BottomTopHeight) { TopAppBar( @@ -510,7 +508,7 @@ fun GenericMainTopBar( horizontalAlignment = Alignment.CenterHorizontally, verticalArrangement = Arrangement.Center, ) { - content(accountViewModel) + content() } } } @@ -521,7 +519,7 @@ fun GenericMainTopBar( }, actions = { SearchButton { nav(Route.Search.route) } }, ) - Divider(thickness = DividerThickness) + HorizontalDivider(thickness = DividerThickness) } } @@ -889,7 +887,7 @@ fun TopBarWithBackButton( }, actions = {}, ) - Divider(thickness = DividerThickness) + HorizontalDivider(thickness = DividerThickness) } } @@ -907,7 +905,7 @@ fun FlexibleTopBarWithBackButton( actions = {}, ) Spacer(modifier = HalfVertSpacer) - Divider(thickness = DividerThickness) + HorizontalDivider(thickness = DividerThickness) } } @@ -1064,8 +1062,6 @@ fun MyExtensibleTopAppBar( } } -private val AppBarHeight = 50.dp - // TODO: this should probably be part of the touch target of the start and end icons, clarify this private val AppBarHorizontalPadding = 4.dp diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/navigation/DrawerContent.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/navigation/DrawerContent.kt index 9a72230aa..fb24a1803 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/navigation/DrawerContent.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/navigation/DrawerContent.kt @@ -45,8 +45,8 @@ import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.Delete import androidx.compose.material.icons.filled.Send import androidx.compose.material3.AlertDialog -import androidx.compose.material3.Divider import androidx.compose.material3.DrawerState +import androidx.compose.material3.HorizontalDivider import androidx.compose.material3.Icon import androidx.compose.material3.IconButton import androidx.compose.material3.MaterialTheme @@ -153,7 +153,7 @@ fun DrawerContent( FollowingAndFollowerCounts(accountViewModel.account.userProfile(), onClickUser) - Divider( + HorizontalDivider( thickness = DividerThickness, modifier = Modifier.padding(top = 20.dp), ) @@ -732,7 +732,7 @@ fun BottomContent( var dialogOpen by remember { mutableStateOf(false) } Column(modifier = Modifier) { - Divider( + HorizontalDivider( modifier = Modifier.padding(top = 15.dp), thickness = DividerThickness, ) diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/note/BadgeCompose.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/BadgeCompose.kt index 30e2c7759..d6fef2970 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/note/BadgeCompose.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/BadgeCompose.kt @@ -32,7 +32,7 @@ import androidx.compose.foundation.layout.width import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.MilitaryTech import androidx.compose.material.icons.filled.MoreVert -import androidx.compose.material3.Divider +import androidx.compose.material3.HorizontalDivider import androidx.compose.material3.Icon import androidx.compose.material3.IconButton import androidx.compose.material3.MaterialTheme @@ -54,6 +54,7 @@ import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.unit.dp import com.vitorpamplona.amethyst.R import com.vitorpamplona.amethyst.ui.navigation.routeFor +import com.vitorpamplona.amethyst.ui.note.elements.NoteDropDownMenu import com.vitorpamplona.amethyst.ui.screen.BadgeCard import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel import com.vitorpamplona.amethyst.ui.theme.DividerThickness @@ -183,7 +184,7 @@ fun BadgeCompose( ) } - Divider( + HorizontalDivider( modifier = Modifier.padding(top = 10.dp), thickness = DividerThickness, ) diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/note/BlankNote.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/BlankNote.kt index 22e42d5aa..6774fde06 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/note/BlankNote.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/BlankNote.kt @@ -28,7 +28,6 @@ import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.padding import androidx.compose.material3.Button import androidx.compose.material3.ButtonDefaults -import androidx.compose.material3.Divider import androidx.compose.material3.HorizontalDivider import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text @@ -183,7 +182,7 @@ fun HiddenNote( } } - Divider( + HorizontalDivider( thickness = DividerThickness, ) } @@ -235,7 +234,7 @@ fun HiddenNoteByMe( } if (!isQuote) { - Divider( + HorizontalDivider( thickness = DividerThickness, ) } diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/note/ChannelCardCompose.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/ChannelCardCompose.kt index e1c59eede..7443a653f 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/note/ChannelCardCompose.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/ChannelCardCompose.kt @@ -36,7 +36,7 @@ import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size -import androidx.compose.material3.Divider +import androidx.compose.material3.HorizontalDivider import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Surface import androidx.compose.material3.Text @@ -400,7 +400,7 @@ fun InnerCardRow( } } - Divider( + HorizontalDivider( thickness = DividerThickness, ) } diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/note/Loaders.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/Loaders.kt new file mode 100644 index 000000000..4ec5f2eac --- /dev/null +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/Loaders.kt @@ -0,0 +1,243 @@ +/** + * 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 androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue +import androidx.compose.runtime.livedata.observeAsState +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.platform.LocalContext +import com.fonfon.kgeohash.toGeoHash +import com.vitorpamplona.amethyst.model.AddressableNote +import com.vitorpamplona.amethyst.model.Channel +import com.vitorpamplona.amethyst.model.Note +import com.vitorpamplona.amethyst.model.User +import com.vitorpamplona.amethyst.service.CachedGeoLocations +import com.vitorpamplona.amethyst.ui.components.GenericLoadable +import com.vitorpamplona.amethyst.ui.screen.equalImmutableLists +import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel +import com.vitorpamplona.quartz.encoders.ATag +import kotlinx.collections.immutable.ImmutableList +import kotlinx.collections.immutable.persistentListOf +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch + +@Composable +fun LoadDecryptedContent( + note: Note, + accountViewModel: AccountViewModel, + inner: @Composable (String) -> Unit, +) { + var decryptedContent by + remember(note.event) { + mutableStateOf( + accountViewModel.cachedDecrypt(note), + ) + } + + decryptedContent?.let { inner(it) } + ?: run { + LaunchedEffect(key1 = decryptedContent) { + accountViewModel.decrypt(note) { decryptedContent = it } + } + } +} + +@Composable +fun LoadDecryptedContentOrNull( + note: Note, + accountViewModel: AccountViewModel, + inner: @Composable (String?) -> Unit, +) { + var decryptedContent by + remember(note.event) { + mutableStateOf( + accountViewModel.cachedDecrypt(note), + ) + } + + if (decryptedContent == null) { + LaunchedEffect(key1 = decryptedContent) { + accountViewModel.decrypt(note) { decryptedContent = it } + } + } + + inner(decryptedContent) +} + +@Composable +fun LoadAddressableNote( + aTagHex: String, + accountViewModel: AccountViewModel, + content: @Composable (AddressableNote?) -> Unit, +) { + var note by + remember(aTagHex) { + mutableStateOf(accountViewModel.getAddressableNoteIfExists(aTagHex)) + } + + if (note == null) { + LaunchedEffect(key1 = aTagHex) { + accountViewModel.checkGetOrCreateAddressableNote(aTagHex) { newNote -> + if (newNote != note) { + note = newNote + } + } + } + } + + content(note) +} + +@Composable +fun LoadAddressableNote( + aTag: ATag, + accountViewModel: AccountViewModel, + content: @Composable (AddressableNote?) -> Unit, +) { + var note by + remember(aTag) { + mutableStateOf(accountViewModel.getAddressableNoteIfExists(aTag.toTag())) + } + + if (note == null) { + LaunchedEffect(key1 = aTag) { + accountViewModel.getOrCreateAddressableNote(aTag) { newNote -> + if (newNote != note) { + note = newNote + } + } + } + } + + content(note) +} + +@Composable +fun LoadStatuses( + user: User, + accountViewModel: AccountViewModel, + content: @Composable (ImmutableList) -> Unit, +) { + var statuses: ImmutableList by remember { mutableStateOf(persistentListOf()) } + + val userStatus by user.live().statuses.observeAsState() + + LaunchedEffect(key1 = userStatus) { + accountViewModel.findStatusesForUser(userStatus?.user ?: user) { newStatuses -> + if (!equalImmutableLists(statuses, newStatuses)) { + statuses = newStatuses + } + } + } + + content(statuses) +} + +@Composable +fun LoadOts( + note: Note, + accountViewModel: AccountViewModel, + whenConfirmed: @Composable (Long) -> Unit, + whenPending: @Composable () -> Unit, +) { + var earliestDate: GenericLoadable by remember { mutableStateOf(GenericLoadable.Loading()) } + + val noteStatus by note.live().innerOts.observeAsState() + + LaunchedEffect(key1 = noteStatus) { + accountViewModel.findOtsEventsForNote(noteStatus?.note ?: note) { newOts -> + earliestDate = + if (newOts == null) { + GenericLoadable.Empty() + } else { + GenericLoadable.Loaded(newOts) + } + } + } + + (earliestDate as? GenericLoadable.Loaded)?.let { + whenConfirmed(it.loaded) + } ?: run { + val account = accountViewModel.account.saveable.observeAsState() + if (account.value?.account?.hasPendingAttestations(note) == true) { + whenPending() + } + } +} + +@Composable +fun LoadCityName( + geohashStr: String, + onLoading: (@Composable () -> Unit)? = null, + content: @Composable (String) -> Unit, +) { + var cityName by remember(geohashStr) { mutableStateOf(CachedGeoLocations.cached(geohashStr)) } + + if (cityName == null) { + if (onLoading != null) { + onLoading() + } + + val context = LocalContext.current + + LaunchedEffect(key1 = geohashStr, context) { + launch(Dispatchers.IO) { + val geoHash = runCatching { geohashStr.toGeoHash() }.getOrNull() + if (geoHash != null) { + val newCityName = + CachedGeoLocations.geoLocate(geohashStr, geoHash.toLocation(), context) + ?.ifBlank { null } + if (newCityName != null && newCityName != cityName) { + cityName = newCityName + } + } + } + } + } else { + cityName?.let { content(it) } + } +} + +@Composable +fun LoadChannel( + baseChannelHex: String, + accountViewModel: AccountViewModel, + content: @Composable (Channel) -> Unit, +) { + var channel by + remember(baseChannelHex) { + mutableStateOf(accountViewModel.getChannelIfExists(baseChannelHex)) + } + + if (channel == null) { + LaunchedEffect(key1 = baseChannelHex) { + accountViewModel.checkGetOrCreateChannel(baseChannelHex) { newChannel -> + launch(Dispatchers.Main) { channel = newChannel } + } + } + } + + channel?.let { content(it) } +} diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/note/MessageSetCompose.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/MessageSetCompose.kt index c48dc99d7..e5beeccf9 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/note/MessageSetCompose.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/MessageSetCompose.kt @@ -30,7 +30,7 @@ import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.width -import androidx.compose.material3.Divider +import androidx.compose.material3.HorizontalDivider import androidx.compose.material3.MaterialTheme import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect @@ -43,6 +43,7 @@ import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.compositeOver import androidx.compose.ui.unit.dp import com.vitorpamplona.amethyst.ui.navigation.routeFor +import com.vitorpamplona.amethyst.ui.note.elements.NoteDropDownMenu import com.vitorpamplona.amethyst.ui.screen.MessageSetCard import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel import com.vitorpamplona.amethyst.ui.theme.DividerThickness @@ -133,7 +134,7 @@ fun MessageSetCompose( } } - Divider( + HorizontalDivider( thickness = DividerThickness, ) } diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/note/MultiSetCompose.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/MultiSetCompose.kt index 15e302737..5a25e557e 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/note/MultiSetCompose.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/MultiSetCompose.kt @@ -36,7 +36,7 @@ import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.width import androidx.compose.foundation.shape.CircleShape -import androidx.compose.material3.Divider +import androidx.compose.material3.HorizontalDivider import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text import androidx.compose.runtime.Composable @@ -70,6 +70,7 @@ import com.vitorpamplona.amethyst.ui.components.RobohashFallbackAsyncImage import com.vitorpamplona.amethyst.ui.components.TranslatableRichTextViewer import com.vitorpamplona.amethyst.ui.navigation.authorRouteFor import com.vitorpamplona.amethyst.ui.navigation.routeFor +import com.vitorpamplona.amethyst.ui.note.elements.NoteDropDownMenu import com.vitorpamplona.amethyst.ui.screen.CombinedZap import com.vitorpamplona.amethyst.ui.screen.MultiSetCard import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel @@ -170,7 +171,7 @@ fun MultiSetCompose( NoteDropDownMenu(baseNote, popupExpanded, null, accountViewModel, nav) } - Divider( + HorizontalDivider( thickness = DividerThickness, ) } 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 295462b07..9ad4e2e6d 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 @@ -20,174 +20,105 @@ */ package com.vitorpamplona.amethyst.ui.note -import android.graphics.Bitmap -import android.util.Log import androidx.compose.animation.Crossfade import androidx.compose.foundation.ExperimentalFoundationApi -import androidx.compose.foundation.Image import androidx.compose.foundation.background -import androidx.compose.foundation.border -import androidx.compose.foundation.clickable import androidx.compose.foundation.combinedClickable -import androidx.compose.foundation.layout.Arrangement 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 import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.size -import androidx.compose.foundation.layout.width -import androidx.compose.foundation.rememberScrollState -import androidx.compose.foundation.shape.CircleShape -import androidx.compose.foundation.shape.CutCornerShape -import androidx.compose.foundation.text.ClickableText -import androidx.compose.foundation.verticalScroll -import androidx.compose.material3.Button -import androidx.compose.material3.Card -import androidx.compose.material3.Divider import androidx.compose.material3.HorizontalDivider -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.Stable import androidx.compose.runtime.State import androidx.compose.runtime.derivedStateOf import androidx.compose.runtime.getValue import androidx.compose.runtime.livedata.observeAsState import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.produceState 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.CenterVertically import androidx.compose.ui.Modifier -import androidx.compose.ui.draw.clip import androidx.compose.ui.graphics.Brush import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.compositeOver -import androidx.compose.ui.graphics.luminance -import androidx.compose.ui.layout.ContentScale -import androidx.compose.ui.platform.LocalClipboardManager -import androidx.compose.ui.platform.LocalContext -import androidx.compose.ui.platform.LocalUriHandler -import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource -import androidx.compose.ui.text.AnnotatedString -import androidx.compose.ui.text.TextStyle -import androidx.compose.ui.text.buildAnnotatedString -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 -import androidx.core.graphics.get import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.lifecycle.distinctUntilChanged import androidx.lifecycle.map -import coil.compose.AsyncImage -import coil.compose.AsyncImagePainter -import coil.request.SuccessResult -import com.fonfon.kgeohash.toGeoHash import com.vitorpamplona.amethyst.R -import com.vitorpamplona.amethyst.commons.BaseMediaContent -import com.vitorpamplona.amethyst.commons.MediaLocalImage -import com.vitorpamplona.amethyst.commons.MediaLocalVideo -import com.vitorpamplona.amethyst.commons.MediaUrlImage -import com.vitorpamplona.amethyst.commons.MediaUrlVideo -import com.vitorpamplona.amethyst.commons.RichTextParser import com.vitorpamplona.amethyst.model.AddressableNote -import com.vitorpamplona.amethyst.model.Bundle import com.vitorpamplona.amethyst.model.Channel -import com.vitorpamplona.amethyst.model.FhirElementDatabase -import com.vitorpamplona.amethyst.model.LensSpecification import com.vitorpamplona.amethyst.model.Note -import com.vitorpamplona.amethyst.model.Patient -import com.vitorpamplona.amethyst.model.Practitioner -import com.vitorpamplona.amethyst.model.RelayBriefInfoCache -import com.vitorpamplona.amethyst.model.Resource -import com.vitorpamplona.amethyst.model.User -import com.vitorpamplona.amethyst.model.VisionPrescription -import com.vitorpamplona.amethyst.model.findReferenceInDb -import com.vitorpamplona.amethyst.model.parseResourceBundleOrNull -import com.vitorpamplona.amethyst.service.CachedGeoLocations -import com.vitorpamplona.amethyst.ui.actions.EditPostView -import com.vitorpamplona.amethyst.ui.actions.NewRelayListView -import com.vitorpamplona.amethyst.ui.components.ClickableUrl -import com.vitorpamplona.amethyst.ui.components.CreateClickableTextWithEmoji -import com.vitorpamplona.amethyst.ui.components.CreateTextWithEmoji import com.vitorpamplona.amethyst.ui.components.GenericLoadable -import com.vitorpamplona.amethyst.ui.components.LoadNote -import com.vitorpamplona.amethyst.ui.components.LoadThumbAndThenVideoView import com.vitorpamplona.amethyst.ui.components.ObserveDisplayNip05Status import com.vitorpamplona.amethyst.ui.components.RobohashFallbackAsyncImage -import com.vitorpamplona.amethyst.ui.components.SensitivityWarning -import com.vitorpamplona.amethyst.ui.components.ShowMoreButton -import com.vitorpamplona.amethyst.ui.components.TranslatableRichTextViewer -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.elements.AddButton -import com.vitorpamplona.amethyst.ui.elements.DisplayFollowingCommunityInPost -import com.vitorpamplona.amethyst.ui.elements.DisplayFollowingHashtagsInPost -import com.vitorpamplona.amethyst.ui.elements.DisplayPoW -import com.vitorpamplona.amethyst.ui.elements.DisplayReward -import com.vitorpamplona.amethyst.ui.elements.DisplayUncitedHashtags -import com.vitorpamplona.amethyst.ui.elements.DisplayZapSplits -import com.vitorpamplona.amethyst.ui.elements.RemoveButton -import com.vitorpamplona.amethyst.ui.elements.Reward import com.vitorpamplona.amethyst.ui.layouts.GenericRepostLayout import com.vitorpamplona.amethyst.ui.navigation.routeFor -import com.vitorpamplona.amethyst.ui.screen.equalImmutableLists +import com.vitorpamplona.amethyst.ui.note.elements.BoostedMark +import com.vitorpamplona.amethyst.ui.note.elements.DisplayEditStatus +import com.vitorpamplona.amethyst.ui.note.elements.DisplayFollowingCommunityInPost +import com.vitorpamplona.amethyst.ui.note.elements.DisplayFollowingHashtagsInPost +import com.vitorpamplona.amethyst.ui.note.elements.DisplayLocation +import com.vitorpamplona.amethyst.ui.note.elements.DisplayOts +import com.vitorpamplona.amethyst.ui.note.elements.DisplayPoW +import com.vitorpamplona.amethyst.ui.note.elements.DisplayReward +import com.vitorpamplona.amethyst.ui.note.elements.DisplayZapSplits +import com.vitorpamplona.amethyst.ui.note.elements.MoreOptionsButton +import com.vitorpamplona.amethyst.ui.note.elements.Reward +import com.vitorpamplona.amethyst.ui.note.elements.ShowForkInformation +import com.vitorpamplona.amethyst.ui.note.types.BadgeDisplay +import com.vitorpamplona.amethyst.ui.note.types.CommunityHeader +import com.vitorpamplona.amethyst.ui.note.types.DisplayPeopleList +import com.vitorpamplona.amethyst.ui.note.types.DisplayRelaySet +import com.vitorpamplona.amethyst.ui.note.types.EditState +import com.vitorpamplona.amethyst.ui.note.types.FileHeaderDisplay +import com.vitorpamplona.amethyst.ui.note.types.FileStorageHeaderDisplay +import com.vitorpamplona.amethyst.ui.note.types.RenderAppDefinition +import com.vitorpamplona.amethyst.ui.note.types.RenderAudioHeader +import com.vitorpamplona.amethyst.ui.note.types.RenderAudioTrack +import com.vitorpamplona.amethyst.ui.note.types.RenderBadgeAward +import com.vitorpamplona.amethyst.ui.note.types.RenderClassifieds +import com.vitorpamplona.amethyst.ui.note.types.RenderEmojiPack +import com.vitorpamplona.amethyst.ui.note.types.RenderFhirResource +import com.vitorpamplona.amethyst.ui.note.types.RenderGitIssueEvent +import com.vitorpamplona.amethyst.ui.note.types.RenderGitPatchEvent +import com.vitorpamplona.amethyst.ui.note.types.RenderGitRepositoryEvent +import com.vitorpamplona.amethyst.ui.note.types.RenderHighlight +import com.vitorpamplona.amethyst.ui.note.types.RenderLiveActivityEvent +import com.vitorpamplona.amethyst.ui.note.types.RenderLongFormContent +import com.vitorpamplona.amethyst.ui.note.types.RenderPinListEvent +import com.vitorpamplona.amethyst.ui.note.types.RenderPoll +import com.vitorpamplona.amethyst.ui.note.types.RenderPostApproval +import com.vitorpamplona.amethyst.ui.note.types.RenderPrivateMessage +import com.vitorpamplona.amethyst.ui.note.types.RenderReaction +import com.vitorpamplona.amethyst.ui.note.types.RenderReport +import com.vitorpamplona.amethyst.ui.note.types.RenderTextEvent +import com.vitorpamplona.amethyst.ui.note.types.RenderTextModificationEvent +import com.vitorpamplona.amethyst.ui.note.types.RenderWikiContent +import com.vitorpamplona.amethyst.ui.note.types.VideoDisplay import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel import com.vitorpamplona.amethyst.ui.screen.loggedIn.ChannelHeader -import com.vitorpamplona.amethyst.ui.screen.loggedIn.CheckIfUrlIsOnline -import com.vitorpamplona.amethyst.ui.screen.loggedIn.CrossfadeCheckIfUrlIsOnline -import com.vitorpamplona.amethyst.ui.screen.loggedIn.JoinCommunityButton -import com.vitorpamplona.amethyst.ui.screen.loggedIn.LeaveCommunityButton -import com.vitorpamplona.amethyst.ui.screen.loggedIn.LiveFlag -import com.vitorpamplona.amethyst.ui.screen.loggedIn.NormalTimeAgo -import com.vitorpamplona.amethyst.ui.screen.loggedIn.ScheduledFlag import com.vitorpamplona.amethyst.ui.theme.DividerThickness import com.vitorpamplona.amethyst.ui.theme.DoubleHorzSpacer import com.vitorpamplona.amethyst.ui.theme.DoubleVertSpacer -import com.vitorpamplona.amethyst.ui.theme.Font14SP import com.vitorpamplona.amethyst.ui.theme.HalfDoubleVertSpacer import com.vitorpamplona.amethyst.ui.theme.HalfPadding -import com.vitorpamplona.amethyst.ui.theme.HalfStartPadding -import com.vitorpamplona.amethyst.ui.theme.HeaderPictureModifier -import com.vitorpamplona.amethyst.ui.theme.QuoteBorder -import com.vitorpamplona.amethyst.ui.theme.Size10dp -import com.vitorpamplona.amethyst.ui.theme.Size15Modifier -import com.vitorpamplona.amethyst.ui.theme.Size16Modifier -import com.vitorpamplona.amethyst.ui.theme.Size24Modifier import com.vitorpamplona.amethyst.ui.theme.Size25dp import com.vitorpamplona.amethyst.ui.theme.Size30Modifier import com.vitorpamplona.amethyst.ui.theme.Size34dp -import com.vitorpamplona.amethyst.ui.theme.Size35Modifier -import com.vitorpamplona.amethyst.ui.theme.Size35dp import com.vitorpamplona.amethyst.ui.theme.Size55Modifier import com.vitorpamplona.amethyst.ui.theme.Size55dp -import com.vitorpamplona.amethyst.ui.theme.Size5dp -import com.vitorpamplona.amethyst.ui.theme.SmallBorder import com.vitorpamplona.amethyst.ui.theme.StdHorzSpacer -import com.vitorpamplona.amethyst.ui.theme.StdPadding import com.vitorpamplona.amethyst.ui.theme.StdVertSpacer import com.vitorpamplona.amethyst.ui.theme.UserNameMaxRowHeight import com.vitorpamplona.amethyst.ui.theme.UserNameRowHeight @@ -195,20 +126,11 @@ import com.vitorpamplona.amethyst.ui.theme.WidthAuthorPictureModifier import com.vitorpamplona.amethyst.ui.theme.boostedNoteModifier import com.vitorpamplona.amethyst.ui.theme.channelNotePictureModifier import com.vitorpamplona.amethyst.ui.theme.grayText -import com.vitorpamplona.amethyst.ui.theme.imageModifier -import com.vitorpamplona.amethyst.ui.theme.innerPostModifier -import com.vitorpamplona.amethyst.ui.theme.lessImportantLink -import com.vitorpamplona.amethyst.ui.theme.mediumImportanceLink import com.vitorpamplona.amethyst.ui.theme.newItemBackgroundColor -import com.vitorpamplona.amethyst.ui.theme.nip05 import com.vitorpamplona.amethyst.ui.theme.normalNoteModifier import com.vitorpamplona.amethyst.ui.theme.normalWithTopMarginNoteModifier -import com.vitorpamplona.amethyst.ui.theme.placeholderText import com.vitorpamplona.amethyst.ui.theme.replyBackground import com.vitorpamplona.amethyst.ui.theme.replyModifier -import com.vitorpamplona.amethyst.ui.theme.subtleBorder -import com.vitorpamplona.quartz.encoders.ATag -import com.vitorpamplona.quartz.encoders.toNpub import com.vitorpamplona.quartz.events.AppDefinitionEvent import com.vitorpamplona.quartz.events.AudioHeaderEvent import com.vitorpamplona.quartz.events.AudioTrackEvent @@ -222,10 +144,6 @@ import com.vitorpamplona.quartz.events.ClassifiedsEvent import com.vitorpamplona.quartz.events.CommunityDefinitionEvent import com.vitorpamplona.quartz.events.CommunityPostApprovalEvent 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 @@ -236,11 +154,7 @@ import com.vitorpamplona.quartz.events.GitRepositoryEvent import com.vitorpamplona.quartz.events.HighlightEvent import com.vitorpamplona.quartz.events.LiveActivitiesChatMessageEvent import com.vitorpamplona.quartz.events.LiveActivitiesEvent -import com.vitorpamplona.quartz.events.LiveActivitiesEvent.Companion.STATUS_ENDED -import com.vitorpamplona.quartz.events.LiveActivitiesEvent.Companion.STATUS_LIVE -import com.vitorpamplona.quartz.events.LiveActivitiesEvent.Companion.STATUS_PLANNED import com.vitorpamplona.quartz.events.LongTextNoteEvent -import com.vitorpamplona.quartz.events.Participant import com.vitorpamplona.quartz.events.PeopleListEvent import com.vitorpamplona.quartz.events.PinListEvent import com.vitorpamplona.quartz.events.PollNoteEvent @@ -251,26 +165,13 @@ import com.vitorpamplona.quartz.events.ReportEvent import com.vitorpamplona.quartz.events.RepostEvent import com.vitorpamplona.quartz.events.TextNoteEvent import com.vitorpamplona.quartz.events.TextNoteModificationEvent -import com.vitorpamplona.quartz.events.UserMetadata -import com.vitorpamplona.quartz.events.VideoEvent import com.vitorpamplona.quartz.events.VideoHorizontalEvent import com.vitorpamplona.quartz.events.VideoVerticalEvent import com.vitorpamplona.quartz.events.WikiNoteEvent -import com.vitorpamplona.quartz.events.toImmutableListOfLists -import kotlinx.collections.immutable.ImmutableList -import kotlinx.collections.immutable.ImmutableMap 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 @OptIn(ExperimentalFoundationApi::class) @Composable @@ -618,313 +519,6 @@ fun NormalNote( } } -@Composable -fun CommunityHeader( - baseNote: AddressableNote, - showBottomDiviser: Boolean, - sendToCommunity: Boolean, - modifier: Modifier = StdPadding, - accountViewModel: AccountViewModel, - nav: (String) -> Unit, -) { - val expanded = remember { mutableStateOf(false) } - - Column(Modifier.fillMaxWidth()) { - Column( - verticalArrangement = Arrangement.Center, - modifier = - Modifier.clickable { - if (sendToCommunity) { - routeFor(baseNote, accountViewModel.userProfile())?.let { nav(it) } - } else { - expanded.value = !expanded.value - } - }, - ) { - ShortCommunityHeader( - baseNote = baseNote, - accountViewModel = accountViewModel, - nav = nav, - ) - - if (expanded.value) { - Column(Modifier.verticalScroll(rememberScrollState())) { - LongCommunityHeader( - baseNote = baseNote, - lineModifier = modifier, - accountViewModel = accountViewModel, - nav = nav, - ) - } - } - } - - if (showBottomDiviser) { - Divider( - thickness = DividerThickness, - ) - } - } -} - -@Composable -fun LongCommunityHeader( - baseNote: AddressableNote, - lineModifier: Modifier = Modifier.padding(horizontal = Size10dp, vertical = Size5dp), - accountViewModel: AccountViewModel, - nav: (String) -> Unit, -) { - val noteState by baseNote.live().metadata.observeAsState() - val noteEvent = - remember(noteState) { noteState?.note?.event as? CommunityDefinitionEvent } ?: return - - Row( - lineModifier, - ) { - val rulesLabel = stringResource(id = R.string.rules) - val summary = - remember(noteState) { - val subject = noteEvent.subject()?.ifEmpty { null } - val body = noteEvent.description()?.ifBlank { null } - val rules = noteEvent.rules()?.ifBlank { null } - - if (!subject.isNullOrBlank() && body?.split("\n")?.get(0)?.contains(subject) == false) { - if (rules == null) { - "### $subject\n$body" - } else { - "### $subject\n$body\n\n### $rulesLabel\n\n$rules" - } - } else { - if (rules == null) { - body - } else { - "$body\n\n$rulesLabel\n$rules" - } - } - } - - Column( - Modifier.weight(1f), - ) { - Row(verticalAlignment = CenterVertically) { - val defaultBackground = MaterialTheme.colorScheme.background - val background = remember { mutableStateOf(defaultBackground) } - - TranslatableRichTextViewer( - content = summary ?: stringResource(id = R.string.community_no_descriptor), - canPreview = false, - tags = EmptyTagList, - backgroundColor = background, - accountViewModel = accountViewModel, - nav = nav, - ) - } - - if (summary != null && noteEvent.hasHashtags()) { - DisplayUncitedHashtags( - remember(noteEvent) { noteEvent.hashtags().toImmutableList() }, - summary ?: "", - nav, - ) - } - } - - Column { - Row { - Spacer(DoubleHorzSpacer) - LongCommunityActionOptions(baseNote, accountViewModel, nav) - } - } - } - - Row( - lineModifier, - verticalAlignment = CenterVertically, - ) { - Text( - text = stringResource(id = R.string.owner), - maxLines = 1, - overflow = TextOverflow.Ellipsis, - modifier = Modifier.width(75.dp), - ) - Spacer(DoubleHorzSpacer) - NoteAuthorPicture(baseNote, nav, accountViewModel, Size25dp) - Spacer(DoubleHorzSpacer) - NoteUsernameDisplay(baseNote, remember { Modifier.weight(1f) }) - } - - var participantUsers by - remember(baseNote) { - mutableStateOf>>( - persistentListOf(), - ) - } - - LaunchedEffect(key1 = noteState) { - val participants = (noteState?.note?.event as? CommunityDefinitionEvent)?.moderators() - - if (participants != null) { - accountViewModel.loadParticipants(participants) { newParticipantUsers -> - if ( - newParticipantUsers != null && !equalImmutableLists(newParticipantUsers, participantUsers) - ) { - participantUsers = newParticipantUsers - } - } - } - } - - participantUsers.forEach { - Row( - lineModifier.clickable { nav("User/${it.second.pubkeyHex}") }, - verticalAlignment = CenterVertically, - ) { - it.first.role?.let { it1 -> - Text( - text = it1.capitalize(Locale.ROOT), - maxLines = 1, - overflow = TextOverflow.Ellipsis, - modifier = Modifier.width(75.dp), - ) - } - Spacer(DoubleHorzSpacer) - ClickableUserPicture(it.second, Size25dp, accountViewModel) - Spacer(DoubleHorzSpacer) - UsernameDisplay(it.second, remember { Modifier.weight(1f) }) - } - } - - Row( - lineModifier, - verticalAlignment = CenterVertically, - ) { - Text( - text = stringResource(id = R.string.created_at), - maxLines = 1, - overflow = TextOverflow.Ellipsis, - modifier = Modifier.width(75.dp), - ) - Spacer(DoubleHorzSpacer) - NormalTimeAgo(baseNote = baseNote, Modifier.weight(1f)) - MoreOptionsButton(baseNote, null, accountViewModel, nav) - } -} - -@Composable -fun ShortCommunityHeader( - baseNote: AddressableNote, - accountViewModel: AccountViewModel, - nav: (String) -> Unit, -) { - val noteState by baseNote.live().metadata.observeAsState() - val noteEvent = - remember(noteState) { noteState?.note?.event as? CommunityDefinitionEvent } ?: return - - val automaticallyShowProfilePicture = - remember { - accountViewModel.settings.showProfilePictures.value - } - - Row(verticalAlignment = CenterVertically) { - noteEvent.image()?.let { - RobohashFallbackAsyncImage( - robot = baseNote.idHex, - model = it, - contentDescription = stringResource(R.string.profile_image), - contentScale = ContentScale.Crop, - modifier = HeaderPictureModifier, - loadProfilePicture = automaticallyShowProfilePicture, - ) - } - - Column( - modifier = - Modifier - .padding(start = 10.dp) - .height(Size35dp) - .weight(1f), - verticalArrangement = Arrangement.Center, - ) { - Row(verticalAlignment = CenterVertically) { - Text( - text = remember(noteState) { noteEvent.dTag() }, - maxLines = 1, - overflow = TextOverflow.Ellipsis, - ) - } - } - - Row( - modifier = - Modifier - .height(Size35dp) - .padding(start = 5.dp), - verticalAlignment = CenterVertically, - ) { - ShortCommunityActionOptions(baseNote, accountViewModel, nav) - } - } -} - -@Composable -private fun ShortCommunityActionOptions( - note: AddressableNote, - accountViewModel: AccountViewModel, - nav: (String) -> Unit, -) { - Spacer(modifier = StdHorzSpacer) - LikeReaction( - baseNote = note, - grayTint = MaterialTheme.colorScheme.onSurface, - accountViewModel = accountViewModel, - nav = nav, - ) - Spacer(modifier = StdHorzSpacer) - ZapReaction( - baseNote = note, - grayTint = MaterialTheme.colorScheme.onSurface, - accountViewModel = accountViewModel, - nav = nav, - ) - - WatchAddressableNoteFollows(note, accountViewModel) { isFollowing -> - if (!isFollowing) { - Spacer(modifier = StdHorzSpacer) - JoinCommunityButton(accountViewModel, note, nav) - } - } -} - -@Composable -fun WatchAddressableNoteFollows( - note: AddressableNote, - accountViewModel: AccountViewModel, - onFollowChanges: @Composable (Boolean) -> Unit, -) { - val showFollowingMark by - remember { - accountViewModel.userFollows - .map { it.user.latestContactList?.isTaggedAddressableNote(note.idHex) ?: false } - .distinctUntilChanged() - } - .observeAsState(false) - - onFollowChanges(showFollowingMark) -} - -@Composable -private fun LongCommunityActionOptions( - note: AddressableNote, - accountViewModel: AccountViewModel, - nav: (String) -> Unit, -) { - WatchAddressableNoteFollows(note, accountViewModel) { isFollowing -> - if (isFollowing) { - LeaveCommunityButton(accountViewModel, note, nav) - } - } -} - @Composable private fun CheckNewAndRenderNote( baseNote: Note, @@ -1109,75 +703,14 @@ fun InnerNoteWithReactions( } if (notBoostedNorQuote) { - Divider( + HorizontalDivider( thickness = DividerThickness, ) } } -@Stable -class EditState() { - private var modificationsList: List = persistentListOf() - private var modificationToShowIndex: Int = -1 - - val modificationToShow: MutableState = mutableStateOf(null) - val showingVersion: MutableState = mutableStateOf(0) - - fun hasModificationsToShow(): Boolean = modificationsList.isNotEmpty() - - fun isOriginal(): Boolean = modificationToShowIndex < 0 - - fun isLatest(): Boolean = modificationToShowIndex == modificationsList.lastIndex - - fun originalVersionId() = 0 - - fun lastVersionId() = modificationsList.size - - fun versionId() = modificationToShowIndex + 1 - - fun latest() = modificationsList.lastOrNull() - - fun latestBefore(createdAt: Long?): Note? { - if (createdAt == null) return latest() - return modificationsList.lastOrNull { (it.createdAt() ?: Long.MAX_VALUE) < createdAt } - } - - fun nextModification() { - if (modificationToShowIndex < 0) { - modificationToShowIndex = 0 - modificationToShow.value = modificationsList.getOrNull(0) - } else { - modificationToShowIndex++ - if (modificationToShowIndex >= modificationsList.size) { - modificationToShowIndex = -1 - modificationToShow.value = null - } else { - modificationToShow.value = modificationsList.getOrNull(modificationToShowIndex) - } - } - - showingVersion.value = versionId() - } - - fun updateModifications(newModifications: List) { - if (modificationsList != newModifications) { - modificationsList = newModifications - - if (newModifications.isEmpty()) { - modificationToShow.value = null - modificationToShowIndex = -1 - } else { - modificationToShowIndex = newModifications.lastIndex - modificationToShow.value = newModifications.last() - } - } - - showingVersion.value = versionId() - } -} - @Composable -private fun NoteBody( +fun NoteBody( baseNote: Note, showAuthorPicture: Boolean = false, unPackReply: Boolean = true, @@ -1404,841 +937,6 @@ private fun RenderNoteRow( } } -@Composable -fun LoadDecryptedContent( - note: Note, - accountViewModel: AccountViewModel, - inner: @Composable (String) -> Unit, -) { - var decryptedContent by - remember(note.event) { - mutableStateOf( - accountViewModel.cachedDecrypt(note), - ) - } - - decryptedContent?.let { inner(it) } - ?: run { - LaunchedEffect(key1 = decryptedContent) { - accountViewModel.decrypt(note) { decryptedContent = it } - } - } -} - -@Composable -fun LoadDecryptedContentOrNull( - note: Note, - accountViewModel: AccountViewModel, - inner: @Composable (String?) -> Unit, -) { - var decryptedContent by - remember(note.event) { - mutableStateOf( - accountViewModel.cachedDecrypt(note), - ) - } - - if (decryptedContent == null) { - LaunchedEffect(key1 = decryptedContent) { - accountViewModel.decrypt(note) { decryptedContent = it } - } - } - - inner(decryptedContent) -} - -@Composable -fun RenderTextEvent( - note: Note, - makeItShort: Boolean, - canPreview: Boolean, - backgroundColor: MutableState, - editState: State>, - accountViewModel: AccountViewModel, - nav: (String) -> Unit, -) { - LoadDecryptedContent( - note, - accountViewModel, - ) { body -> - val eventContent by - remember(note.event) { - derivedStateOf { - val subject = (note.event as? TextNoteEvent)?.subject()?.ifEmpty { null } - val newBody = - if (editState.value is GenericLoadable.Loaded) { - val state = (editState.value as? GenericLoadable.Loaded)?.loaded?.modificationToShow - state?.value?.event?.content() ?: body - } else { - body - } - - if (!subject.isNullOrBlank() && !newBody.split("\n")[0].contains(subject)) { - "### $subject\n$newBody" - } else { - newBody - } - } - } - - val isAuthorTheLoggedUser = remember(note.event) { accountViewModel.isLoggedUser(note.author) } - - if (makeItShort && isAuthorTheLoggedUser) { - Text( - text = eventContent, - color = MaterialTheme.colorScheme.placeholderText, - maxLines = 2, - overflow = TextOverflow.Ellipsis, - ) - } else { - SensitivityWarning( - note = note, - accountViewModel = accountViewModel, - ) { - val modifier = remember(note) { Modifier.fillMaxWidth() } - val tags = remember(note) { note.event?.tags()?.toImmutableListOfLists() ?: EmptyTagList } - - TranslatableRichTextViewer( - content = eventContent, - canPreview = canPreview && !makeItShort, - modifier = modifier, - tags = tags, - backgroundColor = backgroundColor, - accountViewModel = accountViewModel, - nav = nav, - ) - } - - if (note.event?.hasHashtags() == true) { - val hashtags = - remember(note.event) { note.event?.hashtags()?.toImmutableList() ?: persistentListOf() } - DisplayUncitedHashtags(hashtags, eventContent, nav) - } - } - } -} - -@Composable -fun RenderTextModificationEvent( - note: Note, - makeItShort: Boolean, - canPreview: Boolean, - backgroundColor: MutableState, - accountViewModel: AccountViewModel, - nav: (String) -> Unit, -) { - val noteEvent = note.event as? TextNoteModificationEvent ?: return - val noteAuthor = note.author ?: return - - val wantsToEditPost = - remember { - mutableStateOf(false) - } - - val isAuthorTheLoggedUser = - remember { - val authorOfTheOriginalNote = noteEvent.editedNote()?.let { accountViewModel.getNoteIfExists(it)?.author } - - mutableStateOf(accountViewModel.isLoggedUser(authorOfTheOriginalNote)) - } - - noteEvent.editedNote()?.let { - LoadNote(baseNoteHex = it, accountViewModel = accountViewModel) { baseOriginalNote -> - baseOriginalNote?.let { - } - } - } - - Card( - modifier = MaterialTheme.colorScheme.imageModifier, - ) { - Column(Modifier.fillMaxWidth().padding(Size10dp)) { - Text( - text = stringResource(id = R.string.proposal_to_edit), - style = - TextStyle( - fontSize = 18.sp, - fontWeight = FontWeight.Bold, - ), - ) - - Spacer(modifier = StdVertSpacer) - - noteEvent.summary()?.let { - TranslatableRichTextViewer( - content = it, - canPreview = canPreview && !makeItShort, - modifier = Modifier.fillMaxWidth(), - tags = EmptyTagList, - backgroundColor = backgroundColor, - accountViewModel = accountViewModel, - nav = nav, - ) - Spacer(modifier = StdVertSpacer) - } - - noteEvent.editedNote()?.let { - LoadNote(baseNoteHex = it, accountViewModel = accountViewModel) { baseNote -> - baseNote?.let { - val noteState by baseNote.live().metadata.observeAsState() - - val editStateOriginalNote = observeEdits(baseNote = baseNote, accountViewModel = accountViewModel) - - val editState = - remember(note) { - derivedStateOf { - val loadable = editStateOriginalNote.value as? GenericLoadable.Loaded - - val state = EditState() - - val latestChangeByAuthor = - if (loadable != null && loadable.loaded.hasModificationsToShow()) { - loadable.loaded.latestBefore(note.createdAt()) - } else { - null - } - - state.updateModifications(listOfNotNull(latestChangeByAuthor, note)) - - GenericLoadable.Loaded(state) - } - } - - LaunchedEffect(key1 = noteState) { - val newAuthor = accountViewModel.isLoggedUser(noteState?.note?.author) - - if (isAuthorTheLoggedUser.value != newAuthor) { - isAuthorTheLoggedUser.value = newAuthor - } - } - - Column( - modifier = - MaterialTheme.colorScheme.innerPostModifier.padding(Size10dp).clickable { - routeFor(baseNote, accountViewModel.userProfile())?.let { nav(it) } - }, - ) { - NoteBody( - baseNote = baseNote, - showAuthorPicture = true, - unPackReply = false, - makeItShort = false, - canPreview = true, - showSecondRow = false, - backgroundColor = backgroundColor, - editState = editState, - accountViewModel = accountViewModel, - nav = nav, - ) - - if (wantsToEditPost.value) { - EditPostView( - onClose = { - wantsToEditPost.value = false - }, - edit = baseNote, - versionLookingAt = note, - accountViewModel = accountViewModel, - nav = nav, - ) - } - } - } - } - } - - if (isAuthorTheLoggedUser.value) { - Spacer(modifier = StdVertSpacer) - - Button( - onClick = { wantsToEditPost.value = true }, - modifier = Modifier.fillMaxWidth(), - ) { - Text(text = stringResource(id = R.string.accept_the_suggestion)) - } - } - } - } -} - -@Composable -fun RenderPoll( - note: Note, - makeItShort: Boolean, - canPreview: Boolean, - backgroundColor: MutableState, - accountViewModel: AccountViewModel, - nav: (String) -> Unit, -) { - val noteEvent = note.event as? PollNoteEvent ?: return - val eventContent = noteEvent.content() - - if (makeItShort && accountViewModel.isLoggedUser(note.author)) { - Text( - text = eventContent, - color = MaterialTheme.colorScheme.placeholderText, - maxLines = 2, - overflow = TextOverflow.Ellipsis, - ) - } else { - val tags = remember(note) { note.event?.tags()?.toImmutableListOfLists() ?: EmptyTagList } - - SensitivityWarning( - note = note, - accountViewModel = accountViewModel, - ) { - TranslatableRichTextViewer( - content = eventContent, - canPreview = canPreview && !makeItShort, - modifier = remember { Modifier.fillMaxWidth() }, - tags = tags, - backgroundColor = backgroundColor, - accountViewModel = accountViewModel, - nav = nav, - ) - - PollNote( - note, - canPreview = canPreview && !makeItShort, - backgroundColor, - accountViewModel, - nav, - ) - } - - if (noteEvent.hasHashtags()) { - val hashtags = remember { noteEvent.hashtags().toImmutableList() } - DisplayUncitedHashtags(hashtags, eventContent, nav) - } - } -} - -@OptIn(ExperimentalFoundationApi::class) -@Composable -fun RenderAppDefinition( - note: Note, - accountViewModel: AccountViewModel, - nav: (String) -> Unit, -) { - val noteEvent = note.event as? AppDefinitionEvent ?: return - - var metadata by remember { mutableStateOf(null) } - - LaunchedEffect(key1 = noteEvent) { - launch(Dispatchers.Default) { metadata = noteEvent.appMetaData() } - } - - metadata?.let { - Box { - val clipboardManager = LocalClipboardManager.current - val uri = LocalUriHandler.current - - if (!it.banner.isNullOrBlank()) { - var zoomImageDialogOpen by remember { mutableStateOf(false) } - - AsyncImage( - model = it.banner, - contentDescription = stringResource(id = R.string.profile_image), - contentScale = ContentScale.FillWidth, - modifier = - Modifier - .fillMaxWidth() - .height(125.dp) - .combinedClickable( - onClick = {}, - onLongClick = { clipboardManager.setText(AnnotatedString(it.banner!!)) }, - ), - ) - - if (zoomImageDialogOpen) { - ZoomableImageDialog( - imageUrl = RichTextParser.parseImageOrVideo(it.banner!!), - onDismiss = { zoomImageDialogOpen = false }, - accountViewModel = accountViewModel, - ) - } - } else { - Image( - painter = painterResource(R.drawable.profile_banner), - contentDescription = stringResource(id = R.string.profile_banner), - contentScale = ContentScale.FillWidth, - modifier = - Modifier - .fillMaxWidth() - .height(125.dp), - ) - } - - Column( - modifier = - Modifier - .fillMaxWidth() - .padding(horizontal = 10.dp) - .padding(top = 75.dp), - ) { - Row( - horizontalArrangement = Arrangement.SpaceBetween, - verticalAlignment = Alignment.Bottom, - ) { - var zoomImageDialogOpen by remember { mutableStateOf(false) } - - Box(Modifier.size(100.dp)) { - it.picture?.let { - AsyncImage( - model = it, - contentDescription = null, - contentScale = ContentScale.FillWidth, - modifier = - Modifier - .border( - 3.dp, - MaterialTheme.colorScheme.background, - CircleShape, - ) - .clip(shape = CircleShape) - .fillMaxSize() - .background(MaterialTheme.colorScheme.background) - .combinedClickable( - onClick = { zoomImageDialogOpen = true }, - onLongClick = { clipboardManager.setText(AnnotatedString(it)) }, - ), - ) - } - } - - if (zoomImageDialogOpen) { - ZoomableImageDialog( - imageUrl = RichTextParser.parseImageOrVideo(it.banner!!), - onDismiss = { zoomImageDialogOpen = false }, - accountViewModel = accountViewModel, - ) - } - - Spacer(Modifier.weight(1f)) - - Row( - modifier = - Modifier - .height(Size35dp) - .padding(bottom = 3.dp), - ) {} - } - - val name = remember(it) { it.anyName() } - name?.let { - Row(verticalAlignment = Alignment.Bottom, modifier = Modifier.padding(top = 7.dp)) { - CreateTextWithEmoji( - text = it, - tags = remember { (note.event?.tags() ?: emptyArray()).toImmutableListOfLists() }, - fontWeight = FontWeight.Bold, - fontSize = 25.sp, - ) - } - } - - val website = remember(it) { it.website } - if (!website.isNullOrEmpty()) { - Row(verticalAlignment = CenterVertically) { - LinkIcon(Size16Modifier, MaterialTheme.colorScheme.placeholderText) - - ClickableText( - text = AnnotatedString(website.removePrefix("https://")), - onClick = { website.let { runCatching { uri.openUri(it) } } }, - style = LocalTextStyle.current.copy(color = MaterialTheme.colorScheme.primary), - modifier = Modifier.padding(top = 1.dp, bottom = 1.dp, start = 5.dp), - ) - } - } - - it.about?.let { - Row( - modifier = Modifier.padding(top = 5.dp, bottom = 5.dp), - ) { - val tags = - remember(note) { note.event?.tags()?.toImmutableListOfLists() ?: EmptyTagList } - val bgColor = MaterialTheme.colorScheme.background - val backgroundColor = remember { mutableStateOf(bgColor) } - TranslatableRichTextViewer( - content = it, - canPreview = false, - tags = tags, - backgroundColor = backgroundColor, - accountViewModel = accountViewModel, - nav = nav, - ) - } - } - } - } - } -} - -@Composable -private fun RenderHighlight( - note: Note, - makeItShort: Boolean, - canPreview: Boolean, - backgroundColor: MutableState, - accountViewModel: AccountViewModel, - nav: (String) -> Unit, -) { - val quote = remember { (note.event as? HighlightEvent)?.quote() ?: "" } - val author = remember { (note.event as? HighlightEvent)?.author() } - val url = remember { (note.event as? HighlightEvent)?.inUrl() } - val postHex = remember { (note.event as? HighlightEvent)?.taggedAddresses()?.firstOrNull() } - - DisplayHighlight( - highlight = quote, - authorHex = author, - url = url, - postAddress = postHex, - makeItShort = makeItShort, - canPreview = canPreview, - backgroundColor = backgroundColor, - accountViewModel = accountViewModel, - nav = nav, - ) -} - -@Composable -private fun RenderPrivateMessage( - note: Note, - makeItShort: Boolean, - canPreview: Boolean, - backgroundColor: MutableState, - accountViewModel: AccountViewModel, - nav: (String) -> Unit, -) { - val noteEvent = note.event as? PrivateDmEvent ?: return - - val withMe = remember { noteEvent.with(accountViewModel.userProfile().pubkeyHex) } - if (withMe) { - LoadDecryptedContent(note, accountViewModel) { eventContent -> - val modifier = remember(note.event?.id()) { Modifier.fillMaxWidth() } - val isAuthorTheLoggedUser = - remember(note.event?.id()) { accountViewModel.isLoggedUser(note.author) } - - val tags = remember(note) { note.event?.tags()?.toImmutableListOfLists() ?: EmptyTagList } - - if (makeItShort && isAuthorTheLoggedUser) { - Text( - text = eventContent, - color = MaterialTheme.colorScheme.placeholderText, - maxLines = 2, - overflow = TextOverflow.Ellipsis, - ) - } else { - SensitivityWarning( - note = note, - accountViewModel = accountViewModel, - ) { - TranslatableRichTextViewer( - content = eventContent, - canPreview = canPreview && !makeItShort, - modifier = modifier, - tags = tags, - backgroundColor = backgroundColor, - accountViewModel = accountViewModel, - nav = nav, - ) - } - - if (noteEvent.hasHashtags()) { - val hashtags = - remember(note.event?.id()) { - note.event?.hashtags()?.toImmutableList() ?: persistentListOf() - } - DisplayUncitedHashtags(hashtags, eventContent, nav) - } - } - } - } else { - val recipient = noteEvent.recipientPubKeyBytes()?.toNpub() ?: "Someone" - - TranslatableRichTextViewer( - stringResource( - id = R.string.private_conversation_notification, - "@${note.author?.pubkeyNpub()}", - "@$recipient", - ), - canPreview = !makeItShort, - Modifier.fillMaxWidth(), - EmptyTagList, - backgroundColor, - accountViewModel, - nav, - ) - } -} - -@Composable -fun DisplayRelaySet( - baseNote: Note, - backgroundColor: MutableState, - accountViewModel: AccountViewModel, - nav: (String) -> Unit, -) { - val noteEvent = baseNote.event as? RelaySetEvent ?: return - - val relays by - remember(baseNote) { - mutableStateOf( - noteEvent.relays().map { RelayBriefInfoCache.RelayBriefInfo(it) }.toImmutableList(), - ) - } - - var expanded by remember { mutableStateOf(false) } - - val toMembersShow = - if (expanded) { - relays - } else { - relays.take(3) - } - - val relayListName by remember { derivedStateOf { "#${noteEvent.dTag()}" } } - - val relayDescription by remember { derivedStateOf { noteEvent.description() } } - - Text( - text = relayListName, - fontWeight = FontWeight.Bold, - maxLines = 1, - overflow = TextOverflow.Ellipsis, - modifier = - Modifier - .fillMaxWidth() - .padding(5.dp), - textAlign = TextAlign.Center, - ) - - relayDescription?.let { - Text( - text = it, - maxLines = 3, - overflow = TextOverflow.Ellipsis, - modifier = - Modifier - .fillMaxWidth() - .padding(5.dp), - textAlign = TextAlign.Center, - color = Color.Gray, - ) - } - - Box { - Column(modifier = Modifier.padding(top = 5.dp)) { - toMembersShow.forEach { relay -> - Row(modifier = Modifier.fillMaxWidth(), verticalAlignment = CenterVertically) { - Text( - text = relay.displayUrl, - fontWeight = FontWeight.Bold, - maxLines = 1, - overflow = TextOverflow.Ellipsis, - modifier = - Modifier - .padding(start = 10.dp, bottom = 5.dp) - .weight(1f), - ) - - Column(modifier = Modifier.padding(start = 10.dp)) { - RelayOptionsAction(relay.url, accountViewModel, nav) - } - } - } - } - - if (relays.size > 3 && !expanded) { - Row( - verticalAlignment = CenterVertically, - horizontalArrangement = Arrangement.Center, - modifier = - Modifier - .align(Alignment.BottomCenter) - .fillMaxWidth() - .background(getGradient(backgroundColor)), - ) { - ShowMoreButton { expanded = !expanded } - } - } - } -} - -@Composable -private fun RelayOptionsAction( - relay: String, - accountViewModel: AccountViewModel, - nav: (String) -> Unit, -) { - val userStateRelayInfo by accountViewModel.account.userProfile().live().relayInfo.observeAsState() - val isCurrentlyOnTheUsersList by - remember(userStateRelayInfo) { - derivedStateOf { - userStateRelayInfo?.user?.latestContactList?.relays()?.none { it.key == relay } == true - } - } - - var wantsToAddRelay by remember { mutableStateOf("") } - - if (wantsToAddRelay.isNotEmpty()) { - NewRelayListView({ wantsToAddRelay = "" }, accountViewModel, wantsToAddRelay, nav = nav) - } - - if (isCurrentlyOnTheUsersList) { - AddRelayButton { wantsToAddRelay = relay } - } else { - RemoveRelayButton { wantsToAddRelay = relay } - } -} - -@OptIn(ExperimentalLayoutApi::class) -@Composable -fun DisplayPeopleList( - baseNote: Note, - backgroundColor: MutableState, - accountViewModel: AccountViewModel, - nav: (String) -> Unit, -) { - val noteEvent = baseNote.event as? PeopleListEvent ?: return - - var members by remember { mutableStateOf>(persistentListOf()) } - - var expanded by remember { mutableStateOf(false) } - - val toMembersShow = - if (expanded) { - members - } else { - members.take(3) - } - - val name by remember { derivedStateOf { "#${noteEvent.dTag()}" } } - - Text( - text = name, - fontWeight = FontWeight.Bold, - maxLines = 1, - overflow = TextOverflow.Ellipsis, - modifier = - Modifier - .fillMaxWidth() - .padding(5.dp), - textAlign = TextAlign.Center, - ) - - LaunchedEffect(Unit) { accountViewModel.loadUsers(noteEvent.bookmarkedPeople()) { members = it } } - - Box { - FlowRow(modifier = Modifier.padding(top = 5.dp)) { - toMembersShow.forEach { user -> - Row(modifier = Modifier.fillMaxWidth()) { - UserCompose( - user, - overallModifier = Modifier, - accountViewModel = accountViewModel, - nav = nav, - ) - } - } - } - - if (members.size > 3 && !expanded) { - Row( - verticalAlignment = CenterVertically, - horizontalArrangement = Arrangement.Center, - modifier = - Modifier - .align(Alignment.BottomCenter) - .fillMaxWidth() - .background(getGradient(backgroundColor)), - ) { - ShowMoreButton { expanded = !expanded } - } - } - } -} - -@OptIn(ExperimentalLayoutApi::class) -@Composable -private fun RenderBadgeAward( - note: Note, - backgroundColor: MutableState, - accountViewModel: AccountViewModel, - nav: (String) -> Unit, -) { - if (note.replyTo.isNullOrEmpty()) return - - val noteEvent = note.event as? BadgeAwardEvent ?: return - var awardees by remember { mutableStateOf>(listOf()) } - - Text(text = stringResource(R.string.award_granted_to)) - - LaunchedEffect(key1 = note) { accountViewModel.loadUsers(noteEvent.awardees()) { awardees = it } } - - FlowRow(modifier = Modifier.padding(top = 5.dp)) { - awardees.take(100).forEach { user -> - Row( - modifier = - Modifier - .size(size = Size35dp) - .clickable { nav("User/${user.pubkeyHex}") }, - verticalAlignment = CenterVertically, - ) { - ClickableUserPicture( - baseUser = user, - accountViewModel = accountViewModel, - size = Size35dp, - ) - } - } - - if (awardees.size > 100) { - Text(" and ${awardees.size - 100} others", maxLines = 1) - } - } - - note.replyTo?.firstOrNull()?.let { - NoteCompose( - it, - modifier = Modifier, - isBoostedNote = false, - isQuotedNote = true, - unPackReply = false, - parentBackgroundColor = backgroundColor, - accountViewModel = accountViewModel, - nav = nav, - ) - } -} - -@Composable -private fun RenderReaction( - note: Note, - backgroundColor: MutableState, - accountViewModel: AccountViewModel, - nav: (String) -> Unit, -) { - note.replyTo?.lastOrNull()?.let { - NoteCompose( - it, - modifier = Modifier, - isBoostedNote = true, - unPackReply = false, - parentBackgroundColor = backgroundColor, - accountViewModel = accountViewModel, - nav = nav, - ) - } - - // Reposts have trash in their contents. - val refactorReactionText = if (note.event?.content() == "+") "❤" else note.event?.content() ?: "" - - Text( - text = refactorReactionText, - maxLines = 1, - ) -} - @Composable fun RenderRepost( note: Note, @@ -2261,328 +959,6 @@ fun RenderRepost( } } -@Composable -fun RenderPostApproval( - note: Note, - makeItShort: Boolean, - canPreview: Boolean, - backgroundColor: MutableState, - accountViewModel: AccountViewModel, - nav: (String) -> Unit, -) { - if (note.replyTo.isNullOrEmpty()) return - - val noteEvent = note.event as? CommunityPostApprovalEvent ?: return - - Column(Modifier.fillMaxWidth()) { - noteEvent.communities().forEach { - LoadAddressableNote(it, accountViewModel) { - it?.let { - NoteCompose( - it, - parentBackgroundColor = backgroundColor, - accountViewModel = accountViewModel, - nav = nav, - ) - } - } - } - - Text( - text = stringResource(id = R.string.community_approved_posts), - fontWeight = FontWeight.Bold, - maxLines = 1, - overflow = TextOverflow.Ellipsis, - modifier = - Modifier - .fillMaxWidth() - .padding(5.dp), - textAlign = TextAlign.Center, - ) - - note.replyTo?.forEach { - NoteCompose( - it, - modifier = MaterialTheme.colorScheme.replyModifier, - unPackReply = false, - makeItShort = true, - isQuotedNote = true, - parentBackgroundColor = backgroundColor, - accountViewModel = accountViewModel, - nav = nav, - ) - } - } -} - -@Composable -fun LoadAddressableNote( - aTagHex: String, - accountViewModel: AccountViewModel, - content: @Composable (AddressableNote?) -> Unit, -) { - var note by - remember(aTagHex) { - mutableStateOf(accountViewModel.getAddressableNoteIfExists(aTagHex)) - } - - if (note == null) { - LaunchedEffect(key1 = aTagHex) { - accountViewModel.checkGetOrCreateAddressableNote(aTagHex) { newNote -> - if (newNote != note) { - note = newNote - } - } - } - } - - content(note) -} - -@Composable -fun LoadAddressableNote( - aTag: ATag, - accountViewModel: AccountViewModel, - content: @Composable (AddressableNote?) -> Unit, -) { - var note by - remember(aTag) { - mutableStateOf(accountViewModel.getAddressableNoteIfExists(aTag.toTag())) - } - - if (note == null) { - LaunchedEffect(key1 = aTag) { - accountViewModel.getOrCreateAddressableNote(aTag) { newNote -> - if (newNote != note) { - note = newNote - } - } - } - } - - content(note) -} - -@Composable -public fun RenderEmojiPack( - baseNote: Note, - actionable: Boolean, - backgroundColor: MutableState, - accountViewModel: AccountViewModel, - onClick: ((EmojiUrl) -> Unit)? = null, -) { - val noteEvent by - baseNote - .live() - .metadata - .map { it.note.event } - .distinctUntilChanged() - .observeAsState(baseNote.event) - - if (noteEvent == null || noteEvent !is EmojiPackEvent) return - - (noteEvent as? EmojiPackEvent)?.let { - RenderEmojiPack( - noteEvent = it, - baseNote = baseNote, - actionable = actionable, - backgroundColor = backgroundColor, - accountViewModel = accountViewModel, - onClick = onClick, - ) - } -} - -@OptIn(ExperimentalLayoutApi::class) -@Composable -public fun RenderEmojiPack( - noteEvent: EmojiPackEvent, - baseNote: Note, - actionable: Boolean, - backgroundColor: MutableState, - accountViewModel: AccountViewModel, - onClick: ((EmojiUrl) -> Unit)? = null, -) { - var expanded by remember { mutableStateOf(false) } - - val allEmojis = remember(noteEvent) { noteEvent.taggedEmojis() } - - val emojisToShow = - if (expanded) { - allEmojis - } else { - allEmojis.take(60) - } - - Row(verticalAlignment = CenterVertically) { - Text( - text = remember(noteEvent) { "#${noteEvent.dTag()}" }, - fontWeight = FontWeight.Bold, - maxLines = 1, - overflow = TextOverflow.Ellipsis, - modifier = - Modifier - .weight(1F) - .padding(5.dp), - textAlign = TextAlign.Center, - ) - - if (actionable) { - EmojiListOptions(accountViewModel, baseNote) - } - } - - Box(Modifier.fillMaxWidth(), contentAlignment = Alignment.TopCenter) { - FlowRow(modifier = Modifier.padding(top = 5.dp)) { - emojisToShow.forEach { emoji -> - if (onClick != null) { - IconButton(onClick = { onClick(emoji) }, modifier = Size35Modifier) { - AsyncImage( - model = emoji.url, - contentDescription = null, - modifier = Size35Modifier, - ) - } - } else { - Box( - modifier = Size35Modifier, - contentAlignment = Alignment.Center, - ) { - AsyncImage( - model = emoji.url, - contentDescription = null, - modifier = Size35Modifier, - ) - } - } - } - } - - if (allEmojis.size > 60 && !expanded) { - Row( - verticalAlignment = CenterVertically, - horizontalArrangement = Arrangement.Center, - modifier = - Modifier - .align(Alignment.BottomCenter) - .fillMaxWidth() - .background(getGradient(backgroundColor)), - ) { - ShowMoreButton { expanded = !expanded } - } - } - } -} - -@Composable -private fun EmojiListOptions( - accountViewModel: AccountViewModel, - emojiPackNote: Note, -) { - LoadAddressableNote( - aTag = - ATag( - EmojiPackSelectionEvent.KIND, - accountViewModel.userProfile().pubkeyHex, - "", - null, - ), - accountViewModel, - ) { - it?.let { usersEmojiList -> - val hasAddedThis by - remember { - usersEmojiList - .live() - .metadata - .map { usersEmojiList.event?.isTaggedAddressableNote(emojiPackNote.idHex) } - .distinctUntilChanged() - } - .observeAsState() - - Crossfade(targetState = hasAddedThis, label = "EmojiListOptions") { - if (it != true) { - AddButton { accountViewModel.addEmojiPack(usersEmojiList, emojiPackNote) } - } else { - RemoveButton { accountViewModel.removeEmojiPack(usersEmojiList, emojiPackNote) } - } - } - } - } -} - -@OptIn(ExperimentalLayoutApi::class) -@Composable -fun RenderPinListEvent( - baseNote: Note, - backgroundColor: MutableState, - accountViewModel: AccountViewModel, - nav: (String) -> Unit, -) { - val noteEvent = baseNote.event as? PinListEvent ?: return - - val pins by remember { mutableStateOf(noteEvent.pins()) } - - var expanded by remember { mutableStateOf(false) } - - val pinsToShow = - if (expanded) { - pins - } else { - pins.take(3) - } - - Text( - text = "#${noteEvent.dTag()}", - fontWeight = FontWeight.Bold, - maxLines = 1, - overflow = TextOverflow.Ellipsis, - modifier = - Modifier - .fillMaxWidth() - .padding(5.dp), - textAlign = TextAlign.Center, - ) - - Box { - FlowRow(modifier = Modifier.padding(top = 5.dp)) { - pinsToShow.forEach { pin -> - Row(modifier = Modifier.fillMaxWidth(), verticalAlignment = CenterVertically) { - PinIcon( - modifier = Size15Modifier, - tint = MaterialTheme.colorScheme.onBackground.copy(0.32f), - ) - - Spacer(modifier = Modifier.width(5.dp)) - - TranslatableRichTextViewer( - content = pin, - canPreview = true, - tags = EmptyTagList, - backgroundColor = backgroundColor, - accountViewModel = accountViewModel, - nav = nav, - ) - } - } - } - - if (pins.size > 3 && !expanded) { - Row( - verticalAlignment = CenterVertically, - horizontalArrangement = Arrangement.Center, - modifier = - Modifier - .align(Alignment.BottomCenter) - .fillMaxWidth() - .background(getGradient(backgroundColor)), - ) { - ShowMoreButton { expanded = !expanded } - } - } - } -} - fun getGradient(backgroundColor: MutableState): Brush { return Brush.verticalGradient( colors = @@ -2593,104 +969,6 @@ fun getGradient(backgroundColor: MutableState): Brush { ) } -@Composable -private fun RenderAudioTrack( - note: Note, - accountViewModel: AccountViewModel, - nav: (String) -> Unit, -) { - val noteEvent = note.event as? AudioTrackEvent ?: return - - AudioTrackHeader(noteEvent, note, accountViewModel, nav) -} - -@Composable -private fun RenderAudioHeader( - note: Note, - accountViewModel: AccountViewModel, - nav: (String) -> Unit, -) { - val noteEvent = note.event as? AudioHeaderEvent ?: return - - AudioHeader(noteEvent, note, accountViewModel, nav) -} - -@Composable -private fun RenderLongFormContent( - note: Note, - accountViewModel: AccountViewModel, - nav: (String) -> Unit, -) { - val noteEvent = note.event as? LongTextNoteEvent ?: return - - LongFormHeader(noteEvent, note, accountViewModel) -} - -@Composable -private fun RenderReport( - note: Note, - backgroundColor: MutableState, - accountViewModel: AccountViewModel, - nav: (String) -> Unit, -) { - val noteEvent = note.event as? ReportEvent ?: return - - val base = remember { (noteEvent.reportedPost() + noteEvent.reportedAuthor()) } - - val reportType = - base - .map { - when (it.reportType) { - ReportEvent.ReportType.EXPLICIT -> stringResource(R.string.explicit_content) - ReportEvent.ReportType.NUDITY -> stringResource(R.string.nudity) - ReportEvent.ReportType.PROFANITY -> stringResource(R.string.profanity_hateful_speech) - ReportEvent.ReportType.SPAM -> stringResource(R.string.spam) - ReportEvent.ReportType.IMPERSONATION -> stringResource(R.string.impersonation) - ReportEvent.ReportType.ILLEGAL -> stringResource(R.string.illegal_behavior) - ReportEvent.ReportType.OTHER -> stringResource(R.string.other) - } - } - .toSet() - .joinToString(", ") - - val content = - remember { - reportType + (note.event?.content()?.ifBlank { null }?.let { ": $it" } ?: "") - } - - TranslatableRichTextViewer( - content = content, - canPreview = true, - modifier = Modifier, - tags = EmptyTagList, - backgroundColor = backgroundColor, - accountViewModel = accountViewModel, - nav = nav, - ) - - note.replyTo?.lastOrNull()?.let { - NoteCompose( - baseNote = it, - isQuotedNote = true, - modifier = - Modifier - .padding(top = 5.dp) - .fillMaxWidth() - .clip(shape = QuoteBorder) - .border( - 1.dp, - MaterialTheme.colorScheme.subtleBorder, - QuoteBorder, - ), - unPackReply = false, - makeItShort = true, - parentBackgroundColor = backgroundColor, - accountViewModel = accountViewModel, - nav = nav, - ) - } -} - @Composable private fun ReplyRow( note: Note, @@ -2835,228 +1113,6 @@ fun SecondUserInfoRow( } } -@Composable -fun DisplayOts( - note: Note, - accountViewModel: AccountViewModel, -) { - LoadOts( - note, - accountViewModel, - whenConfirmed = { unixtimestamp -> - val context = LocalContext.current - val timeStr by remember(unixtimestamp) { mutableStateOf(timeAgoNoDot(unixtimestamp, context = context)) } - - ClickableText( - text = buildAnnotatedString { append(stringResource(id = R.string.existed_since, timeStr)) }, - onClick = { - val fullDateTime = - SimpleDateFormat.getDateTimeInstance().format(Date(unixtimestamp * 1000)) - - accountViewModel.toast( - context.getString(R.string.ots_info_title), - context.getString(R.string.ots_info_description, fullDateTime), - ) - }, - style = - LocalTextStyle.current.copy( - color = MaterialTheme.colorScheme.lessImportantLink, - fontSize = Font14SP, - fontWeight = FontWeight.Bold, - ), - maxLines = 1, - ) - }, - whenPending = { - Text( - stringResource(id = R.string.timestamp_pending_short), - color = MaterialTheme.colorScheme.lessImportantLink, - fontSize = Font14SP, - fontWeight = FontWeight.Bold, - maxLines = 1, - ) - }, - ) -} - -@Composable -private fun ShowForkInformation( - noteEvent: BaseTextNoteEvent, - modifier: Modifier, - accountViewModel: AccountViewModel, - nav: (String) -> Unit, -) { - val forkedAddress = remember(noteEvent) { noteEvent.forkFromAddress() } - val forkedEvent = remember(noteEvent) { noteEvent.forkFromVersion() } - if (forkedAddress != null) { - LoadAddressableNote(aTag = forkedAddress, accountViewModel = accountViewModel) { addressableNote -> - if (addressableNote != null) { - ForkInformationRowLightColor(addressableNote, modifier, accountViewModel, nav) - } - } - } else if (forkedEvent != null) { - LoadNote(forkedEvent, accountViewModel = accountViewModel) { event -> - if (event != null) { - ForkInformationRowLightColor(event, modifier, accountViewModel, nav) - } - } - } -} - -@Composable -fun ForkInformationRowLightColor( - originalVersion: Note, - modifier: Modifier = Modifier, - accountViewModel: AccountViewModel, - nav: (String) -> Unit, -) { - val noteState by originalVersion.live().metadata.observeAsState() - val note = noteState?.note ?: return - val author = note.author ?: return - val route = remember(note) { routeFor(note, accountViewModel.userProfile()) } - - if (route != null) { - Row(modifier) { - ClickableText( - text = - buildAnnotatedString { - append(stringResource(id = R.string.forked_from)) - append(" ") - }, - onClick = { nav(route) }, - style = LocalTextStyle.current.copy(color = MaterialTheme.colorScheme.nip05, fontSize = Font14SP), - maxLines = 1, - overflow = TextOverflow.Visible, - ) - - val userState by author.live().metadata.observeAsState() - val userDisplayName = remember(userState) { userState?.user?.toBestDisplayName() } - val userTags = - remember(userState) { userState?.user?.info?.latestMetadata?.tags?.toImmutableListOfLists() } - - if (userDisplayName != null) { - CreateClickableTextWithEmoji( - clickablePart = userDisplayName, - maxLines = 1, - route = route, - overrideColor = MaterialTheme.colorScheme.nip05, - fontSize = Font14SP, - nav = nav, - tags = userTags, - ) - } - } - } -} - -@Composable -fun LoadStatuses( - user: User, - accountViewModel: AccountViewModel, - content: @Composable (ImmutableList) -> Unit, -) { - var statuses: ImmutableList by remember { mutableStateOf(persistentListOf()) } - - val userStatus by user.live().statuses.observeAsState() - - LaunchedEffect(key1 = userStatus) { - accountViewModel.findStatusesForUser(userStatus?.user ?: user) { newStatuses -> - if (!equalImmutableLists(statuses, newStatuses)) { - statuses = newStatuses - } - } - } - - content(statuses) -} - -@Composable -fun LoadOts( - note: Note, - accountViewModel: AccountViewModel, - whenConfirmed: @Composable (Long) -> Unit, - whenPending: @Composable () -> Unit, -) { - var earliestDate: GenericLoadable by remember { mutableStateOf(GenericLoadable.Loading()) } - - val noteStatus by note.live().innerOts.observeAsState() - - LaunchedEffect(key1 = noteStatus) { - accountViewModel.findOtsEventsForNote(noteStatus?.note ?: note) { newOts -> - earliestDate = - if (newOts == null) { - GenericLoadable.Empty() - } else { - GenericLoadable.Loaded(newOts) - } - } - } - - (earliestDate as? GenericLoadable.Loaded)?.let { - whenConfirmed(it.loaded) - } ?: run { - val account = accountViewModel.account.saveable.observeAsState() - if (account.value?.account?.hasPendingAttestations(note) == true) { - whenPending() - } - } -} - -@Composable -fun LoadCityName( - geohashStr: String, - onLoading: (@Composable () -> Unit)? = null, - content: @Composable (String) -> Unit, -) { - var cityName by remember(geohashStr) { mutableStateOf(CachedGeoLocations.cached(geohashStr)) } - - if (cityName == null) { - if (onLoading != null) { - onLoading() - } - - val context = LocalContext.current - - LaunchedEffect(key1 = geohashStr, context) { - launch(Dispatchers.IO) { - val geoHash = runCatching { geohashStr.toGeoHash() }.getOrNull() - if (geoHash != null) { - val newCityName = - CachedGeoLocations.geoLocate(geohashStr, geoHash.toLocation(), context)?.ifBlank { null } - if (newCityName != null && newCityName != cityName) { - cityName = newCityName - } - } - } - } - } else { - cityName?.let { content(it) } - } -} - -@Composable -fun DisplayLocation( - geohashStr: String, - nav: (String) -> Unit, -) { - LoadCityName(geohashStr) { cityName -> - ClickableText( - text = AnnotatedString(cityName), - onClick = { nav("Geohash/$geohashStr") }, - style = - LocalTextStyle.current.copy( - color = - MaterialTheme.colorScheme.primary.copy( - alpha = 0.52f, - ), - fontSize = Font14SP, - fontWeight = FontWeight.Bold, - ), - maxLines = 1, - ) - } -} - @Composable fun FirstUserInfoRow( baseNote: Note, @@ -3102,7 +1158,7 @@ fun FirstUserInfoRow( } } - TimeAgo(baseNote) + com.vitorpamplona.amethyst.ui.note.elements.TimeAgo(baseNote) MoreOptionsButton(baseNote, editState, accountViewModel, nav) } @@ -3160,87 +1216,6 @@ fun observeEdits( return editState } -@Composable -fun DisplayEditStatus(editState: EditState) { - ClickableText( - text = - buildAnnotatedString { - if (editState.showingVersion.value == editState.originalVersionId()) { - append(stringResource(id = R.string.original)) - } else if (editState.showingVersion.value == editState.lastVersionId()) { - append(stringResource(id = R.string.edited)) - } else { - append(stringResource(id = R.string.edited_number, editState.versionId())) - } - }, - onClick = { - editState.nextModification() - }, - style = - LocalTextStyle.current.copy( - color = MaterialTheme.colorScheme.placeholderText, - fontWeight = FontWeight.Bold, - ), - maxLines = 1, - modifier = HalfStartPadding, - ) -} - -@Composable -private fun BoostedMark() { - Text( - stringResource(id = R.string.boosted), - fontWeight = FontWeight.Bold, - color = MaterialTheme.colorScheme.placeholderText, - maxLines = 1, - modifier = HalfStartPadding, - ) -} - -@Composable -fun MoreOptionsButton( - baseNote: Note, - editState: State>? = null, - accountViewModel: AccountViewModel, - nav: (String) -> Unit, -) { - val popupExpanded = remember { mutableStateOf(false) } - val enablePopup = remember { { popupExpanded.value = true } } - - IconButton( - modifier = Size24Modifier, - onClick = enablePopup, - ) { - VerticalDotsIcon(R.string.note_options) - - NoteDropDownMenu( - baseNote, - popupExpanded, - editState, - accountViewModel, - nav, - ) - } -} - -@Composable -fun TimeAgo(note: Note) { - val time = remember(note) { note.createdAt() } ?: return - TimeAgo(time) -} - -@Composable -fun TimeAgo(time: Long) { - val context = LocalContext.current - val timeStr by remember(time) { mutableStateOf(timeAgo(time, context = context)) } - - Text( - text = timeStr, - color = MaterialTheme.colorScheme.placeholderText, - maxLines = 1, - ) -} - @Composable private fun AuthorAndRelayInformation( baseNote: Note, @@ -3300,28 +1275,6 @@ private fun RenderAuthorImages( } } -@Composable -fun LoadChannel( - baseChannelHex: String, - accountViewModel: AccountViewModel, - content: @Composable (Channel) -> Unit, -) { - var channel by - remember(baseChannelHex) { - mutableStateOf(accountViewModel.getChannelIfExists(baseChannelHex)) - } - - if (channel == null) { - LaunchedEffect(key1 = baseChannelHex) { - accountViewModel.checkGetOrCreateChannel(baseChannelHex) { newChannel -> - launch(Dispatchers.Main) { channel = newChannel } - } - } - } - - channel?.let { content(it) } -} - @Composable private fun ChannelNotePicture( baseChannel: Channel, @@ -3367,1644 +1320,3 @@ private fun RepostNoteAuthorPicture( }, ) } - -@Composable -fun DisplayHighlight( - highlight: String, - authorHex: String?, - url: String?, - postAddress: ATag?, - makeItShort: Boolean, - canPreview: Boolean, - backgroundColor: MutableState, - accountViewModel: AccountViewModel, - nav: (String) -> Unit, -) { - val quote = - remember { - highlight.split("\n").joinToString("\n") { "> *${it.removeSuffix(" ")}*" } - } - - TranslatableRichTextViewer( - quote, - canPreview = canPreview && !makeItShort, - remember { Modifier.fillMaxWidth() }, - EmptyTagList, - backgroundColor, - accountViewModel, - nav, - ) - - DisplayQuoteAuthor(authorHex ?: "", url, postAddress, accountViewModel, nav) -} - -@OptIn(ExperimentalLayoutApi::class) -@Composable -private fun DisplayQuoteAuthor( - authorHex: String, - url: String?, - postAddress: ATag?, - accountViewModel: AccountViewModel, - nav: (String) -> Unit, -) { - var userBase by remember { mutableStateOf(accountViewModel.getUserIfExists(authorHex)) } - - if (userBase == null) { - LaunchedEffect(Unit) { - accountViewModel.checkGetOrCreateUser(authorHex) { newUserBase -> userBase = newUserBase } - } - } - - val spaceWidth = measureSpaceWidth(textStyle = LocalTextStyle.current) - - FlowRow( - horizontalArrangement = Arrangement.spacedBy(spaceWidth), - verticalArrangement = Arrangement.Center, - ) { - userBase?.let { userBase -> LoadAndDisplayUser(userBase, nav) } - - url?.let { url -> LoadAndDisplayUrl(url) } - - postAddress?.let { address -> LoadAndDisplayPost(address, accountViewModel, nav) } - } -} - -@Composable -private fun LoadAndDisplayPost( - postAddress: ATag, - accountViewModel: AccountViewModel, - nav: (String) -> Unit, -) { - LoadAddressableNote(aTag = postAddress, accountViewModel) { - it?.let { note -> - val noteEvent by - note.live().metadata.map { it.note.event }.distinctUntilChanged().observeAsState(note.event) - - val title = remember(noteEvent) { (noteEvent as? LongTextNoteEvent)?.title() } - - title?.let { - Text(remember { "-" }, maxLines = 1) - ClickableText( - text = AnnotatedString(title), - onClick = { routeFor(note, accountViewModel.userProfile())?.let { nav(it) } }, - style = LocalTextStyle.current.copy(color = MaterialTheme.colorScheme.primary), - ) - } - } - } -} - -@Composable -private fun LoadAndDisplayUrl(url: String) { - val validatedUrl = - remember { - try { - URL(url) - } catch (e: Exception) { - Log.w("Note Compose", "Invalid URI: $url") - null - } - } - - validatedUrl?.host?.let { host -> - Text(remember { "-" }, maxLines = 1) - ClickableUrl(urlText = host, url = url) - } -} - -@Composable -fun LoadAndDisplayUser( - userBase: User, - nav: (String) -> Unit, -) { - LoadAndDisplayUser(userBase, "User/${userBase.pubkeyHex}", nav) -} - -@Composable -fun LoadAndDisplayUser( - userBase: User, - route: String, - nav: (String) -> Unit, -) { - val userState by userBase.live().metadata.observeAsState() - val userDisplayName = remember(userState) { userState?.user?.toBestDisplayName() } - val userTags = - remember(userState) { userState?.user?.info?.latestMetadata?.tags?.toImmutableListOfLists() } - - if (userDisplayName != null) { - CreateClickableTextWithEmoji( - clickablePart = userDisplayName, - maxLines = 1, - route = route, - nav = nav, - tags = userTags, - ) - } -} - -@Composable -fun BadgeDisplay(baseNote: Note) { - val background = MaterialTheme.colorScheme.background - val badgeData = baseNote.event as? BadgeDefinitionEvent ?: return - - val image = remember { badgeData.thumb()?.ifBlank { null } ?: badgeData.image() } - val name = remember { badgeData.name() } - val description = remember { badgeData.description() } - - var backgroundFromImage by remember { mutableStateOf(Pair(background, background)) } - var imageResult by remember { mutableStateOf(null) } - - LaunchedEffect(key1 = imageResult) { - launch(Dispatchers.IO) { - imageResult?.let { - val backgroundColor = - it.drawable.toBitmap(200, 200).copy(Bitmap.Config.ARGB_8888, false).get(0, 199) - val colorFromImage = Color(backgroundColor) - val textBackground = - if (colorFromImage.luminance() > 0.5) { - lightColorScheme().onBackground - } else { - darkColorScheme().onBackground - } - - launch(Dispatchers.Main) { backgroundFromImage = Pair(colorFromImage, textBackground) } - } - } - } - - Row( - modifier = - Modifier - .padding(10.dp) - .clip(shape = CutCornerShape(20, 20, 20, 20)) - .border( - 5.dp, - MaterialTheme.colorScheme.mediumImportanceLink, - CutCornerShape(20), - ) - .background(backgroundFromImage.first), - ) { - RenderBadge( - image, - name, - backgroundFromImage.second, - description, - ) { - if (imageResult == null) { - imageResult = it.result - } - } - } -} - -@Composable -private fun RenderBadge( - image: String?, - name: String?, - backgroundFromImage: Color, - description: String?, - onSuccess: (AsyncImagePainter.State.Success) -> Unit, -) { - Column { - image.let { - AsyncImage( - model = it, - contentDescription = - stringResource( - R.string.badge_award_image_for, - name ?: "", - ), - contentScale = ContentScale.FillWidth, - modifier = Modifier.fillMaxWidth(), - onSuccess = onSuccess, - ) - } - - name?.let { - Text( - text = it, - style = MaterialTheme.typography.bodyLarge, - textAlign = TextAlign.Center, - modifier = - Modifier - .fillMaxWidth() - .padding(start = 10.dp, end = 10.dp), - color = backgroundFromImage, - ) - } - - description?.let { - Text( - text = it, - style = MaterialTheme.typography.bodySmall, - textAlign = TextAlign.Center, - modifier = - Modifier - .fillMaxWidth() - .padding(start = 10.dp, end = 10.dp, bottom = 10.dp), - color = Color.Gray, - maxLines = 3, - overflow = TextOverflow.Ellipsis, - ) - } - } -} - -@Composable -fun FileHeaderDisplay( - note: Note, - roundedCorner: Boolean, - accountViewModel: AccountViewModel, -) { - val event = (note.event as? FileHeaderEvent) ?: return - val fullUrl = event.url() ?: return - - val content by - remember(note) { - val blurHash = event.blurhash() - val hash = event.hash() - val dimensions = event.dimensions() - val description = event.content.ifEmpty { null } ?: event.alt() - val isImage = RichTextParser.isImageUrl(fullUrl) - val uri = note.toNostrUri() - - mutableStateOf( - if (isImage) { - MediaUrlImage( - url = fullUrl, - description = description, - hash = hash, - blurhash = blurHash, - dim = dimensions, - uri = uri, - ) - } else { - MediaUrlVideo( - url = fullUrl, - description = description, - hash = hash, - dim = dimensions, - uri = uri, - authorName = note.author?.toBestDisplayName(), - ) - }, - ) - } - - SensitivityWarning(note = note, accountViewModel = accountViewModel) { - ZoomableContentView( - content = content, - roundedCorner = roundedCorner, - accountViewModel = accountViewModel, - ) - } -} - -@Composable -fun VideoDisplay( - note: Note, - makeItShort: Boolean, - canPreview: Boolean, - backgroundColor: MutableState, - accountViewModel: AccountViewModel, - nav: (String) -> Unit, -) { - val event = (note.event as? VideoEvent) ?: return - val fullUrl = event.url() ?: return - - val title = event.title() - val summary = event.content.ifBlank { null }?.takeIf { title != it } - val image = event.thumb() ?: event.image() - val isYouTube = fullUrl.contains("youtube.com") || fullUrl.contains("youtu.be") - val tags = remember(note) { note.event?.tags()?.toImmutableListOfLists() ?: EmptyTagList } - - val content by - remember(note) { - val blurHash = event.blurhash() - val hash = event.hash() - val dimensions = event.dimensions() - val description = event.content.ifBlank { null } ?: event.alt() - val isImage = RichTextParser.isImageUrl(fullUrl) - val uri = note.toNostrUri() - - mutableStateOf( - if (isImage) { - MediaUrlImage( - url = fullUrl, - description = description, - hash = hash, - blurhash = blurHash, - dim = dimensions, - uri = uri, - ) - } else { - MediaUrlVideo( - url = fullUrl, - description = description, - hash = hash, - dim = dimensions, - uri = uri, - authorName = note.author?.toBestDisplayName(), - artworkUri = event.thumb() ?: event.image(), - ) - }, - ) - } - - SensitivityWarning(note = note, accountViewModel = accountViewModel) { - Column( - modifier = - Modifier - .fillMaxWidth() - .padding(top = 5.dp), - horizontalAlignment = Alignment.CenterHorizontally, - ) { - if (isYouTube) { - val uri = LocalUriHandler.current - Row( - modifier = Modifier.clickable { runCatching { uri.openUri(fullUrl) } }, - ) { - image?.let { - AsyncImage( - model = it, - contentDescription = - stringResource( - R.string.preview_card_image_for, - it, - ), - contentScale = ContentScale.FillWidth, - modifier = MaterialTheme.colorScheme.imageModifier, - ) - } - ?: CreateImageHeader(note, accountViewModel) - } - } else { - ZoomableContentView( - content = content, - roundedCorner = true, - accountViewModel = accountViewModel, - ) - } - - title?.let { - Text( - text = it, - fontWeight = FontWeight.Bold, - maxLines = 3, - overflow = TextOverflow.Ellipsis, - modifier = - Modifier - .fillMaxWidth() - .padding(top = 5.dp), - ) - } - - summary?.let { - TranslatableRichTextViewer( - content = it, - canPreview = canPreview && !makeItShort, - modifier = Modifier.fillMaxWidth(), - tags = tags, - backgroundColor = backgroundColor, - accountViewModel = accountViewModel, - nav = nav, - ) - } - - if (event.hasHashtags()) { - Row( - Modifier.fillMaxWidth(), - ) { - DisplayUncitedHashtags( - remember(event) { event.hashtags().toImmutableList() }, - summary ?: "", - nav, - ) - } - } - } - } -} - -@Composable -fun FileStorageHeaderDisplay( - baseNote: Note, - roundedCorner: Boolean, - accountViewModel: AccountViewModel, -) { - val eventHeader = (baseNote.event as? FileStorageHeaderEvent) ?: return - val dataEventId = eventHeader.dataEventId() ?: return - - LoadNote(baseNoteHex = dataEventId, accountViewModel) { contentNote -> - if (contentNote != null) { - ObserverAndRenderNIP95(baseNote, contentNote, roundedCorner, accountViewModel) - } - } -} - -@Composable -private fun ObserverAndRenderNIP95( - header: Note, - content: Note, - roundedCorner: Boolean, - accountViewModel: AccountViewModel, -) { - val eventHeader = (header.event as? FileStorageHeaderEvent) ?: return - - val appContext = LocalContext.current.applicationContext - - val noteState by content.live().metadata.observeAsState() - - val content by - remember(noteState) { - // Creates a new object when the event arrives to force an update of the image. - val note = noteState?.note - val uri = header.toNostrUri() - val localDir = note?.idHex?.let { File(File(appContext.cacheDir, "NIP95"), it) } - val blurHash = eventHeader.blurhash() - val dimensions = eventHeader.dimensions() - val description = eventHeader.alt() ?: eventHeader.content - val mimeType = eventHeader.mimeType() - - val newContent = - if (mimeType?.startsWith("image") == true) { - MediaLocalImage( - localFile = localDir, - mimeType = mimeType, - description = description, - dim = dimensions, - blurhash = blurHash, - isVerified = true, - uri = uri, - ) - } else { - MediaLocalVideo( - localFile = localDir, - mimeType = mimeType, - description = description, - dim = dimensions, - isVerified = true, - uri = uri, - authorName = header.author?.toBestDisplayName(), - ) - } - - mutableStateOf(newContent) - } - - Crossfade(targetState = content) { - if (it != null) { - SensitivityWarning(note = header, accountViewModel = accountViewModel) { - ZoomableContentView( - content = it, - roundedCorner = roundedCorner, - accountViewModel = accountViewModel, - ) - } - } - } -} - -@Composable -fun AudioTrackHeader( - noteEvent: AudioTrackEvent, - note: Note, - accountViewModel: AccountViewModel, - nav: (String) -> Unit, -) { - val media = remember { noteEvent.media() } - val cover = remember { noteEvent.cover() } - val subject = remember { noteEvent.subject() } - val content = remember { noteEvent.content() } - val participants = remember { noteEvent.participants() } - - var participantUsers by remember { mutableStateOf>>(emptyList()) } - - LaunchedEffect(key1 = participants) { - accountViewModel.loadParticipants(participants) { participantUsers = it } - } - - Row(modifier = Modifier.padding(top = 5.dp)) { - Column(modifier = Modifier.fillMaxWidth(), horizontalAlignment = Alignment.CenterHorizontally) { - Row { - subject?.let { - Row( - verticalAlignment = CenterVertically, - modifier = Modifier.padding(top = 5.dp, bottom = 5.dp), - ) { - Text( - text = it, - fontWeight = FontWeight.Bold, - maxLines = 3, - overflow = TextOverflow.Ellipsis, - modifier = Modifier.fillMaxWidth(), - ) - } - } - } - - participantUsers.forEach { - Row( - verticalAlignment = CenterVertically, - modifier = - Modifier - .padding(top = 5.dp, start = 10.dp, end = 10.dp) - .clickable { - nav("User/${it.second.pubkeyHex}") - }, - ) { - ClickableUserPicture(it.second, 25.dp, accountViewModel) - Spacer(Modifier.width(5.dp)) - UsernameDisplay(it.second, Modifier.weight(1f)) - Spacer(Modifier.width(5.dp)) - it.first.role?.let { - Text( - text = it.capitalize(Locale.ROOT), - color = MaterialTheme.colorScheme.placeholderText, - maxLines = 1, - ) - } - } - } - - media?.let { media -> - Row( - verticalAlignment = CenterVertically, - ) { - cover?.let { cover -> - LoadThumbAndThenVideoView( - videoUri = media, - title = noteEvent.subject(), - thumbUri = cover, - authorName = note.author?.toBestDisplayName(), - roundedCorner = true, - nostrUriCallback = "nostr:${note.toNEvent()}", - accountViewModel = accountViewModel, - ) - } - ?: VideoView( - videoUri = media, - title = noteEvent.subject(), - authorName = note.author?.toBestDisplayName(), - roundedCorner = true, - accountViewModel = accountViewModel, - ) - } - } - } - } -} - -@Composable -fun AudioHeader( - noteEvent: AudioHeaderEvent, - note: Note, - accountViewModel: AccountViewModel, - nav: (String) -> Unit, -) { - val media = remember { noteEvent.stream() ?: noteEvent.download() } - val waveform = remember { noteEvent.wavefrom()?.toImmutableList()?.ifEmpty { null } } - val content = remember { noteEvent.content().ifBlank { null } } - - val defaultBackground = MaterialTheme.colorScheme.background - val background = remember { mutableStateOf(defaultBackground) } - val tags = remember(noteEvent) { noteEvent.tags()?.toImmutableListOfLists() ?: EmptyTagList } - - Row(modifier = Modifier.padding(top = 5.dp)) { - Column(modifier = Modifier.fillMaxWidth(), horizontalAlignment = Alignment.CenterHorizontally) { - media?.let { media -> - Row( - verticalAlignment = CenterVertically, - ) { - VideoView( - videoUri = media, - waveform = waveform, - title = noteEvent.subject(), - authorName = note.author?.toBestDisplayName(), - roundedCorner = true, - accountViewModel = accountViewModel, - nostrUriCallback = note.toNostrUri(), - ) - } - } - - content?.let { - Row( - verticalAlignment = CenterVertically, - modifier = - Modifier - .fillMaxWidth() - .padding(top = 5.dp), - ) { - TranslatableRichTextViewer( - content = it, - canPreview = true, - tags = tags, - backgroundColor = background, - accountViewModel = accountViewModel, - nav = nav, - ) - } - } - - if (noteEvent.hasHashtags()) { - Row(Modifier.fillMaxWidth(), verticalAlignment = CenterVertically) { - val hashtags = remember(noteEvent) { noteEvent.hashtags().toImmutableList() } - DisplayUncitedHashtags(hashtags, content ?: "", nav) - } - } - } - } -} - -@Composable -fun RenderGitPatchEvent( - baseNote: Note, - makeItShort: Boolean, - canPreview: Boolean, - backgroundColor: MutableState, - accountViewModel: AccountViewModel, - nav: (String) -> Unit, -) { - val event = baseNote.event as? GitPatchEvent ?: return - - RenderGitPatchEvent(event, baseNote, makeItShort, canPreview, backgroundColor, accountViewModel, nav) -} - -@Composable -private fun RenderShortRepositoryHeader( - baseNote: AddressableNote, - accountViewModel: AccountViewModel, - nav: (String) -> Unit, -) { - val noteState by baseNote.live().metadata.observeAsState() - val noteEvent = noteState?.note?.event as? GitRepositoryEvent ?: return - - Column( - modifier = MaterialTheme.colorScheme.replyModifier.padding(10.dp), - ) { - val title = remember(noteEvent) { noteEvent.name() ?: noteEvent.dTag() } - Text( - text = stringResource(id = R.string.git_repository, title), - style = MaterialTheme.typography.titleLarge, - maxLines = 1, - overflow = TextOverflow.Ellipsis, - modifier = Modifier.fillMaxWidth(), - ) - - noteEvent.description()?.let { - Spacer(modifier = DoubleVertSpacer) - Text( - text = it, - maxLines = 1, - overflow = TextOverflow.Ellipsis, - ) - } - } -} - -@Composable -private fun RenderGitPatchEvent( - noteEvent: GitPatchEvent, - note: Note, - makeItShort: Boolean, - canPreview: Boolean, - backgroundColor: MutableState, - accountViewModel: AccountViewModel, - nav: (String) -> Unit, -) { - val repository = remember(noteEvent) { noteEvent.repository() } - - if (repository != null) { - LoadAddressableNote(aTag = repository, accountViewModel = accountViewModel) { - if (it != null) { - RenderShortRepositoryHeader(it, accountViewModel, nav) - Spacer(modifier = DoubleVertSpacer) - } - } - } - - LoadDecryptedContent(note, accountViewModel) { body -> - val eventContent by - remember(note.event) { - derivedStateOf { - val subject = (note.event as? TextNoteEvent)?.subject()?.ifEmpty { null } - - if (!subject.isNullOrBlank() && !body.split("\n")[0].contains(subject)) { - "### $subject\n$body" - } else { - body - } - } - } - - val isAuthorTheLoggedUser = remember(note.event) { accountViewModel.isLoggedUser(note.author) } - - if (makeItShort && isAuthorTheLoggedUser) { - Text( - text = eventContent, - color = MaterialTheme.colorScheme.placeholderText, - maxLines = 2, - overflow = TextOverflow.Ellipsis, - ) - } else { - SensitivityWarning( - note = note, - accountViewModel = accountViewModel, - ) { - val modifier = remember(note) { Modifier.fillMaxWidth() } - val tags = remember(note) { note.event?.tags()?.toImmutableListOfLists() ?: EmptyTagList } - - TranslatableRichTextViewer( - content = eventContent, - canPreview = canPreview && !makeItShort, - modifier = modifier, - tags = tags, - backgroundColor = backgroundColor, - accountViewModel = accountViewModel, - nav = nav, - ) - } - - if (note.event?.hasHashtags() == true) { - val hashtags = - remember(note.event) { note.event?.hashtags()?.toImmutableList() ?: persistentListOf() } - DisplayUncitedHashtags(hashtags, eventContent, nav) - } - } - } -} - -@Composable -fun RenderGitIssueEvent( - baseNote: Note, - makeItShort: Boolean, - canPreview: Boolean, - backgroundColor: MutableState, - accountViewModel: AccountViewModel, - nav: (String) -> Unit, -) { - val event = baseNote.event as? GitIssueEvent ?: return - - RenderGitIssueEvent(event, baseNote, makeItShort, canPreview, backgroundColor, accountViewModel, nav) -} - -@Composable -private fun RenderGitIssueEvent( - noteEvent: GitIssueEvent, - note: Note, - makeItShort: Boolean, - canPreview: Boolean, - backgroundColor: MutableState, - accountViewModel: AccountViewModel, - nav: (String) -> Unit, -) { - val repository = remember(noteEvent) { noteEvent.repository() } - - if (repository != null) { - LoadAddressableNote(aTag = repository, accountViewModel = accountViewModel) { - if (it != null) { - RenderShortRepositoryHeader(it, accountViewModel, nav) - Spacer(modifier = DoubleVertSpacer) - } - } - } - - LoadDecryptedContent(note, accountViewModel) { body -> - val eventContent by - remember(note.event) { - derivedStateOf { - val subject = (note.event as? TextNoteEvent)?.subject()?.ifEmpty { null } - - if (!subject.isNullOrBlank() && !body.split("\n")[0].contains(subject)) { - "### $subject\n$body" - } else { - body - } - } - } - - val isAuthorTheLoggedUser = remember(note.event) { accountViewModel.isLoggedUser(note.author) } - - if (makeItShort && isAuthorTheLoggedUser) { - Text( - text = eventContent, - color = MaterialTheme.colorScheme.placeholderText, - maxLines = 2, - overflow = TextOverflow.Ellipsis, - ) - } else { - SensitivityWarning( - note = note, - accountViewModel = accountViewModel, - ) { - val modifier = remember(note) { Modifier.fillMaxWidth() } - val tags = remember(note) { note.event?.tags()?.toImmutableListOfLists() ?: EmptyTagList } - - TranslatableRichTextViewer( - content = eventContent, - canPreview = canPreview && !makeItShort, - modifier = modifier, - tags = tags, - backgroundColor = backgroundColor, - accountViewModel = accountViewModel, - nav = nav, - ) - } - - if (note.event?.hasHashtags() == true) { - val hashtags = - remember(note.event) { note.event?.hashtags()?.toImmutableList() ?: persistentListOf() } - DisplayUncitedHashtags(hashtags, eventContent, nav) - } - } - } -} - -@Composable -fun RenderGitRepositoryEvent( - baseNote: Note, - accountViewModel: AccountViewModel, - nav: (String) -> Unit, -) { - val event = baseNote.event as? GitRepositoryEvent ?: return - - RenderGitRepositoryEvent(event, baseNote, accountViewModel, nav) -} - -@Composable -private fun RenderGitRepositoryEvent( - noteEvent: GitRepositoryEvent, - note: Note, - accountViewModel: AccountViewModel, - nav: (String) -> Unit, -) { - val title = remember(noteEvent) { noteEvent.name() ?: noteEvent.dTag() } - val summary = remember(noteEvent) { noteEvent.description() } - val web = remember(noteEvent) { noteEvent.web() } - val clone = remember(noteEvent) { noteEvent.clone() } - - Row( - modifier = - Modifier - .clip(shape = QuoteBorder) - .border( - 1.dp, - MaterialTheme.colorScheme.subtleBorder, - QuoteBorder, - ).padding(Size10dp), - ) { - Column { - Text( - text = stringResource(id = R.string.git_repository, title), - style = MaterialTheme.typography.titleMedium, - maxLines = 1, - overflow = TextOverflow.Ellipsis, - modifier = Modifier.fillMaxWidth(), - ) - - summary?.let { - Text( - text = it, - modifier = Modifier.fillMaxWidth().padding(vertical = Size5dp), - maxLines = 3, - overflow = TextOverflow.Ellipsis, - ) - } - - HorizontalDivider(thickness = DividerThickness) - - web?.let { - Row(Modifier.fillMaxWidth().padding(top = Size5dp)) { - Text( - text = stringResource(id = R.string.git_web_address), - maxLines = 1, - overflow = TextOverflow.Ellipsis, - ) - Spacer(modifier = StdHorzSpacer) - ClickableUrl( - url = it, - urlText = it.removePrefix("https://").removePrefix("http://"), - ) - } - } - - clone?.let { - Row(Modifier.fillMaxWidth().padding(top = Size5dp)) { - Text( - text = stringResource(id = R.string.git_clone_address), - maxLines = 1, - overflow = TextOverflow.Ellipsis, - ) - Spacer(modifier = StdHorzSpacer) - ClickableUrl( - url = it, - urlText = it.removePrefix("https://").removePrefix("http://"), - ) - } - } - } - } -} - -@Composable -fun RenderLiveActivityEvent( - baseNote: Note, - accountViewModel: AccountViewModel, - nav: (String) -> Unit, -) { - Row(modifier = Modifier.padding(top = 5.dp)) { - Column( - modifier = Modifier.fillMaxWidth(), - horizontalAlignment = Alignment.CenterHorizontally, - ) { - RenderLiveActivityEventInner(baseNote = baseNote, accountViewModel, nav) - } - } -} - -@Composable -fun RenderLiveActivityEventInner( - baseNote: Note, - accountViewModel: AccountViewModel, - nav: (String) -> Unit, -) { - val noteEvent = baseNote.event as? LiveActivitiesEvent ?: return - - val eventUpdates by baseNote.live().metadata.observeAsState() - - val media = remember(eventUpdates) { noteEvent.streaming() } - val cover = remember(eventUpdates) { noteEvent.image() } - val subject = remember(eventUpdates) { noteEvent.title() } - val content = remember(eventUpdates) { noteEvent.summary() } - val participants = remember(eventUpdates) { noteEvent.participants() } - val status = remember(eventUpdates) { noteEvent.status() } - val starts = remember(eventUpdates) { noteEvent.starts() } - - Row( - verticalAlignment = CenterVertically, - modifier = - Modifier - .padding(vertical = 5.dp) - .fillMaxWidth(), - ) { - subject?.let { - Text( - text = it, - fontWeight = FontWeight.Bold, - maxLines = 3, - overflow = TextOverflow.Ellipsis, - modifier = Modifier.weight(1f), - ) - } - - Spacer(modifier = StdHorzSpacer) - - Crossfade(targetState = status, label = "RenderLiveActivityEventInner") { - when (it) { - STATUS_LIVE -> { - media?.let { CrossfadeCheckIfUrlIsOnline(it, accountViewModel) { LiveFlag() } } - } - STATUS_PLANNED -> { - ScheduledFlag(starts) - } - } - } - } - - var participantUsers by remember { - mutableStateOf>>( - persistentListOf(), - ) - } - - LaunchedEffect(key1 = eventUpdates) { - accountViewModel.loadParticipants(participants) { newParticipantUsers -> - if (!equalImmutableLists(newParticipantUsers, participantUsers)) { - participantUsers = newParticipantUsers - } - } - } - - media?.let { media -> - if (status == STATUS_LIVE) { - CheckIfUrlIsOnline(media, accountViewModel) { isOnline -> - if (isOnline) { - Row( - verticalAlignment = CenterVertically, - ) { - VideoView( - videoUri = media, - title = subject, - artworkUri = cover, - authorName = baseNote.author?.toBestDisplayName(), - roundedCorner = true, - accountViewModel = accountViewModel, - nostrUriCallback = "nostr:${baseNote.toNEvent()}", - ) - } - } else { - Row( - verticalAlignment = CenterVertically, - modifier = - Modifier - .padding(10.dp) - .height(100.dp), - ) { - Text( - text = stringResource(id = R.string.live_stream_is_offline), - color = MaterialTheme.colorScheme.onBackground, - fontWeight = FontWeight.Bold, - ) - } - } - } - } else if (status == STATUS_ENDED) { - Row( - verticalAlignment = CenterVertically, - modifier = - Modifier - .padding(10.dp) - .height(100.dp), - ) { - Text( - text = stringResource(id = R.string.live_stream_has_ended), - color = MaterialTheme.colorScheme.onBackground, - fontWeight = FontWeight.Bold, - ) - } - } - } - - participantUsers.forEach { - Row( - verticalAlignment = CenterVertically, - modifier = - Modifier - .padding(vertical = 5.dp) - .clickable { nav("User/${it.second.pubkeyHex}") }, - ) { - ClickableUserPicture(it.second, 25.dp, accountViewModel) - Spacer(StdHorzSpacer) - UsernameDisplay(it.second, Modifier.weight(1f)) - Spacer(StdHorzSpacer) - it.first.role?.let { - Text( - text = it.capitalize(Locale.ROOT), - color = MaterialTheme.colorScheme.placeholderText, - maxLines = 1, - ) - } - } - } -} - -@Composable -private fun LongFormHeader( - noteEvent: LongTextNoteEvent, - note: Note, - accountViewModel: AccountViewModel, -) { - val image = remember(noteEvent) { noteEvent.image() } - val title = remember(noteEvent) { noteEvent.title() } - val summary = - remember(noteEvent) { - noteEvent.summary()?.ifBlank { null } ?: noteEvent.content.take(200).ifBlank { null } - } - - Row( - modifier = - Modifier - .padding(top = Size5dp) - .clip(shape = QuoteBorder) - .border( - 1.dp, - MaterialTheme.colorScheme.subtleBorder, - QuoteBorder, - ), - ) { - Column { - val automaticallyShowUrlPreview = remember { accountViewModel.settings.showUrlPreview.value } - - if (automaticallyShowUrlPreview) { - image?.let { - AsyncImage( - model = it, - contentDescription = - stringResource( - R.string.preview_card_image_for, - it, - ), - contentScale = ContentScale.FillWidth, - modifier = Modifier.fillMaxWidth(), - ) - } - ?: CreateImageHeader(note, accountViewModel) - } - - title?.let { - Text( - text = it, - style = MaterialTheme.typography.bodyLarge, - modifier = - Modifier - .fillMaxWidth() - .padding(start = 10.dp, end = 10.dp, top = 10.dp), - ) - } - - summary?.let { - Spacer(modifier = StdVertSpacer) - Text( - text = it, - style = MaterialTheme.typography.bodySmall, - modifier = - Modifier - .fillMaxWidth() - .padding(start = 10.dp, end = 10.dp, bottom = 10.dp), - color = Color.Gray, - maxLines = 3, - overflow = TextOverflow.Ellipsis, - ) - } - } - } -} - -@Composable -private fun RenderWikiContent( - note: Note, - accountViewModel: AccountViewModel, - nav: (String) -> Unit, -) { - val noteEvent = note.event as? WikiNoteEvent ?: return - - WikiNoteHeader(noteEvent, note, accountViewModel, nav) -} - -@Composable -private fun WikiNoteHeader( - noteEvent: WikiNoteEvent, - note: Note, - accountViewModel: AccountViewModel, - nav: (String) -> Unit, -) { - val title = remember(noteEvent) { noteEvent.title() } - val summary = - remember(noteEvent) { - noteEvent.summary()?.ifBlank { null } ?: noteEvent.content.take(200).ifBlank { null } - } - val image = remember(noteEvent) { noteEvent.image() } - - Row( - modifier = - Modifier - .padding(top = Size5dp) - .clip(shape = QuoteBorder) - .border( - 1.dp, - MaterialTheme.colorScheme.subtleBorder, - QuoteBorder, - ), - ) { - Column { - val automaticallyShowUrlPreview = remember { accountViewModel.settings.showUrlPreview.value } - - if (automaticallyShowUrlPreview) { - image?.let { - AsyncImage( - model = it, - contentDescription = - stringResource( - R.string.preview_card_image_for, - it, - ), - contentScale = ContentScale.FillWidth, - modifier = Modifier.fillMaxWidth(), - ) - } - ?: CreateImageHeader(note, accountViewModel) - } - - title?.let { - Text( - text = it, - style = MaterialTheme.typography.bodyLarge, - modifier = - Modifier - .fillMaxWidth() - .padding(start = 10.dp, end = 10.dp, top = 10.dp), - ) - } - - summary?.let { - Spacer(modifier = StdVertSpacer) - Text( - text = it, - style = MaterialTheme.typography.bodySmall, - modifier = - Modifier - .fillMaxWidth() - .padding(start = 10.dp, end = 10.dp, bottom = 10.dp), - color = Color.Gray, - maxLines = 3, - overflow = TextOverflow.Ellipsis, - ) - } - } - } -} - -@Composable -private fun RenderClassifieds( - noteEvent: ClassifiedsEvent, - note: Note, - accountViewModel: AccountViewModel, - nav: (String) -> Unit, -) { - val image = remember(noteEvent) { noteEvent.image() } - val title = remember(noteEvent) { noteEvent.title() } - val summary = - remember(noteEvent) { noteEvent.summary() ?: noteEvent.content.take(200).ifBlank { null } } - val price = remember(noteEvent) { noteEvent.price() } - val location = remember(noteEvent) { noteEvent.location() } - - Row( - modifier = - Modifier - .clip(shape = QuoteBorder) - .border( - 1.dp, - MaterialTheme.colorScheme.subtleBorder, - QuoteBorder, - ), - ) { - Column { - Row { - image?.let { - AsyncImage( - model = it, - contentDescription = - stringResource( - R.string.preview_card_image_for, - it, - ), - contentScale = ContentScale.FillWidth, - modifier = Modifier.fillMaxWidth(), - ) - } - ?: CreateImageHeader(note, accountViewModel) - } - - Row( - Modifier.padding(start = 10.dp, end = 10.dp, top = 10.dp), - verticalAlignment = CenterVertically, - ) { - title?.let { - Text( - text = it, - style = MaterialTheme.typography.bodyLarge, - maxLines = 1, - overflow = TextOverflow.Ellipsis, - modifier = Modifier.weight(1f), - ) - } - - price?.let { - val priceTag = - remember(noteEvent) { - val newAmount = - price.amount.toBigDecimalOrNull()?.let { showAmount(it) } ?: price.amount - - if (price.frequency != null && price.currency != null) { - "$newAmount ${price.currency}/${price.frequency}" - } else if (price.currency != null) { - "$newAmount ${price.currency}" - } else { - newAmount - } - } - - Text( - text = priceTag, - maxLines = 1, - color = MaterialTheme.colorScheme.primary, - fontWeight = FontWeight.Bold, - modifier = - remember { - Modifier - .clip(SmallBorder) - .padding(start = 5.dp) - }, - ) - } - } - - if (summary != null || location != null) { - Row( - Modifier.padding(start = 10.dp, end = 10.dp, top = 5.dp), - verticalAlignment = CenterVertically, - ) { - summary?.let { - Text( - text = it, - style = MaterialTheme.typography.bodySmall, - modifier = Modifier.weight(1f), - color = Color.Gray, - maxLines = 3, - overflow = TextOverflow.Ellipsis, - ) - } - - /* - Column { - location?.let { - Text( - text = it, - style = MaterialTheme.typography.bodySmall, - color = Color.Gray, - maxLines = 1, - overflow = TextOverflow.Ellipsis, - modifier = Modifier.padding(start = 5.dp) - ) - } - - Button( - modifier = Modifier - .padding(horizontal = 3.dp) - .width(50.dp), - onClick = { - note.author?.let { - accountViewModel.createChatRoomFor(it) { - nav("Room/$it") - } - } - }, - contentPadding = ZeroPadding, - colors = ButtonDefaults - .buttonColors( - containerColor = MaterialTheme.colorScheme.primary - ) - ) { - Icon( - painter = painterResource(R.drawable.ic_dm), - stringResource(R.string.send_a_direct_message), - modifier = Modifier.size(20.dp), - tint = Color.White - ) - } - } - - */ - } - } - - Spacer(modifier = DoubleVertSpacer) - } - } -} - -@Composable -fun CreateImageHeader( - note: Note, - accountViewModel: AccountViewModel, -) { - val banner = remember(note.author?.info) { note.author?.info?.banner } - - Box { - banner?.let { - AsyncImage( - model = it, - contentDescription = - stringResource( - R.string.preview_card_image_for, - it, - ), - contentScale = ContentScale.FillWidth, - modifier = Modifier.fillMaxWidth(), - ) - } - ?: Image( - painter = painterResource(R.drawable.profile_banner), - contentDescription = stringResource(R.string.profile_banner), - contentScale = ContentScale.FillWidth, - modifier = - remember { - Modifier - .fillMaxWidth() - .height(150.dp) - }, - ) - - Box( - remember { - Modifier - .width(75.dp) - .height(75.dp) - .padding(10.dp) - .align(Alignment.BottomStart) - }, - ) { - NoteAuthorPicture(baseNote = note, accountViewModel = accountViewModel, size = Size55dp) - } - } -} - -@Preview -@Composable -fun RenderEyeGlassesPrescriptionPreview() { - 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) -} - -@Composable -fun RenderFhirResource( - baseNote: Note, - accountViewModel: AccountViewModel, - nav: (String) -> Unit, -) { - val event = baseNote.event as? FhirResourceEvent ?: return - - RenderFhirResource(event) -} - -@Composable -fun RenderFhirResource(event: FhirResourceEvent) { - val state by produceState(initialValue = FhirElementDatabase(), key1 = event) { - withContext(Dispatchers.Default) { - parseResourceBundleOrNull(event.content)?.let { - value = it - } - } - } - - state.baseResource?.let { resource -> - when (resource) { - is Bundle -> { - val vision = resource.entry.filterIsInstance(VisionPrescription::class.java) - - vision.firstOrNull()?.let { - RenderEyeGlassesPrescription(it, state.localDb) - } - } - is VisionPrescription -> { - RenderEyeGlassesPrescription(resource, state.localDb) - } - else -> { - } - } - } -} - -@Composable -fun RenderEyeGlassesPrescription( - visionPrescription: VisionPrescription, - db: ImmutableMap, -) { - Column( - modifier = - Modifier - .fillMaxWidth() - .padding(horizontal = 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) - } - - visionPrescription.prescriber?.reference?.let { - val practitioner = findReferenceInDb(it, db) as? Practitioner - - practitioner?.name?.firstOrNull()?.assembleName()?.let { - Spacer(DoubleVertSpacer) - 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/app/src/main/java/com/vitorpamplona/amethyst/ui/note/NoteQuickActionMenu.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/NoteQuickActionMenu.kt index d2bbb7f59..a60b15af4 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/note/NoteQuickActionMenu.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/NoteQuickActionMenu.kt @@ -29,7 +29,6 @@ import androidx.compose.foundation.layout.IntrinsicSize import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer -import androidx.compose.foundation.layout.fillMaxHeight import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding @@ -52,11 +51,12 @@ import androidx.compose.material3.ButtonColors import androidx.compose.material3.ButtonDefaults import androidx.compose.material3.Card import androidx.compose.material3.CardDefaults -import androidx.compose.material3.Divider +import androidx.compose.material3.HorizontalDivider import androidx.compose.material3.Icon import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text import androidx.compose.material3.TextButton +import androidx.compose.material3.VerticalDivider import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.MutableState @@ -128,13 +128,6 @@ val externalLinkForNote = { note: Note -> } } -@Composable -private fun VerticalDivider(color: Color) = - Divider( - color = color, - modifier = Modifier.fillMaxHeight().width(1.dp), - ) - @Composable fun LongPressToQuickAction( baseNote: Note, @@ -263,7 +256,7 @@ private fun RenderMainPopup( onDismiss() } - VerticalDivider(primaryLight) + VerticalDivider(color = primaryLight) NoteQuickActionItem( Icons.Default.AlternateEmail, stringResource(R.string.quick_action_copy_user_id), @@ -274,7 +267,7 @@ private fun RenderMainPopup( onDismiss() } } - VerticalDivider(primaryLight) + VerticalDivider(color = primaryLight) NoteQuickActionItem( Icons.Default.FormatQuote, stringResource(R.string.quick_action_copy_note_id), @@ -287,7 +280,7 @@ private fun RenderMainPopup( } if (!isOwnNote) { - VerticalDivider(primaryLight) + VerticalDivider(color = primaryLight) NoteQuickActionItem( Icons.Default.Block, @@ -302,9 +295,8 @@ private fun RenderMainPopup( } } } - Divider( + HorizontalDivider( color = primaryLight, - modifier = Modifier.fillMaxWidth().width(1.dp), ) Row(modifier = Modifier.height(IntrinsicSize.Min)) { if (isOwnNote) { @@ -337,7 +329,7 @@ private fun RenderMainPopup( } } - VerticalDivider(primaryLight) + VerticalDivider(color = primaryLight) NoteQuickActionItem( icon = ImageVector.vectorResource(id = R.drawable.relays), label = stringResource(R.string.broadcast), @@ -346,7 +338,7 @@ private fun RenderMainPopup( // showSelectTextDialog = true onDismiss() } - VerticalDivider(primaryLight) + VerticalDivider(color = primaryLight) NoteQuickActionItem( icon = Icons.Default.Share, label = stringResource(R.string.quick_action_share), @@ -375,7 +367,7 @@ private fun RenderMainPopup( } if (!isOwnNote) { - VerticalDivider(primaryLight) + VerticalDivider(color = primaryLight) NoteQuickActionItem( Icons.Default.Report, diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/note/ReactionsRow.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/ReactionsRow.kt index 64986be17..49c4f39bd 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/note/ReactionsRow.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/ReactionsRow.kt @@ -103,6 +103,7 @@ import com.vitorpamplona.amethyst.ui.actions.NewPostView import com.vitorpamplona.amethyst.ui.components.GenericLoadable import com.vitorpamplona.amethyst.ui.components.InLineIconRenderer import com.vitorpamplona.amethyst.ui.navigation.routeToMessage +import com.vitorpamplona.amethyst.ui.note.types.EditState import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel import com.vitorpamplona.amethyst.ui.theme.ButtonBorder import com.vitorpamplona.amethyst.ui.theme.DarkerGreen @@ -306,7 +307,7 @@ fun RenderZapRaiser( LinearProgressIndicator( modifier = remember(details) { Modifier.fillMaxWidth().height(if (details) 24.dp else 4.dp) }, color = color, - progress = zapraiserStatus.progress, + progress = { zapraiserStatus.progress }, ) if (details) { diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/note/RelayCompose.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/RelayCompose.kt index 55a1a16e7..a39aa5c26 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/note/RelayCompose.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/RelayCompose.kt @@ -26,7 +26,7 @@ import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding import androidx.compose.material3.Button import androidx.compose.material3.ButtonDefaults -import androidx.compose.material3.Divider +import androidx.compose.material3.HorizontalDivider import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text import androidx.compose.runtime.Composable @@ -101,7 +101,7 @@ fun RelayCompose( } } - Divider( + HorizontalDivider( modifier = Modifier.padding(top = 10.dp), thickness = DividerThickness, ) diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/note/UpdateReactionTypeDialog.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/UpdateReactionTypeDialog.kt index b6861152c..68d1109c8 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/note/UpdateReactionTypeDialog.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/UpdateReactionTypeDialog.kt @@ -82,6 +82,7 @@ import com.vitorpamplona.amethyst.ui.actions.CloseButton import com.vitorpamplona.amethyst.ui.actions.SaveButton import com.vitorpamplona.amethyst.ui.components.InLineIconRenderer import com.vitorpamplona.amethyst.ui.navigation.routeFor +import com.vitorpamplona.amethyst.ui.note.types.RenderEmojiPack import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel import com.vitorpamplona.amethyst.ui.theme.ButtonBorder import com.vitorpamplona.amethyst.ui.theme.placeholderText diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/note/UpdateZapAmountDialog.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/UpdateZapAmountDialog.kt index a38762655..bd8599b8a 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/note/UpdateZapAmountDialog.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/UpdateZapAmountDialog.kt @@ -51,7 +51,7 @@ import androidx.compose.material.icons.outlined.Visibility import androidx.compose.material.icons.outlined.VisibilityOff import androidx.compose.material3.Button import androidx.compose.material3.ButtonDefaults -import androidx.compose.material3.Divider +import androidx.compose.material3.HorizontalDivider import androidx.compose.material3.Icon import androidx.compose.material3.IconButton import androidx.compose.material3.MaterialTheme @@ -408,7 +408,7 @@ fun UpdateZapAmountDialog( ) } - Divider( + HorizontalDivider( modifier = Modifier.padding(vertical = 10.dp), thickness = DividerThickness, ) diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/note/UserCompose.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/UserCompose.kt index d808056d3..6026fb233 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/note/UserCompose.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/UserCompose.kt @@ -24,7 +24,7 @@ import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.padding -import androidx.compose.material3.Divider +import androidx.compose.material3.HorizontalDivider import androidx.compose.runtime.Composable import androidx.compose.runtime.remember import androidx.compose.ui.Alignment @@ -68,7 +68,7 @@ fun UserCompose( } if (showDiviser) { - Divider( + HorizontalDivider( thickness = DividerThickness, ) } diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/note/UserProfilePicture.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/UserProfilePicture.kt index 50f77676d..523afae1d 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/note/UserProfilePicture.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/UserProfilePicture.kt @@ -20,7 +20,6 @@ */ package com.vitorpamplona.amethyst.ui.note -import android.content.Intent import androidx.compose.animation.AnimatedVisibility import androidx.compose.animation.Crossfade import androidx.compose.animation.fadeIn @@ -34,50 +33,30 @@ import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.size import androidx.compose.foundation.shape.CircleShape import androidx.compose.material.ripple.rememberRipple -import androidx.compose.material3.Divider -import androidx.compose.material3.DropdownMenu -import androidx.compose.material3.DropdownMenuItem import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.Text import androidx.compose.runtime.Composable -import androidx.compose.runtime.Immutable -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 -import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember -import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip import androidx.compose.ui.layout.ContentScale -import androidx.compose.ui.platform.LocalClipboardManager -import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.stringResource import androidx.compose.ui.semantics.Role -import androidx.compose.ui.text.AnnotatedString import androidx.compose.ui.unit.Dp -import androidx.core.content.ContextCompat import androidx.lifecycle.distinctUntilChanged import androidx.lifecycle.map import com.vitorpamplona.amethyst.R import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.model.User -import com.vitorpamplona.amethyst.ui.actions.EditPostView -import com.vitorpamplona.amethyst.ui.components.GenericLoadable import com.vitorpamplona.amethyst.ui.components.RobohashAsyncImage import com.vitorpamplona.amethyst.ui.components.RobohashFallbackAsyncImage import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel -import com.vitorpamplona.amethyst.ui.screen.loggedIn.ReportNoteDialog import com.vitorpamplona.quartz.encoders.HexKey -import com.vitorpamplona.quartz.events.TextNoteEvent import kotlinx.collections.immutable.ImmutableSet -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.launch @Composable fun NoteAuthorPicture( @@ -443,313 +422,3 @@ fun WatchUserFollows( onFollowChanges(showFollowingMark) } - -@Immutable -data class DropDownParams( - val isFollowingAuthor: Boolean, - val isPrivateBookmarkNote: Boolean, - val isPublicBookmarkNote: Boolean, - val isLoggedUser: Boolean, - val isSensitive: Boolean, - val showSensitiveContent: Boolean?, -) - -@Composable -fun NoteDropDownMenu( - note: Note, - popupExpanded: MutableState, - editState: State>? = null, - accountViewModel: AccountViewModel, - nav: (String) -> Unit, -) { - var reportDialogShowing by remember { mutableStateOf(false) } - - var state by remember { - mutableStateOf( - DropDownParams( - isFollowingAuthor = false, - isPrivateBookmarkNote = false, - isPublicBookmarkNote = false, - isLoggedUser = false, - isSensitive = false, - showSensitiveContent = null, - ), - ) - } - - val onDismiss = remember(popupExpanded) { { popupExpanded.value = false } } - - val wantsToEditPost = - remember { - mutableStateOf(false) - } - - if (wantsToEditPost.value) { - // avoids changing while drafting a note and a new event shows up. - val versionLookingAt = - remember { - (editState?.value as? GenericLoadable.Loaded)?.loaded?.modificationToShow?.value - } - - EditPostView( - onClose = { - popupExpanded.value = false - wantsToEditPost.value = false - }, - edit = note, - versionLookingAt = versionLookingAt, - accountViewModel = accountViewModel, - nav = nav, - ) - } - - DropdownMenu( - expanded = popupExpanded.value, - onDismissRequest = onDismiss, - ) { - val clipboardManager = LocalClipboardManager.current - val appContext = LocalContext.current.applicationContext - val actContext = LocalContext.current - - WatchBookmarksFollowsAndAccount(note, accountViewModel) { newState -> - if (state != newState) { - state = newState - } - } - - val scope = rememberCoroutineScope() - - if (!state.isFollowingAuthor) { - DropdownMenuItem( - text = { Text(stringResource(R.string.follow)) }, - onClick = { - val author = note.author ?: return@DropdownMenuItem - accountViewModel.follow(author) - onDismiss() - }, - ) - Divider() - } - DropdownMenuItem( - text = { Text(stringResource(R.string.copy_text)) }, - onClick = { - scope.launch(Dispatchers.IO) { - accountViewModel.decrypt(note) { clipboardManager.setText(AnnotatedString(it)) } - onDismiss() - } - }, - ) - DropdownMenuItem( - text = { Text(stringResource(R.string.copy_user_pubkey)) }, - onClick = { - scope.launch(Dispatchers.IO) { - clipboardManager.setText(AnnotatedString("nostr:${note.author?.pubkeyNpub()}")) - onDismiss() - } - }, - ) - DropdownMenuItem( - text = { Text(stringResource(R.string.copy_note_id)) }, - onClick = { - scope.launch(Dispatchers.IO) { - clipboardManager.setText(AnnotatedString("nostr:" + note.toNEvent())) - onDismiss() - } - }, - ) - DropdownMenuItem( - text = { Text(stringResource(R.string.quick_action_share)) }, - onClick = { - val sendIntent = - Intent().apply { - action = Intent.ACTION_SEND - type = "text/plain" - putExtra( - Intent.EXTRA_TEXT, - externalLinkForNote(note), - ) - putExtra( - Intent.EXTRA_TITLE, - actContext.getString(R.string.quick_action_share_browser_link), - ) - } - - val shareIntent = - Intent.createChooser(sendIntent, appContext.getString(R.string.quick_action_share)) - ContextCompat.startActivity(actContext, shareIntent, null) - onDismiss() - }, - ) - Divider() - if (note.event is TextNoteEvent) { - if (state.isLoggedUser) { - DropdownMenuItem( - text = { Text(stringResource(R.string.edit_post)) }, - onClick = { - wantsToEditPost.value = true - }, - ) - } else { - DropdownMenuItem( - text = { Text(stringResource(R.string.propose_an_edit)) }, - onClick = { - wantsToEditPost.value = true - }, - ) - } - } - DropdownMenuItem( - text = { Text(stringResource(R.string.broadcast)) }, - onClick = { - accountViewModel.broadcast(note) - onDismiss() - }, - ) - Divider() - if (accountViewModel.account.hasPendingAttestations(note)) { - DropdownMenuItem( - text = { Text(stringResource(R.string.timestamp_pending)) }, - onClick = { - onDismiss() - }, - ) - } else { - DropdownMenuItem( - text = { Text(stringResource(R.string.timestamp_it)) }, - onClick = { - accountViewModel.timestamp(note) - onDismiss() - }, - ) - } - Divider() - if (state.isPrivateBookmarkNote) { - DropdownMenuItem( - text = { Text(stringResource(R.string.remove_from_private_bookmarks)) }, - onClick = { - accountViewModel.removePrivateBookmark(note) - onDismiss() - }, - ) - } else { - DropdownMenuItem( - text = { Text(stringResource(R.string.add_to_private_bookmarks)) }, - onClick = { - accountViewModel.addPrivateBookmark(note) - onDismiss() - }, - ) - } - if (state.isPublicBookmarkNote) { - DropdownMenuItem( - text = { Text(stringResource(R.string.remove_from_public_bookmarks)) }, - onClick = { - accountViewModel.removePublicBookmark(note) - onDismiss() - }, - ) - } else { - DropdownMenuItem( - text = { Text(stringResource(R.string.add_to_public_bookmarks)) }, - onClick = { - accountViewModel.addPublicBookmark(note) - onDismiss() - }, - ) - } - Divider() - if (state.showSensitiveContent == null || state.showSensitiveContent == true) { - DropdownMenuItem( - text = { Text(stringResource(R.string.content_warning_hide_all_sensitive_content)) }, - onClick = { - scope.launch(Dispatchers.IO) { - accountViewModel.hideSensitiveContent() - onDismiss() - } - }, - ) - } - if (state.showSensitiveContent == null || state.showSensitiveContent == false) { - DropdownMenuItem( - text = { Text(stringResource(R.string.content_warning_show_all_sensitive_content)) }, - onClick = { - scope.launch(Dispatchers.IO) { - accountViewModel.disableContentWarnings() - onDismiss() - } - }, - ) - } - if (state.showSensitiveContent != null) { - DropdownMenuItem( - text = { Text(stringResource(R.string.content_warning_see_warnings)) }, - onClick = { - scope.launch(Dispatchers.IO) { - accountViewModel.seeContentWarnings() - onDismiss() - } - }, - ) - } - Divider() - if (state.isLoggedUser) { - DropdownMenuItem( - text = { Text(stringResource(R.string.request_deletion)) }, - onClick = { - scope.launch(Dispatchers.IO) { - accountViewModel.delete(note) - onDismiss() - } - }, - ) - } else { - DropdownMenuItem( - text = { Text(stringResource(R.string.block_report)) }, - onClick = { reportDialogShowing = true }, - ) - } - } - - if (reportDialogShowing) { - ReportNoteDialog(note = note, accountViewModel = accountViewModel) { - reportDialogShowing = false - onDismiss() - } - } -} - -@Composable -fun WatchBookmarksFollowsAndAccount( - note: Note, - accountViewModel: AccountViewModel, - onNew: (DropDownParams) -> Unit, -) { - val followState by accountViewModel.userProfile().live().follows.observeAsState() - val bookmarkState by accountViewModel.userProfile().live().bookmarks.observeAsState() - val showSensitiveContent by - accountViewModel.showSensitiveContentChanges.observeAsState( - accountViewModel.account.showSensitiveContent, - ) - - LaunchedEffect(key1 = followState, key2 = bookmarkState, key3 = showSensitiveContent) { - launch(Dispatchers.IO) { - accountViewModel.isInPrivateBookmarks(note) { - val newState = - DropDownParams( - isFollowingAuthor = accountViewModel.isFollowing(note.author), - isPrivateBookmarkNote = it, - isPublicBookmarkNote = accountViewModel.isInPublicBookmarks(note), - isLoggedUser = accountViewModel.isLoggedUser(note.author), - isSensitive = note.event?.isSensitive() ?: false, - showSensitiveContent = showSensitiveContent, - ) - - launch(Dispatchers.Main) { - onNew( - newState, - ) - } - } - } - } -} diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/note/ZapNoteCompose.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/ZapNoteCompose.kt index 91d114954..d5d293cd6 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/note/ZapNoteCompose.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/ZapNoteCompose.kt @@ -25,7 +25,7 @@ import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.padding -import androidx.compose.material3.Divider +import androidx.compose.material3.HorizontalDivider import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text import androidx.compose.runtime.Composable @@ -91,7 +91,7 @@ fun ZapNoteCompose( ) { baseAuthor?.let { RenderZapNote(it, baseReqResponse.zapEvent, nav, accountViewModel) } - Divider( + HorizontalDivider( modifier = Modifier.padding(top = 10.dp), thickness = DividerThickness, ) diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/note/ZapTheDevsCard.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/ZapTheDevsCard.kt index 593f6bc3a..f00c680f8 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/note/ZapTheDevsCard.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/ZapTheDevsCard.kt @@ -69,9 +69,9 @@ import com.vitorpamplona.amethyst.model.ThemeType import com.vitorpamplona.amethyst.service.ZapPaymentHandler import com.vitorpamplona.amethyst.ui.components.ClickableText import com.vitorpamplona.amethyst.ui.components.LoadNote -import com.vitorpamplona.amethyst.ui.elements.DisplayZapSplits import com.vitorpamplona.amethyst.ui.navigation.routeFor import com.vitorpamplona.amethyst.ui.navigation.routeToMessage +import com.vitorpamplona.amethyst.ui.note.elements.DisplayZapSplits import com.vitorpamplona.amethyst.ui.screen.SharedPreferencesViewModel import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel import com.vitorpamplona.amethyst.ui.theme.ModifierWidth3dp diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/note/ZapUserSetCompose.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/ZapUserSetCompose.kt index 9f93e498e..2400fc84e 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/note/ZapUserSetCompose.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/ZapUserSetCompose.kt @@ -29,7 +29,7 @@ import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size -import androidx.compose.material3.Divider +import androidx.compose.material3.HorizontalDivider import androidx.compose.material3.MaterialTheme import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect @@ -132,7 +132,7 @@ fun ZapUserSetCompose( } } - Divider( + HorizontalDivider( thickness = DividerThickness, ) } diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/elements/AddRemoveButtons.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/elements/AddRemoveButtons.kt similarity index 98% rename from app/src/main/java/com/vitorpamplona/amethyst/ui/elements/AddRemoveButtons.kt rename to app/src/main/java/com/vitorpamplona/amethyst/ui/note/elements/AddRemoveButtons.kt index e656751b1..9ec25686a 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/elements/AddRemoveButtons.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/elements/AddRemoveButtons.kt @@ -18,7 +18,7 @@ * 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.elements +package com.vitorpamplona.amethyst.ui.note.elements import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.PaddingValues diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/note/elements/BoostedMark.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/elements/BoostedMark.kt new file mode 100644 index 000000000..acb98798c --- /dev/null +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/elements/BoostedMark.kt @@ -0,0 +1,41 @@ +/** + * 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.elements + +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.font.FontWeight +import com.vitorpamplona.amethyst.R +import com.vitorpamplona.amethyst.ui.theme.HalfStartPadding +import com.vitorpamplona.amethyst.ui.theme.placeholderText + +@Composable +fun BoostedMark() { + Text( + stringResource(id = R.string.boosted), + fontWeight = FontWeight.Bold, + color = MaterialTheme.colorScheme.placeholderText, + maxLines = 1, + modifier = HalfStartPadding, + ) +} diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/note/elements/DefaultImageHeader.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/elements/DefaultImageHeader.kt new file mode 100644 index 000000000..055f77bd0 --- /dev/null +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/elements/DefaultImageHeader.kt @@ -0,0 +1,70 @@ +/** + * 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.elements + +import androidx.compose.foundation.Image +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.layout.ContentScale +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource +import coil.compose.AsyncImage +import com.vitorpamplona.amethyst.R +import com.vitorpamplona.amethyst.model.Note +import com.vitorpamplona.amethyst.ui.note.NoteAuthorPicture +import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel +import com.vitorpamplona.amethyst.ui.theme.Size55dp +import com.vitorpamplona.amethyst.ui.theme.authorNotePictureForImageHeader +import com.vitorpamplona.amethyst.ui.theme.imageHeaderBannerSize + +@Composable +fun DefaultImageHeader( + note: Note, + accountViewModel: AccountViewModel, +) { + Box { + note.author?.info?.banner?.let { + AsyncImage( + model = it, + contentDescription = + stringResource( + R.string.preview_card_image_for, + it, + ), + contentScale = ContentScale.FillWidth, + modifier = Modifier.fillMaxWidth(), + ) + } + ?: Image( + painter = painterResource(R.drawable.profile_banner), + contentDescription = stringResource(R.string.profile_banner), + contentScale = ContentScale.FillWidth, + modifier = imageHeaderBannerSize, + ) + + Box(authorNotePictureForImageHeader.align(Alignment.BottomStart)) { + NoteAuthorPicture(baseNote = note, accountViewModel = accountViewModel, size = Size55dp) + } + } +} diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/elements/DisplayCommunity.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/elements/DisplayCommunity.kt similarity index 98% rename from app/src/main/java/com/vitorpamplona/amethyst/ui/elements/DisplayCommunity.kt rename to app/src/main/java/com/vitorpamplona/amethyst/ui/note/elements/DisplayCommunity.kt index 0685e1090..1459b0c81 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/elements/DisplayCommunity.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/elements/DisplayCommunity.kt @@ -18,7 +18,7 @@ * 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.elements +package com.vitorpamplona.amethyst.ui.note.elements import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/note/elements/DisplayEditStatus.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/elements/DisplayEditStatus.kt new file mode 100644 index 000000000..0981fc846 --- /dev/null +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/elements/DisplayEditStatus.kt @@ -0,0 +1,59 @@ +/** + * 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.elements + +import androidx.compose.foundation.text.ClickableText +import androidx.compose.material3.LocalTextStyle +import androidx.compose.material3.MaterialTheme +import androidx.compose.runtime.Composable +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.buildAnnotatedString +import androidx.compose.ui.text.font.FontWeight +import com.vitorpamplona.amethyst.R +import com.vitorpamplona.amethyst.ui.note.types.EditState +import com.vitorpamplona.amethyst.ui.theme.HalfStartPadding +import com.vitorpamplona.amethyst.ui.theme.placeholderText + +@Composable +fun DisplayEditStatus(editState: EditState) { + ClickableText( + text = + buildAnnotatedString { + if (editState.showingVersion.value == editState.originalVersionId()) { + append(stringResource(id = R.string.original)) + } else if (editState.showingVersion.value == editState.lastVersionId()) { + append(stringResource(id = R.string.edited)) + } else { + append(stringResource(id = R.string.edited_number, editState.versionId())) + } + }, + onClick = { + editState.nextModification() + }, + style = + LocalTextStyle.current.copy( + color = MaterialTheme.colorScheme.placeholderText, + fontWeight = FontWeight.Bold, + ), + maxLines = 1, + modifier = HalfStartPadding, + ) +} diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/elements/DisplayHashtags.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/elements/DisplayHashtags.kt similarity index 98% rename from app/src/main/java/com/vitorpamplona/amethyst/ui/elements/DisplayHashtags.kt rename to app/src/main/java/com/vitorpamplona/amethyst/ui/note/elements/DisplayHashtags.kt index cd9f71574..bd286ea84 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/elements/DisplayHashtags.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/elements/DisplayHashtags.kt @@ -18,7 +18,7 @@ * 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.elements +package com.vitorpamplona.amethyst.ui.note.elements import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/note/elements/DisplayLocation.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/elements/DisplayLocation.kt new file mode 100644 index 000000000..4f15be3cc --- /dev/null +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/elements/DisplayLocation.kt @@ -0,0 +1,53 @@ +/** + * 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.elements + +import androidx.compose.foundation.text.ClickableText +import androidx.compose.material3.LocalTextStyle +import androidx.compose.material3.MaterialTheme +import androidx.compose.runtime.Composable +import androidx.compose.ui.text.AnnotatedString +import androidx.compose.ui.text.font.FontWeight +import com.vitorpamplona.amethyst.ui.note.LoadCityName +import com.vitorpamplona.amethyst.ui.theme.Font14SP + +@Composable +fun DisplayLocation( + geohashStr: String, + nav: (String) -> Unit, +) { + LoadCityName(geohashStr) { cityName -> + ClickableText( + text = AnnotatedString(cityName), + onClick = { nav("Geohash/$geohashStr") }, + style = + LocalTextStyle.current.copy( + color = + MaterialTheme.colorScheme.primary.copy( + alpha = 0.52f, + ), + fontSize = Font14SP, + fontWeight = FontWeight.Bold, + ), + maxLines = 1, + ) + } +} diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/note/elements/DisplayOts.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/elements/DisplayOts.kt new file mode 100644 index 000000000..4c3488c64 --- /dev/null +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/elements/DisplayOts.kt @@ -0,0 +1,102 @@ +/** + * 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.elements + +import androidx.compose.foundation.text.ClickableText +import androidx.compose.material3.LocalTextStyle +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.buildAnnotatedString +import androidx.compose.ui.text.font.FontWeight +import com.vitorpamplona.amethyst.R +import com.vitorpamplona.amethyst.model.Note +import com.vitorpamplona.amethyst.ui.note.LoadOts +import com.vitorpamplona.amethyst.ui.note.timeAgoNoDot +import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel +import com.vitorpamplona.amethyst.ui.theme.Font14SP +import com.vitorpamplona.amethyst.ui.theme.lessImportantLink +import java.text.SimpleDateFormat +import java.util.Date + +@Composable +fun DisplayOts( + note: Note, + accountViewModel: AccountViewModel, +) { + LoadOts( + note, + accountViewModel, + whenConfirmed = { unixtimestamp -> + val context = LocalContext.current + val timeStr by remember(unixtimestamp) { + mutableStateOf( + timeAgoNoDot( + unixtimestamp, + context = context, + ), + ) + } + + ClickableText( + text = + buildAnnotatedString { + append( + stringResource( + id = R.string.existed_since, + timeStr, + ), + ) + }, + onClick = { + val fullDateTime = + SimpleDateFormat.getDateTimeInstance().format(Date(unixtimestamp * 1000)) + + accountViewModel.toast( + context.getString(R.string.ots_info_title), + context.getString(R.string.ots_info_description, fullDateTime), + ) + }, + style = + LocalTextStyle.current.copy( + color = MaterialTheme.colorScheme.lessImportantLink, + fontSize = Font14SP, + fontWeight = FontWeight.Bold, + ), + maxLines = 1, + ) + }, + whenPending = { + Text( + stringResource(id = R.string.timestamp_pending_short), + color = MaterialTheme.colorScheme.lessImportantLink, + fontSize = Font14SP, + fontWeight = FontWeight.Bold, + maxLines = 1, + ) + }, + ) +} diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/elements/DisplayPoW.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/elements/DisplayPoW.kt similarity index 97% rename from app/src/main/java/com/vitorpamplona/amethyst/ui/elements/DisplayPoW.kt rename to app/src/main/java/com/vitorpamplona/amethyst/ui/note/elements/DisplayPoW.kt index 184ebe35c..842dc8c5e 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/elements/DisplayPoW.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/elements/DisplayPoW.kt @@ -18,7 +18,7 @@ * 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.elements +package com.vitorpamplona.amethyst.ui.note.elements import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/elements/DisplayReward.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/elements/DisplayReward.kt similarity index 99% rename from app/src/main/java/com/vitorpamplona/amethyst/ui/elements/DisplayReward.kt rename to app/src/main/java/com/vitorpamplona/amethyst/ui/note/elements/DisplayReward.kt index 2c7520103..5fc6a69ed 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/elements/DisplayReward.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/elements/DisplayReward.kt @@ -18,7 +18,7 @@ * 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.elements +package com.vitorpamplona.amethyst.ui.note.elements import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Arrangement diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/elements/DisplayUncitedHashtags.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/elements/DisplayUncitedHashtags.kt similarity index 97% rename from app/src/main/java/com/vitorpamplona/amethyst/ui/elements/DisplayUncitedHashtags.kt rename to app/src/main/java/com/vitorpamplona/amethyst/ui/note/elements/DisplayUncitedHashtags.kt index e185ab24f..551f60689 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/elements/DisplayUncitedHashtags.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/elements/DisplayUncitedHashtags.kt @@ -18,7 +18,7 @@ * 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.elements +package com.vitorpamplona.amethyst.ui.note.elements import androidx.compose.foundation.layout.ExperimentalLayoutApi import androidx.compose.foundation.layout.FlowRow diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/elements/DisplayZapSplits.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/elements/DisplayZapSplits.kt similarity index 98% rename from app/src/main/java/com/vitorpamplona/amethyst/ui/elements/DisplayZapSplits.kt rename to app/src/main/java/com/vitorpamplona/amethyst/ui/note/elements/DisplayZapSplits.kt index ecdf5ad57..40d63f7e2 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/elements/DisplayZapSplits.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/elements/DisplayZapSplits.kt @@ -18,7 +18,7 @@ * 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.elements +package com.vitorpamplona.amethyst.ui.note.elements import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.ExperimentalLayoutApi diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/note/elements/DropDownMenu.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/elements/DropDownMenu.kt new file mode 100644 index 000000000..27d37e018 --- /dev/null +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/elements/DropDownMenu.kt @@ -0,0 +1,393 @@ +/** + * 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.elements + +import android.content.Intent +import androidx.compose.material3.DropdownMenu +import androidx.compose.material3.DropdownMenuItem +import androidx.compose.material3.HorizontalDivider +import androidx.compose.material3.IconButton +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.Immutable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.MutableState +import androidx.compose.runtime.State +import androidx.compose.runtime.getValue +import androidx.compose.runtime.livedata.observeAsState +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.runtime.setValue +import androidx.compose.ui.platform.LocalClipboardManager +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.AnnotatedString +import androidx.core.content.ContextCompat +import com.vitorpamplona.amethyst.R +import com.vitorpamplona.amethyst.model.Note +import com.vitorpamplona.amethyst.ui.actions.EditPostView +import com.vitorpamplona.amethyst.ui.components.GenericLoadable +import com.vitorpamplona.amethyst.ui.note.VerticalDotsIcon +import com.vitorpamplona.amethyst.ui.note.externalLinkForNote +import com.vitorpamplona.amethyst.ui.note.types.EditState +import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel +import com.vitorpamplona.amethyst.ui.screen.loggedIn.ReportNoteDialog +import com.vitorpamplona.amethyst.ui.theme.DividerThickness +import com.vitorpamplona.amethyst.ui.theme.Size24Modifier +import com.vitorpamplona.quartz.events.TextNoteEvent +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch + +@Composable +fun MoreOptionsButton( + baseNote: Note, + editState: State>? = null, + accountViewModel: AccountViewModel, + nav: (String) -> Unit, +) { + val popupExpanded = remember { mutableStateOf(false) } + + IconButton( + modifier = Size24Modifier, + onClick = { popupExpanded.value = true }, + ) { + VerticalDotsIcon(R.string.note_options) + + NoteDropDownMenu( + baseNote, + popupExpanded, + editState, + accountViewModel, + nav, + ) + } +} + +@Immutable +data class DropDownParams( + val isFollowingAuthor: Boolean, + val isPrivateBookmarkNote: Boolean, + val isPublicBookmarkNote: Boolean, + val isLoggedUser: Boolean, + val isSensitive: Boolean, + val showSensitiveContent: Boolean?, +) + +@Composable +fun NoteDropDownMenu( + note: Note, + popupExpanded: MutableState, + editState: State>? = null, + accountViewModel: AccountViewModel, + nav: (String) -> Unit, +) { + var reportDialogShowing by remember { mutableStateOf(false) } + + var state by remember { + mutableStateOf( + DropDownParams( + isFollowingAuthor = false, + isPrivateBookmarkNote = false, + isPublicBookmarkNote = false, + isLoggedUser = false, + isSensitive = false, + showSensitiveContent = null, + ), + ) + } + + val onDismiss = remember(popupExpanded) { { popupExpanded.value = false } } + + val wantsToEditPost = + remember { + mutableStateOf(false) + } + + if (wantsToEditPost.value) { + // avoids changing while drafting a note and a new event shows up. + val versionLookingAt = + remember { + (editState?.value as? GenericLoadable.Loaded)?.loaded?.modificationToShow?.value + } + + EditPostView( + onClose = { + popupExpanded.value = false + wantsToEditPost.value = false + }, + edit = note, + versionLookingAt = versionLookingAt, + accountViewModel = accountViewModel, + nav = nav, + ) + } + + DropdownMenu( + expanded = popupExpanded.value, + onDismissRequest = onDismiss, + ) { + val clipboardManager = LocalClipboardManager.current + val appContext = LocalContext.current.applicationContext + val actContext = LocalContext.current + + WatchBookmarksFollowsAndAccount(note, accountViewModel) { newState -> + if (state != newState) { + state = newState + } + } + + val scope = rememberCoroutineScope() + + if (!state.isFollowingAuthor) { + DropdownMenuItem( + text = { Text(stringResource(R.string.follow)) }, + onClick = { + val author = note.author ?: return@DropdownMenuItem + accountViewModel.follow(author) + onDismiss() + }, + ) + HorizontalDivider(thickness = DividerThickness) + } + DropdownMenuItem( + text = { Text(stringResource(R.string.copy_text)) }, + onClick = { + scope.launch(Dispatchers.IO) { + accountViewModel.decrypt(note) { clipboardManager.setText(AnnotatedString(it)) } + onDismiss() + } + }, + ) + DropdownMenuItem( + text = { Text(stringResource(R.string.copy_user_pubkey)) }, + onClick = { + scope.launch(Dispatchers.IO) { + clipboardManager.setText(AnnotatedString("nostr:${note.author?.pubkeyNpub()}")) + onDismiss() + } + }, + ) + DropdownMenuItem( + text = { Text(stringResource(R.string.copy_note_id)) }, + onClick = { + scope.launch(Dispatchers.IO) { + clipboardManager.setText(AnnotatedString("nostr:" + note.toNEvent())) + onDismiss() + } + }, + ) + DropdownMenuItem( + text = { Text(stringResource(R.string.quick_action_share)) }, + onClick = { + val sendIntent = + Intent().apply { + action = Intent.ACTION_SEND + type = "text/plain" + putExtra( + Intent.EXTRA_TEXT, + externalLinkForNote(note), + ) + putExtra( + Intent.EXTRA_TITLE, + actContext.getString(R.string.quick_action_share_browser_link), + ) + } + + val shareIntent = + Intent.createChooser(sendIntent, appContext.getString(R.string.quick_action_share)) + ContextCompat.startActivity(actContext, shareIntent, null) + onDismiss() + }, + ) + HorizontalDivider(thickness = DividerThickness) + if (note.event is TextNoteEvent) { + if (state.isLoggedUser) { + DropdownMenuItem( + text = { Text(stringResource(R.string.edit_post)) }, + onClick = { + wantsToEditPost.value = true + }, + ) + } else { + DropdownMenuItem( + text = { Text(stringResource(R.string.propose_an_edit)) }, + onClick = { + wantsToEditPost.value = true + }, + ) + } + } + DropdownMenuItem( + text = { Text(stringResource(R.string.broadcast)) }, + onClick = { + accountViewModel.broadcast(note) + onDismiss() + }, + ) + HorizontalDivider(thickness = DividerThickness) + if (accountViewModel.account.hasPendingAttestations(note)) { + DropdownMenuItem( + text = { Text(stringResource(R.string.timestamp_pending)) }, + onClick = { + onDismiss() + }, + ) + } else { + DropdownMenuItem( + text = { Text(stringResource(R.string.timestamp_it)) }, + onClick = { + accountViewModel.timestamp(note) + onDismiss() + }, + ) + } + HorizontalDivider(thickness = DividerThickness) + if (state.isPrivateBookmarkNote) { + DropdownMenuItem( + text = { Text(stringResource(R.string.remove_from_private_bookmarks)) }, + onClick = { + accountViewModel.removePrivateBookmark(note) + onDismiss() + }, + ) + } else { + DropdownMenuItem( + text = { Text(stringResource(R.string.add_to_private_bookmarks)) }, + onClick = { + accountViewModel.addPrivateBookmark(note) + onDismiss() + }, + ) + } + if (state.isPublicBookmarkNote) { + DropdownMenuItem( + text = { Text(stringResource(R.string.remove_from_public_bookmarks)) }, + onClick = { + accountViewModel.removePublicBookmark(note) + onDismiss() + }, + ) + } else { + DropdownMenuItem( + text = { Text(stringResource(R.string.add_to_public_bookmarks)) }, + onClick = { + accountViewModel.addPublicBookmark(note) + onDismiss() + }, + ) + } + HorizontalDivider(thickness = DividerThickness) + if (state.showSensitiveContent == null || state.showSensitiveContent == true) { + DropdownMenuItem( + text = { Text(stringResource(R.string.content_warning_hide_all_sensitive_content)) }, + onClick = { + scope.launch(Dispatchers.IO) { + accountViewModel.hideSensitiveContent() + onDismiss() + } + }, + ) + } + if (state.showSensitiveContent == null || state.showSensitiveContent == false) { + DropdownMenuItem( + text = { Text(stringResource(R.string.content_warning_show_all_sensitive_content)) }, + onClick = { + scope.launch(Dispatchers.IO) { + accountViewModel.disableContentWarnings() + onDismiss() + } + }, + ) + } + if (state.showSensitiveContent != null) { + DropdownMenuItem( + text = { Text(stringResource(R.string.content_warning_see_warnings)) }, + onClick = { + scope.launch(Dispatchers.IO) { + accountViewModel.seeContentWarnings() + onDismiss() + } + }, + ) + } + HorizontalDivider(thickness = DividerThickness) + if (state.isLoggedUser) { + DropdownMenuItem( + text = { Text(stringResource(R.string.request_deletion)) }, + onClick = { + scope.launch(Dispatchers.IO) { + accountViewModel.delete(note) + onDismiss() + } + }, + ) + } else { + DropdownMenuItem( + text = { Text(stringResource(R.string.block_report)) }, + onClick = { reportDialogShowing = true }, + ) + } + } + + if (reportDialogShowing) { + ReportNoteDialog(note = note, accountViewModel = accountViewModel) { + reportDialogShowing = false + onDismiss() + } + } +} + +@Composable +fun WatchBookmarksFollowsAndAccount( + note: Note, + accountViewModel: AccountViewModel, + onNew: (DropDownParams) -> Unit, +) { + val followState by accountViewModel.userProfile().live().follows.observeAsState() + val bookmarkState by accountViewModel.userProfile().live().bookmarks.observeAsState() + val showSensitiveContent by + accountViewModel.showSensitiveContentChanges.observeAsState( + accountViewModel.account.showSensitiveContent, + ) + + LaunchedEffect(key1 = followState, key2 = bookmarkState, key3 = showSensitiveContent) { + launch(Dispatchers.IO) { + accountViewModel.isInPrivateBookmarks(note) { + val newState = + DropDownParams( + isFollowingAuthor = accountViewModel.isFollowing(note.author), + isPrivateBookmarkNote = it, + isPublicBookmarkNote = accountViewModel.isInPublicBookmarks(note), + isLoggedUser = accountViewModel.isLoggedUser(note.author), + isSensitive = note.event?.isSensitive() ?: false, + showSensitiveContent = showSensitiveContent, + ) + + launch(Dispatchers.Main) { + onNew( + newState, + ) + } + } + } + } +} diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/note/elements/ForkInfo.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/elements/ForkInfo.kt new file mode 100644 index 000000000..62ccc2ce3 --- /dev/null +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/elements/ForkInfo.kt @@ -0,0 +1,155 @@ +/** + * 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.elements + +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.text.ClickableText +import androidx.compose.material3.LocalTextStyle +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.livedata.observeAsState +import androidx.compose.runtime.remember +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.buildAnnotatedString +import androidx.compose.ui.text.style.TextOverflow +import com.vitorpamplona.amethyst.R +import com.vitorpamplona.amethyst.model.Note +import com.vitorpamplona.amethyst.ui.components.CreateClickableTextWithEmoji +import com.vitorpamplona.amethyst.ui.components.LoadNote +import com.vitorpamplona.amethyst.ui.navigation.routeFor +import com.vitorpamplona.amethyst.ui.note.LoadAddressableNote +import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel +import com.vitorpamplona.amethyst.ui.theme.Font14SP +import com.vitorpamplona.amethyst.ui.theme.StdHorzSpacer +import com.vitorpamplona.amethyst.ui.theme.nip05 +import com.vitorpamplona.quartz.events.BaseTextNoteEvent +import com.vitorpamplona.quartz.events.toImmutableListOfLists + +@Composable +fun ShowForkInformation( + noteEvent: BaseTextNoteEvent, + modifier: Modifier, + accountViewModel: AccountViewModel, + nav: (String) -> Unit, +) { + val forkedAddress = remember(noteEvent) { noteEvent.forkFromAddress() } + val forkedEvent = remember(noteEvent) { noteEvent.forkFromVersion() } + if (forkedAddress != null) { + LoadAddressableNote( + aTag = forkedAddress, + accountViewModel = accountViewModel, + ) { addressableNote -> + if (addressableNote != null) { + ForkInformationRowLightColor(addressableNote, modifier, accountViewModel, nav) + } + } + } else if (forkedEvent != null) { + LoadNote(forkedEvent, accountViewModel = accountViewModel) { event -> + if (event != null) { + ForkInformationRowLightColor(event, modifier, accountViewModel, nav) + } + } + } +} + +@Composable +fun ForkInformationRowLightColor( + originalVersion: Note, + modifier: Modifier = Modifier, + accountViewModel: AccountViewModel, + nav: (String) -> Unit, +) { + val noteState by originalVersion.live().metadata.observeAsState() + val note = noteState?.note ?: return + val author = note.author ?: return + val route = remember(note) { routeFor(note, accountViewModel.userProfile()) } + + if (route != null) { + Row(modifier) { + ClickableText( + text = + buildAnnotatedString { + append(stringResource(id = R.string.forked_from)) + append(" ") + }, + onClick = { nav(route) }, + style = + LocalTextStyle.current.copy( + color = MaterialTheme.colorScheme.nip05, + fontSize = Font14SP, + ), + maxLines = 1, + overflow = TextOverflow.Visible, + ) + + val userState by author.live().metadata.observeAsState() + val userDisplayName = remember(userState) { userState?.user?.toBestDisplayName() } + val userTags = + remember(userState) { userState?.user?.info?.latestMetadata?.tags?.toImmutableListOfLists() } + + if (userDisplayName != null) { + CreateClickableTextWithEmoji( + clickablePart = userDisplayName, + maxLines = 1, + route = route, + overrideColor = MaterialTheme.colorScheme.nip05, + fontSize = Font14SP, + nav = nav, + tags = userTags, + ) + } + } + } +} + +@Composable +fun ForkInformationRow( + originalVersion: Note, + modifier: Modifier = Modifier, + accountViewModel: AccountViewModel, + nav: (String) -> Unit, +) { + val noteState by originalVersion.live().metadata.observeAsState() + val note = noteState?.note ?: return + val author = note.author ?: return + val route = remember(note) { routeFor(note, accountViewModel.userProfile()) } + + if (route != null) { + Row(modifier) { + Text(stringResource(id = R.string.forked_from)) + Spacer(modifier = StdHorzSpacer) + + val userMetadata by author.live().userMetadataInfo.observeAsState() + + CreateClickableTextWithEmoji( + clickablePart = userMetadata?.bestDisplayName() ?: userMetadata?.bestUsername() ?: author.pubkeyDisplayHex(), + maxLines = 1, + route = route, + nav = nav, + tags = userMetadata?.tags, + ) + } + } +} diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/note/elements/TimeAgo.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/elements/TimeAgo.kt new file mode 100644 index 000000000..9c48a2393 --- /dev/null +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/elements/TimeAgo.kt @@ -0,0 +1,50 @@ +/** + * 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.elements + +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.ui.platform.LocalContext +import com.vitorpamplona.amethyst.model.Note +import com.vitorpamplona.amethyst.ui.note.timeAgo +import com.vitorpamplona.amethyst.ui.theme.placeholderText + +@Composable +fun TimeAgo(note: Note) { + val time = remember(note) { note.createdAt() } ?: return + TimeAgo(time) +} + +@Composable +fun TimeAgo(time: Long) { + val context = LocalContext.current + val timeStr by remember(time) { mutableStateOf(timeAgo(time, context = context)) } + + Text( + text = timeStr, + color = MaterialTheme.colorScheme.placeholderText, + maxLines = 1, + ) +} diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/note/types/AppDefinition.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/types/AppDefinition.kt new file mode 100644 index 000000000..607354e1b --- /dev/null +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/types/AppDefinition.kt @@ -0,0 +1,245 @@ +/** + * 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.types + +import androidx.compose.foundation.ExperimentalFoundationApi +import androidx.compose.foundation.Image +import androidx.compose.foundation.background +import androidx.compose.foundation.border +import androidx.compose.foundation.combinedClickable +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.foundation.text.ClickableText +import androidx.compose.material3.LocalTextStyle +import androidx.compose.material3.MaterialTheme +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.layout.ContentScale +import androidx.compose.ui.platform.LocalClipboardManager +import androidx.compose.ui.platform.LocalUriHandler +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.AnnotatedString +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import coil.compose.AsyncImage +import com.vitorpamplona.amethyst.R +import com.vitorpamplona.amethyst.commons.RichTextParser +import com.vitorpamplona.amethyst.model.Note +import com.vitorpamplona.amethyst.ui.components.CreateTextWithEmoji +import com.vitorpamplona.amethyst.ui.components.TranslatableRichTextViewer +import com.vitorpamplona.amethyst.ui.components.ZoomableImageDialog +import com.vitorpamplona.amethyst.ui.note.LinkIcon +import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel +import com.vitorpamplona.amethyst.ui.theme.Size16Modifier +import com.vitorpamplona.amethyst.ui.theme.Size35dp +import com.vitorpamplona.amethyst.ui.theme.placeholderText +import com.vitorpamplona.quartz.events.AppDefinitionEvent +import com.vitorpamplona.quartz.events.EmptyTagList +import com.vitorpamplona.quartz.events.UserMetadata +import com.vitorpamplona.quartz.events.toImmutableListOfLists +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch + +@OptIn(ExperimentalFoundationApi::class) +@Composable +fun RenderAppDefinition( + note: Note, + accountViewModel: AccountViewModel, + nav: (String) -> Unit, +) { + val noteEvent = note.event as? AppDefinitionEvent ?: return + + var metadata by remember { mutableStateOf(null) } + + LaunchedEffect(key1 = noteEvent) { + launch(Dispatchers.Default) { metadata = noteEvent.appMetaData() } + } + + metadata?.let { + Box { + val clipboardManager = LocalClipboardManager.current + val uri = LocalUriHandler.current + + if (!it.banner.isNullOrBlank()) { + var zoomImageDialogOpen by remember { mutableStateOf(false) } + + AsyncImage( + model = it.banner, + contentDescription = stringResource(id = R.string.profile_image), + contentScale = ContentScale.FillWidth, + modifier = + Modifier + .fillMaxWidth() + .height(125.dp) + .combinedClickable( + onClick = {}, + onLongClick = { clipboardManager.setText(AnnotatedString(it.banner!!)) }, + ), + ) + + if (zoomImageDialogOpen) { + ZoomableImageDialog( + imageUrl = RichTextParser.parseImageOrVideo(it.banner!!), + onDismiss = { zoomImageDialogOpen = false }, + accountViewModel = accountViewModel, + ) + } + } else { + Image( + painter = painterResource(R.drawable.profile_banner), + contentDescription = stringResource(id = R.string.profile_banner), + contentScale = ContentScale.FillWidth, + modifier = + Modifier + .fillMaxWidth() + .height(125.dp), + ) + } + + Column( + modifier = + Modifier + .fillMaxWidth() + .padding(horizontal = 10.dp) + .padding(top = 75.dp), + ) { + Row( + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.Bottom, + ) { + var zoomImageDialogOpen by remember { mutableStateOf(false) } + + Box(Modifier.size(100.dp)) { + it.picture?.let { + AsyncImage( + model = it, + contentDescription = null, + contentScale = ContentScale.FillWidth, + modifier = + Modifier + .border( + 3.dp, + MaterialTheme.colorScheme.background, + CircleShape, + ) + .clip(shape = CircleShape) + .fillMaxSize() + .background(MaterialTheme.colorScheme.background) + .combinedClickable( + onClick = { zoomImageDialogOpen = true }, + onLongClick = { clipboardManager.setText(AnnotatedString(it)) }, + ), + ) + } + } + + if (zoomImageDialogOpen) { + ZoomableImageDialog( + imageUrl = RichTextParser.parseImageOrVideo(it.banner!!), + onDismiss = { zoomImageDialogOpen = false }, + accountViewModel = accountViewModel, + ) + } + + Spacer(Modifier.weight(1f)) + + Row( + modifier = + Modifier + .height(Size35dp) + .padding(bottom = 3.dp), + ) {} + } + + val name = remember(it) { it.anyName() } + name?.let { + Row( + verticalAlignment = Alignment.Bottom, + modifier = Modifier.padding(top = 7.dp), + ) { + CreateTextWithEmoji( + text = it, + tags = + remember { + (note.event?.tags() ?: emptyArray()).toImmutableListOfLists() + }, + fontWeight = FontWeight.Bold, + fontSize = 25.sp, + ) + } + } + + val website = remember(it) { it.website } + if (!website.isNullOrEmpty()) { + Row(verticalAlignment = Alignment.CenterVertically) { + LinkIcon(Size16Modifier, MaterialTheme.colorScheme.placeholderText) + + ClickableText( + text = AnnotatedString(website.removePrefix("https://")), + onClick = { website.let { runCatching { uri.openUri(it) } } }, + style = LocalTextStyle.current.copy(color = MaterialTheme.colorScheme.primary), + modifier = Modifier.padding(top = 1.dp, bottom = 1.dp, start = 5.dp), + ) + } + } + + it.about?.let { + Row( + modifier = Modifier.padding(top = 5.dp, bottom = 5.dp), + ) { + val tags = + remember(note) { + note.event?.tags()?.toImmutableListOfLists() ?: EmptyTagList + } + val bgColor = MaterialTheme.colorScheme.background + val backgroundColor = remember { mutableStateOf(bgColor) } + TranslatableRichTextViewer( + content = it, + canPreview = false, + tags = tags, + backgroundColor = backgroundColor, + accountViewModel = accountViewModel, + nav = nav, + ) + } + } + } + } + } +} diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/note/types/AudioTrack.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/types/AudioTrack.kt new file mode 100644 index 000000000..3a70d2d6b --- /dev/null +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/types/AudioTrack.kt @@ -0,0 +1,235 @@ +/** + * 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.types + +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.width +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.unit.dp +import com.vitorpamplona.amethyst.model.Note +import com.vitorpamplona.amethyst.model.User +import com.vitorpamplona.amethyst.ui.components.LoadThumbAndThenVideoView +import com.vitorpamplona.amethyst.ui.components.TranslatableRichTextViewer +import com.vitorpamplona.amethyst.ui.components.VideoView +import com.vitorpamplona.amethyst.ui.note.ClickableUserPicture +import com.vitorpamplona.amethyst.ui.note.UsernameDisplay +import com.vitorpamplona.amethyst.ui.note.elements.DisplayUncitedHashtags +import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel +import com.vitorpamplona.amethyst.ui.theme.placeholderText +import com.vitorpamplona.quartz.events.AudioHeaderEvent +import com.vitorpamplona.quartz.events.AudioTrackEvent +import com.vitorpamplona.quartz.events.Participant +import com.vitorpamplona.quartz.events.toImmutableListOfLists +import kotlinx.collections.immutable.toImmutableList +import java.util.Locale + +@Composable +fun RenderAudioTrack( + note: Note, + accountViewModel: AccountViewModel, + nav: (String) -> Unit, +) { + val noteEvent = note.event as? AudioTrackEvent ?: return + + AudioTrackHeader(noteEvent, note, accountViewModel, nav) +} + +@Composable +fun AudioTrackHeader( + noteEvent: AudioTrackEvent, + note: Note, + accountViewModel: AccountViewModel, + nav: (String) -> Unit, +) { + val media = remember { noteEvent.media() } + val cover = remember { noteEvent.cover() } + val subject = remember { noteEvent.subject() } + val content = remember { noteEvent.content() } + val participants = remember { noteEvent.participants() } + + var participantUsers by remember { mutableStateOf>>(emptyList()) } + + LaunchedEffect(key1 = participants) { + accountViewModel.loadParticipants(participants) { participantUsers = it } + } + + Row(modifier = Modifier.padding(top = 5.dp)) { + Column( + modifier = Modifier.fillMaxWidth(), + horizontalAlignment = Alignment.CenterHorizontally, + ) { + Row { + subject?.let { + Row( + verticalAlignment = Alignment.CenterVertically, + modifier = Modifier.padding(top = 5.dp, bottom = 5.dp), + ) { + Text( + text = it, + fontWeight = FontWeight.Bold, + maxLines = 3, + overflow = TextOverflow.Ellipsis, + modifier = Modifier.fillMaxWidth(), + ) + } + } + } + + participantUsers.forEach { + Row( + verticalAlignment = Alignment.CenterVertically, + modifier = + Modifier + .padding(top = 5.dp, start = 10.dp, end = 10.dp) + .clickable { + nav("User/${it.second.pubkeyHex}") + }, + ) { + ClickableUserPicture(it.second, 25.dp, accountViewModel) + Spacer(Modifier.width(5.dp)) + UsernameDisplay(it.second, Modifier.weight(1f)) + Spacer(Modifier.width(5.dp)) + it.first.role?.let { + Text( + text = it.capitalize(Locale.ROOT), + color = MaterialTheme.colorScheme.placeholderText, + maxLines = 1, + ) + } + } + } + + media?.let { media -> + Row( + verticalAlignment = Alignment.CenterVertically, + ) { + cover?.let { cover -> + LoadThumbAndThenVideoView( + videoUri = media, + title = noteEvent.subject(), + thumbUri = cover, + authorName = note.author?.toBestDisplayName(), + roundedCorner = true, + nostrUriCallback = "nostr:${note.toNEvent()}", + accountViewModel = accountViewModel, + ) + } + ?: VideoView( + videoUri = media, + title = noteEvent.subject(), + authorName = note.author?.toBestDisplayName(), + roundedCorner = true, + accountViewModel = accountViewModel, + ) + } + } + } + } +} + +@Composable +fun RenderAudioHeader( + note: Note, + accountViewModel: AccountViewModel, + nav: (String) -> Unit, +) { + val noteEvent = note.event as? AudioHeaderEvent ?: return + + AudioHeader(noteEvent, note, accountViewModel, nav) +} + +@Composable +fun AudioHeader( + noteEvent: AudioHeaderEvent, + note: Note, + accountViewModel: AccountViewModel, + nav: (String) -> Unit, +) { + val media = remember { noteEvent.stream() ?: noteEvent.download() } + val waveform = remember { noteEvent.wavefrom()?.toImmutableList()?.ifEmpty { null } } + val content = remember { noteEvent.content().ifBlank { null } } + + val defaultBackground = MaterialTheme.colorScheme.background + val background = remember { mutableStateOf(defaultBackground) } + val tags = remember(noteEvent) { noteEvent.tags().toImmutableListOfLists() } + + Row(modifier = Modifier.padding(top = 5.dp)) { + Column(modifier = Modifier.fillMaxWidth(), horizontalAlignment = Alignment.CenterHorizontally) { + media?.let { media -> + Row( + verticalAlignment = Alignment.CenterVertically, + ) { + VideoView( + videoUri = media, + waveform = waveform, + title = noteEvent.subject(), + authorName = note.author?.toBestDisplayName(), + roundedCorner = true, + accountViewModel = accountViewModel, + nostrUriCallback = note.toNostrUri(), + ) + } + } + + content?.let { + Row( + verticalAlignment = Alignment.CenterVertically, + modifier = + Modifier + .fillMaxWidth() + .padding(top = 5.dp), + ) { + TranslatableRichTextViewer( + content = it, + canPreview = true, + tags = tags, + backgroundColor = background, + accountViewModel = accountViewModel, + nav = nav, + ) + } + } + + if (noteEvent.hasHashtags()) { + Row(Modifier.fillMaxWidth(), verticalAlignment = Alignment.CenterVertically) { + val hashtags = remember(noteEvent) { noteEvent.hashtags().toImmutableList() } + DisplayUncitedHashtags(hashtags, content ?: "", nav) + } + } + } + } +} diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/note/types/Badge.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/types/Badge.kt new file mode 100644 index 000000000..f6deaa3fe --- /dev/null +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/types/Badge.kt @@ -0,0 +1,233 @@ +/** + * 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.types + +import android.graphics.Bitmap +import androidx.compose.foundation.background +import androidx.compose.foundation.border +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.ExperimentalLayoutApi +import androidx.compose.foundation.layout.FlowRow +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.shape.CutCornerShape +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +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.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.luminance +import androidx.compose.ui.layout.ContentScale +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.unit.dp +import androidx.core.graphics.drawable.toBitmap +import androidx.core.graphics.get +import coil.compose.AsyncImage +import coil.compose.AsyncImagePainter +import coil.request.SuccessResult +import com.vitorpamplona.amethyst.R +import com.vitorpamplona.amethyst.model.Note +import com.vitorpamplona.amethyst.model.User +import com.vitorpamplona.amethyst.ui.note.ClickableUserPicture +import com.vitorpamplona.amethyst.ui.note.NoteCompose +import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel +import com.vitorpamplona.amethyst.ui.theme.Size35dp +import com.vitorpamplona.amethyst.ui.theme.mediumImportanceLink +import com.vitorpamplona.quartz.events.BadgeAwardEvent +import com.vitorpamplona.quartz.events.BadgeDefinitionEvent +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch + +@Composable +fun BadgeDisplay(baseNote: Note) { + val background = MaterialTheme.colorScheme.background + val badgeData = baseNote.event as? BadgeDefinitionEvent ?: return + + val image = remember { badgeData.thumb()?.ifBlank { null } ?: badgeData.image() } + val name = remember { badgeData.name() } + val description = remember { badgeData.description() } + + var backgroundFromImage by remember { mutableStateOf(Pair(background, background)) } + var imageResult by remember { mutableStateOf(null) } + + LaunchedEffect(key1 = imageResult) { + launch(Dispatchers.IO) { + imageResult?.let { + val backgroundColor = + it.drawable.toBitmap(200, 200).copy(Bitmap.Config.ARGB_8888, false).get(0, 199) + val colorFromImage = Color(backgroundColor) + val textBackground = + if (colorFromImage.luminance() > 0.5) { + lightColorScheme().onBackground + } else { + darkColorScheme().onBackground + } + + launch(Dispatchers.Main) { backgroundFromImage = Pair(colorFromImage, textBackground) } + } + } + } + + Row( + modifier = + Modifier + .padding(10.dp) + .clip(shape = CutCornerShape(20, 20, 20, 20)) + .border( + 5.dp, + MaterialTheme.colorScheme.mediumImportanceLink, + CutCornerShape(20), + ) + .background(backgroundFromImage.first), + ) { + RenderBadge( + image, + name, + backgroundFromImage.second, + description, + ) { + if (imageResult == null) { + imageResult = it.result + } + } + } +} + +@Composable +private fun RenderBadge( + image: String?, + name: String?, + backgroundFromImage: Color, + description: String?, + onSuccess: (AsyncImagePainter.State.Success) -> Unit, +) { + Column { + image.let { + AsyncImage( + model = it, + contentDescription = + stringResource( + R.string.badge_award_image_for, + name ?: "", + ), + contentScale = ContentScale.FillWidth, + modifier = Modifier.fillMaxWidth(), + onSuccess = onSuccess, + ) + } + + name?.let { + Text( + text = it, + style = MaterialTheme.typography.bodyLarge, + textAlign = TextAlign.Center, + modifier = + Modifier + .fillMaxWidth() + .padding(start = 10.dp, end = 10.dp), + color = backgroundFromImage, + ) + } + + description?.let { + Text( + text = it, + style = MaterialTheme.typography.bodySmall, + textAlign = TextAlign.Center, + modifier = + Modifier + .fillMaxWidth() + .padding(start = 10.dp, end = 10.dp, bottom = 10.dp), + color = Color.Gray, + maxLines = 3, + overflow = TextOverflow.Ellipsis, + ) + } + } +} + +@OptIn(ExperimentalLayoutApi::class) +@Composable +fun RenderBadgeAward( + note: Note, + backgroundColor: MutableState, + accountViewModel: AccountViewModel, + nav: (String) -> Unit, +) { + if (note.replyTo.isNullOrEmpty()) return + + val noteEvent = note.event as? BadgeAwardEvent ?: return + var awardees by remember { mutableStateOf>(listOf()) } + + Text(text = stringResource(R.string.award_granted_to)) + + LaunchedEffect(key1 = note) { accountViewModel.loadUsers(noteEvent.awardees()) { awardees = it } } + + FlowRow(modifier = Modifier.padding(top = 5.dp)) { + awardees.take(100).forEach { user -> + Row( + modifier = + Modifier + .size(size = Size35dp) + .clickable { nav("User/${user.pubkeyHex}") }, + verticalAlignment = Alignment.CenterVertically, + ) { + ClickableUserPicture( + baseUser = user, + accountViewModel = accountViewModel, + size = Size35dp, + ) + } + } + + if (awardees.size > 100) { + Text(" and ${awardees.size - 100} others", maxLines = 1) + } + } + + note.replyTo?.firstOrNull()?.let { + NoteCompose( + it, + modifier = Modifier, + isBoostedNote = false, + isQuotedNote = true, + unPackReply = false, + parentBackgroundColor = backgroundColor, + accountViewModel = accountViewModel, + nav = nav, + ) + } +} diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/note/types/Classifieds.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/types/Classifieds.kt new file mode 100644 index 000000000..6d586ca52 --- /dev/null +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/types/Classifieds.kt @@ -0,0 +1,161 @@ +/** + * 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.types + +import androidx.compose.foundation.border +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.layout.ContentScale +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.unit.dp +import coil.compose.AsyncImage +import com.vitorpamplona.amethyst.R +import com.vitorpamplona.amethyst.model.Note +import com.vitorpamplona.amethyst.ui.note.elements.DefaultImageHeader +import com.vitorpamplona.amethyst.ui.note.showAmount +import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel +import com.vitorpamplona.amethyst.ui.theme.DoubleVertSpacer +import com.vitorpamplona.amethyst.ui.theme.QuoteBorder +import com.vitorpamplona.amethyst.ui.theme.SmallBorder +import com.vitorpamplona.amethyst.ui.theme.subtleBorder +import com.vitorpamplona.quartz.events.ClassifiedsEvent + +@Composable +fun RenderClassifieds( + noteEvent: ClassifiedsEvent, + note: Note, + accountViewModel: AccountViewModel, + nav: (String) -> Unit, +) { + val image = remember(noteEvent) { noteEvent.image() } + val title = remember(noteEvent) { noteEvent.title() } + val summary = + remember(noteEvent) { noteEvent.summary() ?: noteEvent.content.take(200).ifBlank { null } } + val price = remember(noteEvent) { noteEvent.price() } + val location = remember(noteEvent) { noteEvent.location() } + + Row( + modifier = + Modifier + .clip(shape = QuoteBorder) + .border( + 1.dp, + MaterialTheme.colorScheme.subtleBorder, + QuoteBorder, + ), + ) { + Column { + Row { + image?.let { + AsyncImage( + model = it, + contentDescription = + stringResource( + R.string.preview_card_image_for, + it, + ), + contentScale = ContentScale.FillWidth, + modifier = Modifier.fillMaxWidth(), + ) + } + ?: DefaultImageHeader(note, accountViewModel) + } + + Row( + Modifier.padding(start = 10.dp, end = 10.dp, top = 10.dp), + verticalAlignment = Alignment.CenterVertically, + ) { + title?.let { + Text( + text = it, + style = MaterialTheme.typography.bodyLarge, + maxLines = 1, + overflow = TextOverflow.Ellipsis, + modifier = Modifier.weight(1f), + ) + } + + price?.let { + val priceTag = + remember(noteEvent) { + val newAmount = + price.amount.toBigDecimalOrNull()?.let { showAmount(it) } + ?: price.amount + + if (price.frequency != null && price.currency != null) { + "$newAmount ${price.currency}/${price.frequency}" + } else if (price.currency != null) { + "$newAmount ${price.currency}" + } else { + newAmount + } + } + + Text( + text = priceTag, + maxLines = 1, + color = MaterialTheme.colorScheme.primary, + fontWeight = FontWeight.Bold, + modifier = + remember { + Modifier + .clip(SmallBorder) + .padding(start = 5.dp) + }, + ) + } + } + + if (summary != null || location != null) { + Row( + Modifier.padding(start = 10.dp, end = 10.dp, top = 5.dp), + verticalAlignment = Alignment.CenterVertically, + ) { + summary?.let { + Text( + text = it, + style = MaterialTheme.typography.bodySmall, + modifier = Modifier.weight(1f), + color = Color.Gray, + maxLines = 3, + overflow = TextOverflow.Ellipsis, + ) + } + } + } + + Spacer(modifier = DoubleVertSpacer) + } + } +} diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/note/types/CommunityHeader.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/types/CommunityHeader.kt new file mode 100644 index 000000000..3dcf3a8cb --- /dev/null +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/types/CommunityHeader.kt @@ -0,0 +1,393 @@ +/** + * 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.types + +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.verticalScroll +import androidx.compose.material3.HorizontalDivider +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue +import androidx.compose.runtime.livedata.observeAsState +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.layout.ContentScale +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.unit.dp +import androidx.lifecycle.distinctUntilChanged +import androidx.lifecycle.map +import com.vitorpamplona.amethyst.R +import com.vitorpamplona.amethyst.model.AddressableNote +import com.vitorpamplona.amethyst.model.User +import com.vitorpamplona.amethyst.ui.components.RobohashFallbackAsyncImage +import com.vitorpamplona.amethyst.ui.components.TranslatableRichTextViewer +import com.vitorpamplona.amethyst.ui.navigation.routeFor +import com.vitorpamplona.amethyst.ui.note.ClickableUserPicture +import com.vitorpamplona.amethyst.ui.note.LikeReaction +import com.vitorpamplona.amethyst.ui.note.NoteAuthorPicture +import com.vitorpamplona.amethyst.ui.note.NoteUsernameDisplay +import com.vitorpamplona.amethyst.ui.note.UsernameDisplay +import com.vitorpamplona.amethyst.ui.note.ZapReaction +import com.vitorpamplona.amethyst.ui.note.elements.DisplayUncitedHashtags +import com.vitorpamplona.amethyst.ui.note.elements.MoreOptionsButton +import com.vitorpamplona.amethyst.ui.screen.equalImmutableLists +import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel +import com.vitorpamplona.amethyst.ui.screen.loggedIn.JoinCommunityButton +import com.vitorpamplona.amethyst.ui.screen.loggedIn.LeaveCommunityButton +import com.vitorpamplona.amethyst.ui.screen.loggedIn.NormalTimeAgo +import com.vitorpamplona.amethyst.ui.theme.DividerThickness +import com.vitorpamplona.amethyst.ui.theme.DoubleHorzSpacer +import com.vitorpamplona.amethyst.ui.theme.HeaderPictureModifier +import com.vitorpamplona.amethyst.ui.theme.Size10dp +import com.vitorpamplona.amethyst.ui.theme.Size25dp +import com.vitorpamplona.amethyst.ui.theme.Size35dp +import com.vitorpamplona.amethyst.ui.theme.Size5dp +import com.vitorpamplona.amethyst.ui.theme.StdHorzSpacer +import com.vitorpamplona.amethyst.ui.theme.StdPadding +import com.vitorpamplona.quartz.events.CommunityDefinitionEvent +import com.vitorpamplona.quartz.events.EmptyTagList +import com.vitorpamplona.quartz.events.Participant +import kotlinx.collections.immutable.ImmutableList +import kotlinx.collections.immutable.persistentListOf +import kotlinx.collections.immutable.toImmutableList +import java.util.Locale + +@Composable +fun CommunityHeader( + baseNote: AddressableNote, + showBottomDiviser: Boolean, + sendToCommunity: Boolean, + modifier: Modifier = StdPadding, + accountViewModel: AccountViewModel, + nav: (String) -> Unit, +) { + val expanded = remember { mutableStateOf(false) } + + Column(Modifier.fillMaxWidth()) { + Column( + verticalArrangement = Arrangement.Center, + modifier = + Modifier.clickable { + if (sendToCommunity) { + routeFor(baseNote, accountViewModel.userProfile())?.let { nav(it) } + } else { + expanded.value = !expanded.value + } + }, + ) { + ShortCommunityHeader( + baseNote = baseNote, + accountViewModel = accountViewModel, + nav = nav, + ) + + if (expanded.value) { + Column(Modifier.verticalScroll(rememberScrollState())) { + LongCommunityHeader( + baseNote = baseNote, + lineModifier = modifier, + accountViewModel = accountViewModel, + nav = nav, + ) + } + } + } + + if (showBottomDiviser) { + HorizontalDivider( + thickness = DividerThickness, + ) + } + } +} + +@Composable +fun LongCommunityHeader( + baseNote: AddressableNote, + lineModifier: Modifier = Modifier.padding(horizontal = Size10dp, vertical = Size5dp), + accountViewModel: AccountViewModel, + nav: (String) -> Unit, +) { + val noteState by baseNote.live().metadata.observeAsState() + val noteEvent = + remember(noteState) { noteState?.note?.event as? CommunityDefinitionEvent } ?: return + + Row( + lineModifier, + ) { + val rulesLabel = stringResource(id = R.string.rules) + val summary = + remember(noteState) { + val subject = noteEvent.subject()?.ifEmpty { null } + val body = noteEvent.description()?.ifBlank { null } + val rules = noteEvent.rules()?.ifBlank { null } + + if (!subject.isNullOrBlank() && body?.split("\n")?.get(0)?.contains(subject) == false) { + if (rules == null) { + "### $subject\n$body" + } else { + "### $subject\n$body\n\n### $rulesLabel\n\n$rules" + } + } else { + if (rules == null) { + body + } else { + "$body\n\n$rulesLabel\n$rules" + } + } + } + + Column( + Modifier.weight(1f), + ) { + Row(verticalAlignment = Alignment.CenterVertically) { + val defaultBackground = MaterialTheme.colorScheme.background + val background = remember { mutableStateOf(defaultBackground) } + + TranslatableRichTextViewer( + content = summary ?: stringResource(id = R.string.community_no_descriptor), + canPreview = false, + tags = EmptyTagList, + backgroundColor = background, + accountViewModel = accountViewModel, + nav = nav, + ) + } + + if (summary != null && noteEvent.hasHashtags()) { + DisplayUncitedHashtags( + remember(noteEvent) { noteEvent.hashtags().toImmutableList() }, + summary ?: "", + nav, + ) + } + } + + Column { + Row { + Spacer(DoubleHorzSpacer) + LongCommunityActionOptions(baseNote, accountViewModel, nav) + } + } + } + + Row( + lineModifier, + verticalAlignment = Alignment.CenterVertically, + ) { + Text( + text = stringResource(id = R.string.owner), + maxLines = 1, + overflow = TextOverflow.Ellipsis, + modifier = Modifier.width(75.dp), + ) + Spacer(DoubleHorzSpacer) + NoteAuthorPicture(baseNote, nav, accountViewModel, Size25dp) + Spacer(DoubleHorzSpacer) + NoteUsernameDisplay(baseNote, remember { Modifier.weight(1f) }) + } + + var participantUsers by + remember(baseNote) { + mutableStateOf>>( + persistentListOf(), + ) + } + + LaunchedEffect(key1 = noteState) { + val participants = (noteState?.note?.event as? CommunityDefinitionEvent)?.moderators() + + if (participants != null) { + accountViewModel.loadParticipants(participants) { newParticipantUsers -> + if ( + newParticipantUsers != null && !equalImmutableLists(newParticipantUsers, participantUsers) + ) { + participantUsers = newParticipantUsers + } + } + } + } + + participantUsers.forEach { + Row( + lineModifier.clickable { nav("User/${it.second.pubkeyHex}") }, + verticalAlignment = Alignment.CenterVertically, + ) { + it.first.role?.let { it1 -> + Text( + text = it1.capitalize(Locale.ROOT), + maxLines = 1, + overflow = TextOverflow.Ellipsis, + modifier = Modifier.width(75.dp), + ) + } + Spacer(DoubleHorzSpacer) + ClickableUserPicture(it.second, Size25dp, accountViewModel) + Spacer(DoubleHorzSpacer) + UsernameDisplay(it.second, remember { Modifier.weight(1f) }) + } + } + + Row( + lineModifier, + verticalAlignment = Alignment.CenterVertically, + ) { + Text( + text = stringResource(id = R.string.created_at), + maxLines = 1, + overflow = TextOverflow.Ellipsis, + modifier = Modifier.width(75.dp), + ) + Spacer(DoubleHorzSpacer) + NormalTimeAgo(baseNote = baseNote, Modifier.weight(1f)) + MoreOptionsButton(baseNote, null, accountViewModel, nav) + } +} + +@Composable +fun ShortCommunityHeader( + baseNote: AddressableNote, + accountViewModel: AccountViewModel, + nav: (String) -> Unit, +) { + val noteState by baseNote.live().metadata.observeAsState() + val noteEvent = + remember(noteState) { noteState?.note?.event as? CommunityDefinitionEvent } ?: return + + val automaticallyShowProfilePicture = + remember { + accountViewModel.settings.showProfilePictures.value + } + + Row(verticalAlignment = Alignment.CenterVertically) { + noteEvent.image()?.let { + RobohashFallbackAsyncImage( + robot = baseNote.idHex, + model = it, + contentDescription = stringResource(R.string.profile_image), + contentScale = ContentScale.Crop, + modifier = HeaderPictureModifier, + loadProfilePicture = automaticallyShowProfilePicture, + ) + } + + Column( + modifier = + Modifier + .padding(start = 10.dp) + .height(Size35dp) + .weight(1f), + verticalArrangement = Arrangement.Center, + ) { + Row(verticalAlignment = Alignment.CenterVertically) { + Text( + text = remember(noteState) { noteEvent.dTag() }, + maxLines = 1, + overflow = TextOverflow.Ellipsis, + ) + } + } + + Row( + modifier = + Modifier + .height(Size35dp) + .padding(start = 5.dp), + verticalAlignment = Alignment.CenterVertically, + ) { + ShortCommunityActionOptions(baseNote, accountViewModel, nav) + } + } +} + +@Composable +private fun ShortCommunityActionOptions( + note: AddressableNote, + accountViewModel: AccountViewModel, + nav: (String) -> Unit, +) { + Spacer(modifier = StdHorzSpacer) + LikeReaction( + baseNote = note, + grayTint = MaterialTheme.colorScheme.onSurface, + accountViewModel = accountViewModel, + nav = nav, + ) + Spacer(modifier = StdHorzSpacer) + ZapReaction( + baseNote = note, + grayTint = MaterialTheme.colorScheme.onSurface, + accountViewModel = accountViewModel, + nav = nav, + ) + + WatchAddressableNoteFollows(note, accountViewModel) { isFollowing -> + if (!isFollowing) { + Spacer(modifier = StdHorzSpacer) + JoinCommunityButton(accountViewModel, note, nav) + } + } +} + +@Composable +private fun LongCommunityActionOptions( + note: AddressableNote, + accountViewModel: AccountViewModel, + nav: (String) -> Unit, +) { + WatchAddressableNoteFollows(note, accountViewModel) { isFollowing -> + if (isFollowing) { + LeaveCommunityButton(accountViewModel, note, nav) + } + } +} + +@Composable +fun WatchAddressableNoteFollows( + note: AddressableNote, + accountViewModel: AccountViewModel, + onFollowChanges: @Composable (Boolean) -> Unit, +) { + val showFollowingMark by + remember { + accountViewModel.userFollows + .map { it.user.latestContactList?.isTaggedAddressableNote(note.idHex) ?: false } + .distinctUntilChanged() + } + .observeAsState(false) + + onFollowChanges(showFollowingMark) +} diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/note/types/Emoji.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/types/Emoji.kt new file mode 100644 index 000000000..2e767e945 --- /dev/null +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/types/Emoji.kt @@ -0,0 +1,309 @@ +/** + * 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.types + +import androidx.compose.animation.Crossfade +import androidx.compose.foundation.background +import androidx.compose.foundation.border +import androidx.compose.foundation.layout.Arrangement +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.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.material3.IconButton +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.MutableState +import androidx.compose.runtime.getValue +import androidx.compose.runtime.livedata.observeAsState +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.layout.ContentScale +import androidx.compose.ui.res.stringResource +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.unit.dp +import androidx.lifecycle.distinctUntilChanged +import androidx.lifecycle.map +import coil.compose.AsyncImage +import com.vitorpamplona.amethyst.R +import com.vitorpamplona.amethyst.model.Note +import com.vitorpamplona.amethyst.ui.components.ShowMoreButton +import com.vitorpamplona.amethyst.ui.note.LoadAddressableNote +import com.vitorpamplona.amethyst.ui.note.elements.AddButton +import com.vitorpamplona.amethyst.ui.note.elements.DefaultImageHeader +import com.vitorpamplona.amethyst.ui.note.elements.RemoveButton +import com.vitorpamplona.amethyst.ui.note.getGradient +import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel +import com.vitorpamplona.amethyst.ui.theme.QuoteBorder +import com.vitorpamplona.amethyst.ui.theme.Size35Modifier +import com.vitorpamplona.amethyst.ui.theme.Size5dp +import com.vitorpamplona.amethyst.ui.theme.StdVertSpacer +import com.vitorpamplona.amethyst.ui.theme.subtleBorder +import com.vitorpamplona.quartz.encoders.ATag +import com.vitorpamplona.quartz.events.EmojiPackEvent +import com.vitorpamplona.quartz.events.EmojiPackSelectionEvent +import com.vitorpamplona.quartz.events.EmojiUrl +import com.vitorpamplona.quartz.events.WikiNoteEvent + +@Composable +fun RenderWikiContent( + note: Note, + accountViewModel: AccountViewModel, + nav: (String) -> Unit, +) { + val noteEvent = note.event as? WikiNoteEvent ?: return + + WikiNoteHeader(noteEvent, note, accountViewModel, nav) +} + +@Composable +private fun WikiNoteHeader( + noteEvent: WikiNoteEvent, + note: Note, + accountViewModel: AccountViewModel, + nav: (String) -> Unit, +) { + val title = remember(noteEvent) { noteEvent.title() } + val summary = + remember(noteEvent) { + noteEvent.summary()?.ifBlank { null } ?: noteEvent.content.take(200).ifBlank { null } + } + val image = remember(noteEvent) { noteEvent.image() } + + Row( + modifier = + Modifier + .padding(top = Size5dp) + .clip(shape = QuoteBorder) + .border( + 1.dp, + MaterialTheme.colorScheme.subtleBorder, + QuoteBorder, + ), + ) { + Column { + val automaticallyShowUrlPreview = + remember { accountViewModel.settings.showUrlPreview.value } + + if (automaticallyShowUrlPreview) { + image?.let { + AsyncImage( + model = it, + contentDescription = + stringResource( + R.string.preview_card_image_for, + it, + ), + contentScale = ContentScale.FillWidth, + modifier = Modifier.fillMaxWidth(), + ) + } + ?: DefaultImageHeader(note, accountViewModel) + } + + title?.let { + Text( + text = it, + style = MaterialTheme.typography.bodyLarge, + modifier = + Modifier + .fillMaxWidth() + .padding(start = 10.dp, end = 10.dp, top = 10.dp), + ) + } + + summary?.let { + Spacer(modifier = StdVertSpacer) + Text( + text = it, + style = MaterialTheme.typography.bodySmall, + modifier = + Modifier + .fillMaxWidth() + .padding(start = 10.dp, end = 10.dp, bottom = 10.dp), + color = Color.Gray, + maxLines = 3, + overflow = TextOverflow.Ellipsis, + ) + } + } + } +} + +@Composable +public fun RenderEmojiPack( + baseNote: Note, + actionable: Boolean, + backgroundColor: MutableState, + accountViewModel: AccountViewModel, + onClick: ((EmojiUrl) -> Unit)? = null, +) { + val noteEvent by + baseNote + .live() + .metadata + .map { it.note.event } + .distinctUntilChanged() + .observeAsState(baseNote.event) + + if (noteEvent == null || noteEvent !is EmojiPackEvent) return + + (noteEvent as? EmojiPackEvent)?.let { + RenderEmojiPack( + noteEvent = it, + baseNote = baseNote, + actionable = actionable, + backgroundColor = backgroundColor, + accountViewModel = accountViewModel, + onClick = onClick, + ) + } +} + +@OptIn(ExperimentalLayoutApi::class) +@Composable +public fun RenderEmojiPack( + noteEvent: EmojiPackEvent, + baseNote: Note, + actionable: Boolean, + backgroundColor: MutableState, + accountViewModel: AccountViewModel, + onClick: ((EmojiUrl) -> Unit)? = null, +) { + var expanded by remember { mutableStateOf(false) } + + val allEmojis = remember(noteEvent) { noteEvent.taggedEmojis() } + + val emojisToShow = + if (expanded) { + allEmojis + } else { + allEmojis.take(60) + } + + Row(verticalAlignment = Alignment.CenterVertically) { + Text( + text = remember(noteEvent) { "#${noteEvent.dTag()}" }, + fontWeight = FontWeight.Bold, + maxLines = 1, + overflow = TextOverflow.Ellipsis, + modifier = + Modifier + .weight(1F) + .padding(5.dp), + textAlign = TextAlign.Center, + ) + + if (actionable) { + EmojiListOptions(accountViewModel, baseNote) + } + } + + Box(Modifier.fillMaxWidth(), contentAlignment = Alignment.TopCenter) { + FlowRow(modifier = Modifier.padding(top = 5.dp)) { + emojisToShow.forEach { emoji -> + if (onClick != null) { + IconButton(onClick = { onClick(emoji) }, modifier = Size35Modifier) { + AsyncImage( + model = emoji.url, + contentDescription = null, + modifier = Size35Modifier, + ) + } + } else { + Box( + modifier = Size35Modifier, + contentAlignment = Alignment.Center, + ) { + AsyncImage( + model = emoji.url, + contentDescription = null, + modifier = Size35Modifier, + ) + } + } + } + } + + if (allEmojis.size > 60 && !expanded) { + Row( + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.Center, + modifier = + Modifier + .align(Alignment.BottomCenter) + .fillMaxWidth() + .background(getGradient(backgroundColor)), + ) { + ShowMoreButton { expanded = !expanded } + } + } + } +} + +@Composable +private fun EmojiListOptions( + accountViewModel: AccountViewModel, + emojiPackNote: Note, +) { + LoadAddressableNote( + aTag = + ATag( + EmojiPackSelectionEvent.KIND, + accountViewModel.userProfile().pubkeyHex, + "", + null, + ), + accountViewModel, + ) { + it?.let { usersEmojiList -> + val hasAddedThis by + remember { + usersEmojiList + .live() + .metadata + .map { usersEmojiList.event?.isTaggedAddressableNote(emojiPackNote.idHex) } + .distinctUntilChanged() + } + .observeAsState() + + Crossfade(targetState = hasAddedThis, label = "EmojiListOptions") { + if (it != true) { + AddButton { accountViewModel.addEmojiPack(usersEmojiList, emojiPackNote) } + } else { + RemoveButton { accountViewModel.removeEmojiPack(usersEmojiList, emojiPackNote) } + } + } + } + } +} diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/note/types/FileHeader.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/types/FileHeader.kt new file mode 100644 index 000000000..cda8eda50 --- /dev/null +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/types/FileHeader.kt @@ -0,0 +1,85 @@ +/** + * 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.types + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import com.vitorpamplona.amethyst.commons.BaseMediaContent +import com.vitorpamplona.amethyst.commons.MediaUrlImage +import com.vitorpamplona.amethyst.commons.MediaUrlVideo +import com.vitorpamplona.amethyst.commons.RichTextParser +import com.vitorpamplona.amethyst.model.Note +import com.vitorpamplona.amethyst.ui.components.SensitivityWarning +import com.vitorpamplona.amethyst.ui.components.ZoomableContentView +import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel +import com.vitorpamplona.quartz.events.FileHeaderEvent + +@Composable +fun FileHeaderDisplay( + note: Note, + roundedCorner: Boolean, + accountViewModel: AccountViewModel, +) { + val event = (note.event as? FileHeaderEvent) ?: return + val fullUrl = event.url() ?: return + + val content by + remember(note) { + val blurHash = event.blurhash() + val hash = event.hash() + val dimensions = event.dimensions() + val description = event.content.ifEmpty { null } ?: event.alt() + val isImage = RichTextParser.isImageUrl(fullUrl) + val uri = note.toNostrUri() + + mutableStateOf( + if (isImage) { + MediaUrlImage( + url = fullUrl, + description = description, + hash = hash, + blurhash = blurHash, + dim = dimensions, + uri = uri, + ) + } else { + MediaUrlVideo( + url = fullUrl, + description = description, + hash = hash, + dim = dimensions, + uri = uri, + authorName = note.author?.toBestDisplayName(), + ) + }, + ) + } + + SensitivityWarning(note = note, accountViewModel = accountViewModel) { + ZoomableContentView( + content = content, + roundedCorner = roundedCorner, + accountViewModel = accountViewModel, + ) + } +} diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/note/types/FileStorage.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/types/FileStorage.kt new file mode 100644 index 000000000..d28d51d0f --- /dev/null +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/types/FileStorage.kt @@ -0,0 +1,118 @@ +/** + * 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.types + +import androidx.compose.animation.Crossfade +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.livedata.observeAsState +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.ui.platform.LocalContext +import com.vitorpamplona.amethyst.commons.BaseMediaContent +import com.vitorpamplona.amethyst.commons.MediaLocalImage +import com.vitorpamplona.amethyst.commons.MediaLocalVideo +import com.vitorpamplona.amethyst.model.Note +import com.vitorpamplona.amethyst.ui.components.LoadNote +import com.vitorpamplona.amethyst.ui.components.SensitivityWarning +import com.vitorpamplona.amethyst.ui.components.ZoomableContentView +import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel +import com.vitorpamplona.quartz.events.FileStorageHeaderEvent +import java.io.File + +@Composable +fun FileStorageHeaderDisplay( + baseNote: Note, + roundedCorner: Boolean, + accountViewModel: AccountViewModel, +) { + val eventHeader = (baseNote.event as? FileStorageHeaderEvent) ?: return + val dataEventId = eventHeader.dataEventId() ?: return + + LoadNote(baseNoteHex = dataEventId, accountViewModel) { contentNote -> + if (contentNote != null) { + ObserverAndRenderNIP95(baseNote, contentNote, roundedCorner, accountViewModel) + } + } +} + +@Composable +private fun ObserverAndRenderNIP95( + header: Note, + content: Note, + roundedCorner: Boolean, + accountViewModel: AccountViewModel, +) { + val eventHeader = (header.event as? FileStorageHeaderEvent) ?: return + + val appContext = LocalContext.current.applicationContext + + val noteState by content.live().metadata.observeAsState() + + val content by + remember(noteState) { + // Creates a new object when the event arrives to force an update of the image. + val note = noteState?.note + val uri = header.toNostrUri() + val localDir = note?.idHex?.let { File(File(appContext.cacheDir, "NIP95"), it) } + val blurHash = eventHeader.blurhash() + val dimensions = eventHeader.dimensions() + val description = eventHeader.alt() ?: eventHeader.content + val mimeType = eventHeader.mimeType() + + val newContent = + if (mimeType?.startsWith("image") == true) { + MediaLocalImage( + localFile = localDir, + mimeType = mimeType, + description = description, + dim = dimensions, + blurhash = blurHash, + isVerified = true, + uri = uri, + ) + } else { + MediaLocalVideo( + localFile = localDir, + mimeType = mimeType, + description = description, + dim = dimensions, + isVerified = true, + uri = uri, + authorName = header.author?.toBestDisplayName(), + ) + } + + mutableStateOf(newContent) + } + + Crossfade(targetState = content) { + if (it != null) { + SensitivityWarning(note = header, accountViewModel = accountViewModel) { + ZoomableContentView( + content = it, + roundedCorner = roundedCorner, + accountViewModel = accountViewModel, + ) + } + } + } +} diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/note/types/Git.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/types/Git.kt new file mode 100644 index 000000000..bdf0986d1 --- /dev/null +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/types/Git.kt @@ -0,0 +1,384 @@ +/** + * 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.types + +import androidx.compose.foundation.border +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.material3.HorizontalDivider +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.MutableState +import androidx.compose.runtime.derivedStateOf +import androidx.compose.runtime.getValue +import androidx.compose.runtime.livedata.observeAsState +import androidx.compose.runtime.remember +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.unit.dp +import com.vitorpamplona.amethyst.R +import com.vitorpamplona.amethyst.model.AddressableNote +import com.vitorpamplona.amethyst.model.Note +import com.vitorpamplona.amethyst.ui.components.ClickableUrl +import com.vitorpamplona.amethyst.ui.components.SensitivityWarning +import com.vitorpamplona.amethyst.ui.components.TranslatableRichTextViewer +import com.vitorpamplona.amethyst.ui.note.LoadAddressableNote +import com.vitorpamplona.amethyst.ui.note.LoadDecryptedContent +import com.vitorpamplona.amethyst.ui.note.elements.DisplayUncitedHashtags +import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel +import com.vitorpamplona.amethyst.ui.theme.DividerThickness +import com.vitorpamplona.amethyst.ui.theme.DoubleVertSpacer +import com.vitorpamplona.amethyst.ui.theme.QuoteBorder +import com.vitorpamplona.amethyst.ui.theme.Size10dp +import com.vitorpamplona.amethyst.ui.theme.Size5dp +import com.vitorpamplona.amethyst.ui.theme.StdHorzSpacer +import com.vitorpamplona.amethyst.ui.theme.placeholderText +import com.vitorpamplona.amethyst.ui.theme.replyModifier +import com.vitorpamplona.amethyst.ui.theme.subtleBorder +import com.vitorpamplona.quartz.events.EmptyTagList +import com.vitorpamplona.quartz.events.GitIssueEvent +import com.vitorpamplona.quartz.events.GitPatchEvent +import com.vitorpamplona.quartz.events.GitRepositoryEvent +import com.vitorpamplona.quartz.events.TextNoteEvent +import com.vitorpamplona.quartz.events.toImmutableListOfLists +import kotlinx.collections.immutable.persistentListOf +import kotlinx.collections.immutable.toImmutableList + +@Composable +fun RenderGitPatchEvent( + baseNote: Note, + makeItShort: Boolean, + canPreview: Boolean, + backgroundColor: MutableState, + accountViewModel: AccountViewModel, + nav: (String) -> Unit, +) { + val event = baseNote.event as? GitPatchEvent ?: return + + RenderGitPatchEvent( + event, + baseNote, + makeItShort, + canPreview, + backgroundColor, + accountViewModel, + nav, + ) +} + +@Composable +private fun RenderShortRepositoryHeader( + baseNote: AddressableNote, + accountViewModel: AccountViewModel, + nav: (String) -> Unit, +) { + val noteState by baseNote.live().metadata.observeAsState() + val noteEvent = noteState?.note?.event as? GitRepositoryEvent ?: return + + Column( + modifier = MaterialTheme.colorScheme.replyModifier.padding(10.dp), + ) { + val title = remember(noteEvent) { noteEvent.name() ?: noteEvent.dTag() } + Text( + text = stringResource(id = R.string.git_repository, title), + style = MaterialTheme.typography.titleLarge, + maxLines = 1, + overflow = TextOverflow.Ellipsis, + modifier = Modifier.fillMaxWidth(), + ) + + noteEvent.description()?.let { + Spacer(modifier = DoubleVertSpacer) + Text( + text = it, + maxLines = 1, + overflow = TextOverflow.Ellipsis, + ) + } + } +} + +@Composable +private fun RenderGitPatchEvent( + noteEvent: GitPatchEvent, + note: Note, + makeItShort: Boolean, + canPreview: Boolean, + backgroundColor: MutableState, + accountViewModel: AccountViewModel, + nav: (String) -> Unit, +) { + val repository = remember(noteEvent) { noteEvent.repository() } + + if (repository != null) { + LoadAddressableNote(aTag = repository, accountViewModel = accountViewModel) { + if (it != null) { + RenderShortRepositoryHeader(it, accountViewModel, nav) + Spacer(modifier = DoubleVertSpacer) + } + } + } + + LoadDecryptedContent(note, accountViewModel) { body -> + val eventContent by + remember(note.event) { + derivedStateOf { + val subject = (note.event as? TextNoteEvent)?.subject()?.ifEmpty { null } + + if (!subject.isNullOrBlank() && !body.split("\n")[0].contains(subject)) { + "### $subject\n$body" + } else { + body + } + } + } + + val isAuthorTheLoggedUser = + remember(note.event) { accountViewModel.isLoggedUser(note.author) } + + if (makeItShort && isAuthorTheLoggedUser) { + Text( + text = eventContent, + color = MaterialTheme.colorScheme.placeholderText, + maxLines = 2, + overflow = TextOverflow.Ellipsis, + ) + } else { + SensitivityWarning( + note = note, + accountViewModel = accountViewModel, + ) { + val modifier = remember(note) { Modifier.fillMaxWidth() } + val tags = + remember(note) { note.event?.tags()?.toImmutableListOfLists() ?: EmptyTagList } + + TranslatableRichTextViewer( + content = eventContent, + canPreview = canPreview && !makeItShort, + modifier = modifier, + tags = tags, + backgroundColor = backgroundColor, + accountViewModel = accountViewModel, + nav = nav, + ) + } + + if (note.event?.hasHashtags() == true) { + val hashtags = + remember(note.event) { + note.event?.hashtags()?.toImmutableList() ?: persistentListOf() + } + DisplayUncitedHashtags(hashtags, eventContent, nav) + } + } + } +} + +@Composable +fun RenderGitIssueEvent( + baseNote: Note, + makeItShort: Boolean, + canPreview: Boolean, + backgroundColor: MutableState, + accountViewModel: AccountViewModel, + nav: (String) -> Unit, +) { + val event = baseNote.event as? GitIssueEvent ?: return + + RenderGitIssueEvent( + event, + baseNote, + makeItShort, + canPreview, + backgroundColor, + accountViewModel, + nav, + ) +} + +@Composable +private fun RenderGitIssueEvent( + noteEvent: GitIssueEvent, + note: Note, + makeItShort: Boolean, + canPreview: Boolean, + backgroundColor: MutableState, + accountViewModel: AccountViewModel, + nav: (String) -> Unit, +) { + val repository = remember(noteEvent) { noteEvent.repository() } + + if (repository != null) { + LoadAddressableNote(aTag = repository, accountViewModel = accountViewModel) { + if (it != null) { + RenderShortRepositoryHeader(it, accountViewModel, nav) + Spacer(modifier = DoubleVertSpacer) + } + } + } + + LoadDecryptedContent(note, accountViewModel) { body -> + val eventContent by + remember(note.event) { + derivedStateOf { + val subject = (note.event as? TextNoteEvent)?.subject()?.ifEmpty { null } + + if (!subject.isNullOrBlank() && !body.split("\n")[0].contains(subject)) { + "### $subject\n$body" + } else { + body + } + } + } + + val isAuthorTheLoggedUser = + remember(note.event) { accountViewModel.isLoggedUser(note.author) } + + if (makeItShort && isAuthorTheLoggedUser) { + Text( + text = eventContent, + color = MaterialTheme.colorScheme.placeholderText, + maxLines = 2, + overflow = TextOverflow.Ellipsis, + ) + } else { + SensitivityWarning( + note = note, + accountViewModel = accountViewModel, + ) { + val modifier = remember(note) { Modifier.fillMaxWidth() } + val tags = + remember(note) { note.event?.tags()?.toImmutableListOfLists() ?: EmptyTagList } + + TranslatableRichTextViewer( + content = eventContent, + canPreview = canPreview && !makeItShort, + modifier = modifier, + tags = tags, + backgroundColor = backgroundColor, + accountViewModel = accountViewModel, + nav = nav, + ) + } + + if (note.event?.hasHashtags() == true) { + val hashtags = + remember(note.event) { + note.event?.hashtags()?.toImmutableList() ?: persistentListOf() + } + DisplayUncitedHashtags(hashtags, eventContent, nav) + } + } + } +} + +@Composable +fun RenderGitRepositoryEvent( + baseNote: Note, + accountViewModel: AccountViewModel, + nav: (String) -> Unit, +) { + val event = baseNote.event as? GitRepositoryEvent ?: return + + RenderGitRepositoryEvent(event, baseNote, accountViewModel, nav) +} + +@Composable +private fun RenderGitRepositoryEvent( + noteEvent: GitRepositoryEvent, + note: Note, + accountViewModel: AccountViewModel, + nav: (String) -> Unit, +) { + val title = remember(noteEvent) { noteEvent.name() ?: noteEvent.dTag() } + val summary = remember(noteEvent) { noteEvent.description() } + val web = remember(noteEvent) { noteEvent.web() } + val clone = remember(noteEvent) { noteEvent.clone() } + + Row( + modifier = + Modifier + .clip(shape = QuoteBorder) + .border( + 1.dp, + MaterialTheme.colorScheme.subtleBorder, + QuoteBorder, + ).padding(Size10dp), + ) { + Column { + Text( + text = stringResource(id = R.string.git_repository, title), + style = MaterialTheme.typography.titleMedium, + maxLines = 1, + overflow = TextOverflow.Ellipsis, + modifier = Modifier.fillMaxWidth(), + ) + + summary?.let { + Text( + text = it, + modifier = Modifier.fillMaxWidth().padding(vertical = Size5dp), + maxLines = 3, + overflow = TextOverflow.Ellipsis, + ) + } + + HorizontalDivider(thickness = DividerThickness) + + web?.let { + Row(Modifier.fillMaxWidth().padding(top = Size5dp)) { + Text( + text = stringResource(id = R.string.git_web_address), + maxLines = 1, + overflow = TextOverflow.Ellipsis, + ) + Spacer(modifier = StdHorzSpacer) + ClickableUrl( + url = it, + urlText = it.removePrefix("https://").removePrefix("http://"), + ) + } + } + + clone?.let { + Row(Modifier.fillMaxWidth().padding(top = Size5dp)) { + Text( + text = stringResource(id = R.string.git_clone_address), + maxLines = 1, + overflow = TextOverflow.Ellipsis, + ) + Spacer(modifier = StdHorzSpacer) + ClickableUrl( + url = it, + urlText = it.removePrefix("https://").removePrefix("http://"), + ) + } + } + } + } +} diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/note/types/Highlight.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/types/Highlight.kt new file mode 100644 index 000000000..7fe5f703b --- /dev/null +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/types/Highlight.kt @@ -0,0 +1,201 @@ +/** + * 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.types + +import android.util.Log +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.ExperimentalLayoutApi +import androidx.compose.foundation.layout.FlowRow +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.text.ClickableText +import androidx.compose.material3.LocalTextStyle +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +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 +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.text.AnnotatedString +import androidx.lifecycle.distinctUntilChanged +import androidx.lifecycle.map +import com.vitorpamplona.amethyst.model.Note +import com.vitorpamplona.amethyst.model.User +import com.vitorpamplona.amethyst.ui.components.ClickableUrl +import com.vitorpamplona.amethyst.ui.components.CreateClickableTextWithEmoji +import com.vitorpamplona.amethyst.ui.components.TranslatableRichTextViewer +import com.vitorpamplona.amethyst.ui.components.measureSpaceWidth +import com.vitorpamplona.amethyst.ui.navigation.routeFor +import com.vitorpamplona.amethyst.ui.note.LoadAddressableNote +import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel +import com.vitorpamplona.quartz.encoders.ATag +import com.vitorpamplona.quartz.events.EmptyTagList +import com.vitorpamplona.quartz.events.HighlightEvent +import com.vitorpamplona.quartz.events.LongTextNoteEvent +import java.net.URL + +@Composable +fun RenderHighlight( + note: Note, + makeItShort: Boolean, + canPreview: Boolean, + backgroundColor: MutableState, + accountViewModel: AccountViewModel, + nav: (String) -> Unit, +) { + val quote = remember { (note.event as? HighlightEvent)?.quote() ?: "" } + val author = remember { (note.event as? HighlightEvent)?.author() } + val url = remember { (note.event as? HighlightEvent)?.inUrl() } + val postHex = remember { (note.event as? HighlightEvent)?.taggedAddresses()?.firstOrNull() } + + DisplayHighlight( + highlight = quote, + authorHex = author, + url = url, + postAddress = postHex, + makeItShort = makeItShort, + canPreview = canPreview, + backgroundColor = backgroundColor, + accountViewModel = accountViewModel, + nav = nav, + ) +} + +@Composable +fun DisplayHighlight( + highlight: String, + authorHex: String?, + url: String?, + postAddress: ATag?, + makeItShort: Boolean, + canPreview: Boolean, + backgroundColor: MutableState, + accountViewModel: AccountViewModel, + nav: (String) -> Unit, +) { + val quote = + remember { + highlight.split("\n").joinToString("\n") { "> *${it.removeSuffix(" ")}*" } + } + + TranslatableRichTextViewer( + quote, + canPreview = canPreview && !makeItShort, + remember { Modifier.fillMaxWidth() }, + EmptyTagList, + backgroundColor, + accountViewModel, + nav, + ) + + DisplayQuoteAuthor(authorHex ?: "", url, postAddress, accountViewModel, nav) +} + +@OptIn(ExperimentalLayoutApi::class) +@Composable +private fun DisplayQuoteAuthor( + authorHex: String, + url: String?, + postAddress: ATag?, + accountViewModel: AccountViewModel, + nav: (String) -> Unit, +) { + var userBase by remember { mutableStateOf(accountViewModel.getUserIfExists(authorHex)) } + + if (userBase == null) { + LaunchedEffect(Unit) { + accountViewModel.checkGetOrCreateUser(authorHex) { newUserBase -> + userBase = newUserBase + } + } + } + + val spaceWidth = measureSpaceWidth(textStyle = LocalTextStyle.current) + + FlowRow( + horizontalArrangement = Arrangement.spacedBy(spaceWidth), + verticalArrangement = Arrangement.Center, + ) { + userBase?.let { userBase -> + val userMetadata by userBase.live().userMetadataInfo.observeAsState() + + CreateClickableTextWithEmoji( + clickablePart = userMetadata?.bestDisplayName() ?: userMetadata?.bestUsername() ?: userBase.pubkeyDisplayHex(), + maxLines = 1, + route = "User/${userBase.pubkeyHex}", + nav = nav, + tags = userMetadata?.tags, + ) + } + + url?.let { url -> LoadAndDisplayUrl(url) } + + postAddress?.let { address -> LoadAndDisplayPost(address, accountViewModel, nav) } + } +} + +@Composable +fun LoadAndDisplayUrl(url: String) { + val validatedUrl = + remember { + try { + URL(url) + } catch (e: Exception) { + Log.w("Note Compose", "Invalid URI: $url") + null + } + } + + validatedUrl?.host?.let { host -> + Text(remember { "-" }, maxLines = 1) + ClickableUrl(urlText = host, url = url) + } +} + +@Composable +private fun LoadAndDisplayPost( + postAddress: ATag, + accountViewModel: AccountViewModel, + nav: (String) -> Unit, +) { + LoadAddressableNote(aTag = postAddress, accountViewModel) { + it?.let { note -> + val noteEvent by + note.live().metadata.map { it.note.event }.distinctUntilChanged().observeAsState(note.event) + + val title = remember(noteEvent) { (noteEvent as? LongTextNoteEvent)?.title() } + + title?.let { + Text(remember { "-" }, maxLines = 1) + ClickableText( + text = AnnotatedString(title), + onClick = { routeFor(note, accountViewModel.userProfile())?.let { nav(it) } }, + style = LocalTextStyle.current.copy(color = MaterialTheme.colorScheme.primary), + ) + } + } + } +} diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/note/types/LiveActivity.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/types/LiveActivity.kt new file mode 100644 index 000000000..36f25aaed --- /dev/null +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/types/LiveActivity.kt @@ -0,0 +1,217 @@ +/** + * 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.types + +import androidx.compose.animation.Crossfade +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue +import androidx.compose.runtime.livedata.observeAsState +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.unit.dp +import com.vitorpamplona.amethyst.R +import com.vitorpamplona.amethyst.model.Note +import com.vitorpamplona.amethyst.model.User +import com.vitorpamplona.amethyst.ui.components.VideoView +import com.vitorpamplona.amethyst.ui.note.ClickableUserPicture +import com.vitorpamplona.amethyst.ui.note.UsernameDisplay +import com.vitorpamplona.amethyst.ui.screen.equalImmutableLists +import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel +import com.vitorpamplona.amethyst.ui.screen.loggedIn.CheckIfUrlIsOnline +import com.vitorpamplona.amethyst.ui.screen.loggedIn.CrossfadeCheckIfUrlIsOnline +import com.vitorpamplona.amethyst.ui.screen.loggedIn.LiveFlag +import com.vitorpamplona.amethyst.ui.screen.loggedIn.ScheduledFlag +import com.vitorpamplona.amethyst.ui.theme.StdHorzSpacer +import com.vitorpamplona.amethyst.ui.theme.placeholderText +import com.vitorpamplona.quartz.events.LiveActivitiesEvent +import com.vitorpamplona.quartz.events.Participant +import kotlinx.collections.immutable.ImmutableList +import kotlinx.collections.immutable.persistentListOf +import java.util.Locale + +@Composable +fun RenderLiveActivityEvent( + baseNote: Note, + accountViewModel: AccountViewModel, + nav: (String) -> Unit, +) { + Row(modifier = Modifier.padding(top = 5.dp)) { + Column( + modifier = Modifier.fillMaxWidth(), + horizontalAlignment = Alignment.CenterHorizontally, + ) { + RenderLiveActivityEventInner(baseNote = baseNote, accountViewModel, nav) + } + } +} + +@Composable +fun RenderLiveActivityEventInner( + baseNote: Note, + accountViewModel: AccountViewModel, + nav: (String) -> Unit, +) { + val noteEvent = baseNote.event as? LiveActivitiesEvent ?: return + + val eventUpdates by baseNote.live().metadata.observeAsState() + + val media = remember(eventUpdates) { noteEvent.streaming() } + val cover = remember(eventUpdates) { noteEvent.image() } + val subject = remember(eventUpdates) { noteEvent.title() } + val content = remember(eventUpdates) { noteEvent.summary() } + val participants = remember(eventUpdates) { noteEvent.participants() } + val status = remember(eventUpdates) { noteEvent.status() } + val starts = remember(eventUpdates) { noteEvent.starts() } + + Row( + verticalAlignment = Alignment.CenterVertically, + modifier = + Modifier + .padding(vertical = 5.dp) + .fillMaxWidth(), + ) { + subject?.let { + Text( + text = it, + fontWeight = FontWeight.Bold, + maxLines = 3, + overflow = TextOverflow.Ellipsis, + modifier = Modifier.weight(1f), + ) + } + + Spacer(modifier = StdHorzSpacer) + + Crossfade(targetState = status, label = "RenderLiveActivityEventInner") { + when (it) { + LiveActivitiesEvent.STATUS_LIVE -> { + media?.let { CrossfadeCheckIfUrlIsOnline(it, accountViewModel) { LiveFlag() } } + } + + LiveActivitiesEvent.STATUS_PLANNED -> { + ScheduledFlag(starts) + } + } + } + } + + var participantUsers by remember { + mutableStateOf>>( + persistentListOf(), + ) + } + + LaunchedEffect(key1 = eventUpdates) { + accountViewModel.loadParticipants(participants) { newParticipantUsers -> + if (!equalImmutableLists(newParticipantUsers, participantUsers)) { + participantUsers = newParticipantUsers + } + } + } + + media?.let { media -> + if (status == LiveActivitiesEvent.STATUS_LIVE) { + CheckIfUrlIsOnline(media, accountViewModel) { isOnline -> + if (isOnline) { + Row( + verticalAlignment = Alignment.CenterVertically, + ) { + VideoView( + videoUri = media, + title = subject, + artworkUri = cover, + authorName = baseNote.author?.toBestDisplayName(), + roundedCorner = true, + accountViewModel = accountViewModel, + nostrUriCallback = "nostr:${baseNote.toNEvent()}", + ) + } + } else { + Row( + verticalAlignment = Alignment.CenterVertically, + modifier = + Modifier + .padding(10.dp) + .height(100.dp), + ) { + Text( + text = stringResource(id = R.string.live_stream_is_offline), + color = MaterialTheme.colorScheme.onBackground, + fontWeight = FontWeight.Bold, + ) + } + } + } + } else if (status == LiveActivitiesEvent.STATUS_ENDED) { + Row( + verticalAlignment = Alignment.CenterVertically, + modifier = + Modifier + .padding(10.dp) + .height(100.dp), + ) { + Text( + text = stringResource(id = R.string.live_stream_has_ended), + color = MaterialTheme.colorScheme.onBackground, + fontWeight = FontWeight.Bold, + ) + } + } + } + + participantUsers.forEach { + Row( + verticalAlignment = Alignment.CenterVertically, + modifier = + Modifier + .padding(vertical = 5.dp) + .clickable { nav("User/${it.second.pubkeyHex}") }, + ) { + ClickableUserPicture(it.second, 25.dp, accountViewModel) + Spacer(StdHorzSpacer) + UsernameDisplay(it.second, Modifier.weight(1f)) + Spacer(StdHorzSpacer) + it.first.role?.let { + Text( + text = it.capitalize(Locale.ROOT), + color = MaterialTheme.colorScheme.placeholderText, + maxLines = 1, + ) + } + } + } +} diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/note/types/LongForm.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/types/LongForm.kt new file mode 100644 index 000000000..f4584efbe --- /dev/null +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/types/LongForm.kt @@ -0,0 +1,133 @@ +/** + * 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.types + +import androidx.compose.foundation.border +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.layout.ContentScale +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.unit.dp +import coil.compose.AsyncImage +import com.vitorpamplona.amethyst.R +import com.vitorpamplona.amethyst.model.Note +import com.vitorpamplona.amethyst.ui.note.elements.DefaultImageHeader +import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel +import com.vitorpamplona.amethyst.ui.theme.QuoteBorder +import com.vitorpamplona.amethyst.ui.theme.Size5dp +import com.vitorpamplona.amethyst.ui.theme.StdVertSpacer +import com.vitorpamplona.amethyst.ui.theme.subtleBorder +import com.vitorpamplona.quartz.events.LongTextNoteEvent + +@Composable +fun RenderLongFormContent( + note: Note, + accountViewModel: AccountViewModel, + nav: (String) -> Unit, +) { + val noteEvent = note.event as? LongTextNoteEvent ?: return + + LongFormHeader(noteEvent, note, accountViewModel) +} + +@Composable +private fun LongFormHeader( + noteEvent: LongTextNoteEvent, + note: Note, + accountViewModel: AccountViewModel, +) { + val image = remember(noteEvent) { noteEvent.image() } + val title = remember(noteEvent) { noteEvent.title() } + val summary = + remember(noteEvent) { + noteEvent.summary()?.ifBlank { null } ?: noteEvent.content.take(200).ifBlank { null } + } + + Row( + modifier = + Modifier + .padding(top = Size5dp) + .clip(shape = QuoteBorder) + .border( + 1.dp, + MaterialTheme.colorScheme.subtleBorder, + QuoteBorder, + ), + ) { + Column { + val automaticallyShowUrlPreview = + remember { accountViewModel.settings.showUrlPreview.value } + + if (automaticallyShowUrlPreview) { + image?.let { + AsyncImage( + model = it, + contentDescription = + stringResource( + R.string.preview_card_image_for, + it, + ), + contentScale = ContentScale.FillWidth, + modifier = Modifier.fillMaxWidth(), + ) + } + ?: DefaultImageHeader(note, accountViewModel) + } + + title?.let { + Text( + text = it, + style = MaterialTheme.typography.bodyLarge, + modifier = + Modifier + .fillMaxWidth() + .padding(start = 10.dp, end = 10.dp, top = 10.dp), + ) + } + + summary?.let { + Spacer(modifier = StdVertSpacer) + Text( + text = it, + style = MaterialTheme.typography.bodySmall, + modifier = + Modifier + .fillMaxWidth() + .padding(start = 10.dp, end = 10.dp, bottom = 10.dp), + color = Color.Gray, + maxLines = 3, + overflow = TextOverflow.Ellipsis, + ) + } + } + } +} diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/note/types/MedicalData.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/types/MedicalData.kt new file mode 100644 index 000000000..0231b8d60 --- /dev/null +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/types/MedicalData.kt @@ -0,0 +1,265 @@ +/** + * 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.types + +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.IntrinsicSize +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.material3.HorizontalDivider +import androidx.compose.material3.Text +import androidx.compose.material3.VerticalDivider +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.produceState +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import com.vitorpamplona.amethyst.model.Bundle +import com.vitorpamplona.amethyst.model.FhirElementDatabase +import com.vitorpamplona.amethyst.model.LensSpecification +import com.vitorpamplona.amethyst.model.Note +import com.vitorpamplona.amethyst.model.Patient +import com.vitorpamplona.amethyst.model.Practitioner +import com.vitorpamplona.amethyst.model.Resource +import com.vitorpamplona.amethyst.model.VisionPrescription +import com.vitorpamplona.amethyst.model.findReferenceInDb +import com.vitorpamplona.amethyst.model.parseResourceBundleOrNull +import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel +import com.vitorpamplona.amethyst.ui.theme.DividerThickness +import com.vitorpamplona.amethyst.ui.theme.DoubleVertSpacer +import com.vitorpamplona.amethyst.ui.theme.Size10dp +import com.vitorpamplona.amethyst.ui.theme.StdVertSpacer +import com.vitorpamplona.quartz.events.Event +import com.vitorpamplona.quartz.events.FhirResourceEvent +import kotlinx.collections.immutable.ImmutableMap +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext +import java.text.DecimalFormat +import java.text.NumberFormat + +@Preview +@Composable +fun RenderEyeGlassesPrescriptionPreview() { + 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) +} + +@Composable +fun RenderFhirResource( + baseNote: Note, + accountViewModel: AccountViewModel, + nav: (String) -> Unit, +) { + val event = baseNote.event as? FhirResourceEvent ?: return + + RenderFhirResource(event) +} + +@Composable +fun RenderFhirResource(event: FhirResourceEvent) { + val state by produceState(initialValue = FhirElementDatabase(), key1 = event) { + withContext(Dispatchers.Default) { + parseResourceBundleOrNull(event.content)?.let { + value = it + } + } + } + + state.baseResource?.let { resource -> + when (resource) { + is Bundle -> { + val vision = resource.entry.filterIsInstance(VisionPrescription::class.java) + + vision.firstOrNull()?.let { + RenderEyeGlassesPrescription(it, state.localDb) + } + } + is VisionPrescription -> { + RenderEyeGlassesPrescription(resource, state.localDb) + } + else -> { + } + } + } +} + +@Composable +fun RenderEyeGlassesPrescription( + visionPrescription: VisionPrescription, + db: ImmutableMap, +) { + Column( + modifier = + Modifier + .fillMaxWidth() + .padding(horizontal = 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) + } + + visionPrescription.prescriber?.reference?.let { + val practitioner = findReferenceInDb(it, db) as? Practitioner + + practitioner?.name?.firstOrNull()?.assembleName()?.let { + Spacer(DoubleVertSpacer) + 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/app/src/main/java/com/vitorpamplona/amethyst/ui/note/types/PeopleList.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/types/PeopleList.kt new file mode 100644 index 000000000..a5d6c2ae0 --- /dev/null +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/types/PeopleList.kt @@ -0,0 +1,126 @@ +/** + * 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.types + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.ExperimentalLayoutApi +import androidx.compose.foundation.layout.FlowRow +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.MutableState +import androidx.compose.runtime.derivedStateOf +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +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.text.font.FontWeight +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.unit.dp +import com.vitorpamplona.amethyst.model.Note +import com.vitorpamplona.amethyst.model.User +import com.vitorpamplona.amethyst.ui.components.ShowMoreButton +import com.vitorpamplona.amethyst.ui.note.UserCompose +import com.vitorpamplona.amethyst.ui.note.getGradient +import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel +import com.vitorpamplona.quartz.events.PeopleListEvent +import kotlinx.collections.immutable.ImmutableList +import kotlinx.collections.immutable.persistentListOf + +@OptIn(ExperimentalLayoutApi::class) +@Composable +fun DisplayPeopleList( + baseNote: Note, + backgroundColor: MutableState, + accountViewModel: AccountViewModel, + nav: (String) -> Unit, +) { + val noteEvent = baseNote.event as? PeopleListEvent ?: return + + var members by remember { mutableStateOf>(persistentListOf()) } + + var expanded by remember { mutableStateOf(false) } + + val toMembersShow = + if (expanded) { + members + } else { + members.take(3) + } + + val name by remember { derivedStateOf { "#${noteEvent.dTag()}" } } + + Text( + text = name, + fontWeight = FontWeight.Bold, + maxLines = 1, + overflow = TextOverflow.Ellipsis, + modifier = + Modifier + .fillMaxWidth() + .padding(5.dp), + textAlign = TextAlign.Center, + ) + + LaunchedEffect(Unit) { + accountViewModel.loadUsers(noteEvent.bookmarkedPeople()) { + members = it + } + } + + Box { + FlowRow(modifier = Modifier.padding(top = 5.dp)) { + toMembersShow.forEach { user -> + Row(modifier = Modifier.fillMaxWidth()) { + UserCompose( + user, + overallModifier = Modifier, + accountViewModel = accountViewModel, + nav = nav, + ) + } + } + } + + if (members.size > 3 && !expanded) { + Row( + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.Center, + modifier = + Modifier + .align(Alignment.BottomCenter) + .fillMaxWidth() + .background(getGradient(backgroundColor)), + ) { + ShowMoreButton { expanded = !expanded } + } + } + } +} diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/note/types/PinList.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/types/PinList.kt new file mode 100644 index 000000000..b5261a7dd --- /dev/null +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/types/PinList.kt @@ -0,0 +1,131 @@ +/** + * 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.types + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.ExperimentalLayoutApi +import androidx.compose.foundation.layout.FlowRow +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.width +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.MutableState +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +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.text.font.FontWeight +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.unit.dp +import com.vitorpamplona.amethyst.model.Note +import com.vitorpamplona.amethyst.ui.components.ShowMoreButton +import com.vitorpamplona.amethyst.ui.components.TranslatableRichTextViewer +import com.vitorpamplona.amethyst.ui.note.PinIcon +import com.vitorpamplona.amethyst.ui.note.getGradient +import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel +import com.vitorpamplona.amethyst.ui.theme.Size15Modifier +import com.vitorpamplona.quartz.events.EmptyTagList +import com.vitorpamplona.quartz.events.PinListEvent + +@OptIn(ExperimentalLayoutApi::class) +@Composable +fun RenderPinListEvent( + baseNote: Note, + backgroundColor: MutableState, + accountViewModel: AccountViewModel, + nav: (String) -> Unit, +) { + val noteEvent = baseNote.event as? PinListEvent ?: return + + val pins by remember { mutableStateOf(noteEvent.pins()) } + + var expanded by remember { mutableStateOf(false) } + + val pinsToShow = + if (expanded) { + pins + } else { + pins.take(3) + } + + Text( + text = "#${noteEvent.dTag()}", + fontWeight = FontWeight.Bold, + maxLines = 1, + overflow = TextOverflow.Ellipsis, + modifier = + Modifier + .fillMaxWidth() + .padding(5.dp), + textAlign = TextAlign.Center, + ) + + Box { + FlowRow(modifier = Modifier.padding(top = 5.dp)) { + pinsToShow.forEach { pin -> + Row( + modifier = Modifier.fillMaxWidth(), + verticalAlignment = Alignment.CenterVertically, + ) { + PinIcon( + modifier = Size15Modifier, + tint = MaterialTheme.colorScheme.onBackground.copy(0.32f), + ) + + Spacer(modifier = Modifier.width(5.dp)) + + TranslatableRichTextViewer( + content = pin, + canPreview = true, + tags = EmptyTagList, + backgroundColor = backgroundColor, + accountViewModel = accountViewModel, + nav = nav, + ) + } + } + } + + if (pins.size > 3 && !expanded) { + Row( + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.Center, + modifier = + Modifier + .align(Alignment.BottomCenter) + .fillMaxWidth() + .background(getGradient(backgroundColor)), + ) { + ShowMoreButton { expanded = !expanded } + } + } + } +} diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/note/types/Poll.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/types/Poll.kt new file mode 100644 index 000000000..2dfc1ed79 --- /dev/null +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/types/Poll.kt @@ -0,0 +1,94 @@ +/** + * 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.types + +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.MutableState +import androidx.compose.runtime.remember +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.text.style.TextOverflow +import com.vitorpamplona.amethyst.model.Note +import com.vitorpamplona.amethyst.ui.components.SensitivityWarning +import com.vitorpamplona.amethyst.ui.components.TranslatableRichTextViewer +import com.vitorpamplona.amethyst.ui.note.PollNote +import com.vitorpamplona.amethyst.ui.note.elements.DisplayUncitedHashtags +import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel +import com.vitorpamplona.amethyst.ui.theme.placeholderText +import com.vitorpamplona.quartz.events.EmptyTagList +import com.vitorpamplona.quartz.events.PollNoteEvent +import com.vitorpamplona.quartz.events.toImmutableListOfLists +import kotlinx.collections.immutable.toImmutableList + +@Composable +fun RenderPoll( + note: Note, + makeItShort: Boolean, + canPreview: Boolean, + backgroundColor: MutableState, + accountViewModel: AccountViewModel, + nav: (String) -> Unit, +) { + val noteEvent = note.event as? PollNoteEvent ?: return + val eventContent = noteEvent.content() + + if (makeItShort && accountViewModel.isLoggedUser(note.author)) { + Text( + text = eventContent, + color = MaterialTheme.colorScheme.placeholderText, + maxLines = 2, + overflow = TextOverflow.Ellipsis, + ) + } else { + val tags = remember(note) { note.event?.tags()?.toImmutableListOfLists() ?: EmptyTagList } + + SensitivityWarning( + note = note, + accountViewModel = accountViewModel, + ) { + TranslatableRichTextViewer( + content = eventContent, + canPreview = canPreview && !makeItShort, + modifier = remember { Modifier.fillMaxWidth() }, + tags = tags, + backgroundColor = backgroundColor, + accountViewModel = accountViewModel, + nav = nav, + ) + + PollNote( + note, + canPreview = canPreview && !makeItShort, + backgroundColor, + accountViewModel, + nav, + ) + } + + if (noteEvent.hasHashtags()) { + val hashtags = remember { noteEvent.hashtags().toImmutableList() } + DisplayUncitedHashtags(hashtags, eventContent, nav) + } + } +} diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/note/types/PrivateMessage.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/types/PrivateMessage.kt new file mode 100644 index 000000000..537a9ee6d --- /dev/null +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/types/PrivateMessage.kt @@ -0,0 +1,118 @@ +/** + * 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.types + +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.MutableState +import androidx.compose.runtime.remember +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.style.TextOverflow +import com.vitorpamplona.amethyst.R +import com.vitorpamplona.amethyst.model.Note +import com.vitorpamplona.amethyst.ui.components.SensitivityWarning +import com.vitorpamplona.amethyst.ui.components.TranslatableRichTextViewer +import com.vitorpamplona.amethyst.ui.note.LoadDecryptedContent +import com.vitorpamplona.amethyst.ui.note.elements.DisplayUncitedHashtags +import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel +import com.vitorpamplona.amethyst.ui.theme.placeholderText +import com.vitorpamplona.quartz.encoders.toNpub +import com.vitorpamplona.quartz.events.EmptyTagList +import com.vitorpamplona.quartz.events.PrivateDmEvent +import com.vitorpamplona.quartz.events.toImmutableListOfLists +import kotlinx.collections.immutable.persistentListOf +import kotlinx.collections.immutable.toImmutableList + +@Composable +fun RenderPrivateMessage( + note: Note, + makeItShort: Boolean, + canPreview: Boolean, + backgroundColor: MutableState, + accountViewModel: AccountViewModel, + nav: (String) -> Unit, +) { + val noteEvent = note.event as? PrivateDmEvent ?: return + + val withMe = remember { noteEvent.with(accountViewModel.userProfile().pubkeyHex) } + if (withMe) { + LoadDecryptedContent(note, accountViewModel) { eventContent -> + val modifier = remember(note.event?.id()) { Modifier.fillMaxWidth() } + val isAuthorTheLoggedUser = + remember(note.event?.id()) { accountViewModel.isLoggedUser(note.author) } + + val tags = + remember(note) { note.event?.tags()?.toImmutableListOfLists() ?: EmptyTagList } + + if (makeItShort && isAuthorTheLoggedUser) { + Text( + text = eventContent, + color = MaterialTheme.colorScheme.placeholderText, + maxLines = 2, + overflow = TextOverflow.Ellipsis, + ) + } else { + SensitivityWarning( + note = note, + accountViewModel = accountViewModel, + ) { + TranslatableRichTextViewer( + content = eventContent, + canPreview = canPreview && !makeItShort, + modifier = modifier, + tags = tags, + backgroundColor = backgroundColor, + accountViewModel = accountViewModel, + nav = nav, + ) + } + + if (noteEvent.hasHashtags()) { + val hashtags = + remember(note.event?.id()) { + note.event?.hashtags()?.toImmutableList() ?: persistentListOf() + } + DisplayUncitedHashtags(hashtags, eventContent, nav) + } + } + } + } else { + val recipient = noteEvent.recipientPubKeyBytes()?.toNpub() ?: "Someone" + + TranslatableRichTextViewer( + stringResource( + id = R.string.private_conversation_notification, + "@${note.author?.pubkeyNpub()}", + "@$recipient", + ), + canPreview = !makeItShort, + Modifier.fillMaxWidth(), + EmptyTagList, + backgroundColor, + accountViewModel, + nav, + ) + } +} diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/note/types/Reaction.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/types/Reaction.kt new file mode 100644 index 000000000..d42515700 --- /dev/null +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/types/Reaction.kt @@ -0,0 +1,58 @@ +/** + * 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.types + +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.MutableState +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import com.vitorpamplona.amethyst.model.Note +import com.vitorpamplona.amethyst.ui.note.NoteCompose +import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel + +@Composable +fun RenderReaction( + note: Note, + backgroundColor: MutableState, + accountViewModel: AccountViewModel, + nav: (String) -> Unit, +) { + note.replyTo?.lastOrNull()?.let { + NoteCompose( + it, + modifier = Modifier, + isBoostedNote = true, + unPackReply = false, + parentBackgroundColor = backgroundColor, + accountViewModel = accountViewModel, + nav = nav, + ) + } + + // Reposts have trash in their contents. + val refactorReactionText = if (note.event?.content() == "+") "❤" else note.event?.content() ?: "" + + Text( + text = refactorReactionText, + maxLines = 1, + ) +} diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/note/types/RelayList.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/types/RelayList.kt new file mode 100644 index 000000000..ea134909e --- /dev/null +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/types/RelayList.kt @@ -0,0 +1,178 @@ +/** + * 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.types + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.MutableState +import androidx.compose.runtime.derivedStateOf +import androidx.compose.runtime.getValue +import androidx.compose.runtime.livedata.observeAsState +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +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.text.font.FontWeight +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.unit.dp +import com.vitorpamplona.amethyst.model.Note +import com.vitorpamplona.amethyst.model.RelayBriefInfoCache +import com.vitorpamplona.amethyst.ui.actions.NewRelayListView +import com.vitorpamplona.amethyst.ui.components.ShowMoreButton +import com.vitorpamplona.amethyst.ui.note.AddRelayButton +import com.vitorpamplona.amethyst.ui.note.RemoveRelayButton +import com.vitorpamplona.amethyst.ui.note.getGradient +import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel +import com.vitorpamplona.quartz.events.RelaySetEvent +import kotlinx.collections.immutable.toImmutableList + +@Composable +fun DisplayRelaySet( + baseNote: Note, + backgroundColor: MutableState, + accountViewModel: AccountViewModel, + nav: (String) -> Unit, +) { + val noteEvent = baseNote.event as? RelaySetEvent ?: return + + val relays by + remember(baseNote) { + mutableStateOf( + noteEvent.relays().map { RelayBriefInfoCache.RelayBriefInfo(it) }.toImmutableList(), + ) + } + + var expanded by remember { mutableStateOf(false) } + + val toMembersShow = + if (expanded) { + relays + } else { + relays.take(3) + } + + val relayListName by remember { derivedStateOf { "#${noteEvent.dTag()}" } } + + val relayDescription by remember { derivedStateOf { noteEvent.description() } } + + Text( + text = relayListName, + fontWeight = FontWeight.Bold, + maxLines = 1, + overflow = TextOverflow.Ellipsis, + modifier = + Modifier + .fillMaxWidth() + .padding(5.dp), + textAlign = TextAlign.Center, + ) + + relayDescription?.let { + Text( + text = it, + maxLines = 3, + overflow = TextOverflow.Ellipsis, + modifier = + Modifier + .fillMaxWidth() + .padding(5.dp), + textAlign = TextAlign.Center, + color = Color.Gray, + ) + } + + Box { + Column(modifier = Modifier.padding(top = 5.dp)) { + toMembersShow.forEach { relay -> + Row( + modifier = Modifier.fillMaxWidth(), + verticalAlignment = Alignment.CenterVertically, + ) { + Text( + text = relay.displayUrl, + fontWeight = FontWeight.Bold, + maxLines = 1, + overflow = TextOverflow.Ellipsis, + modifier = + Modifier + .padding(start = 10.dp, bottom = 5.dp) + .weight(1f), + ) + + Column(modifier = Modifier.padding(start = 10.dp)) { + RelayOptionsAction(relay.url, accountViewModel, nav) + } + } + } + } + + if (relays.size > 3 && !expanded) { + Row( + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.Center, + modifier = + Modifier + .align(Alignment.BottomCenter) + .fillMaxWidth() + .background(getGradient(backgroundColor)), + ) { + ShowMoreButton { expanded = !expanded } + } + } + } +} + +@Composable +private fun RelayOptionsAction( + relay: String, + accountViewModel: AccountViewModel, + nav: (String) -> Unit, +) { + val userStateRelayInfo by accountViewModel.account.userProfile().live().relayInfo.observeAsState() + val isCurrentlyOnTheUsersList by + remember(userStateRelayInfo) { + derivedStateOf { + userStateRelayInfo?.user?.latestContactList?.relays()?.none { it.key == relay } == true + } + } + + var wantsToAddRelay by remember { mutableStateOf("") } + + if (wantsToAddRelay.isNotEmpty()) { + NewRelayListView({ wantsToAddRelay = "" }, accountViewModel, wantsToAddRelay, nav = nav) + } + + if (isCurrentlyOnTheUsersList) { + AddRelayButton { wantsToAddRelay = relay } + } else { + RemoveRelayButton { wantsToAddRelay = relay } + } +} diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/note/types/RenderPostApproval.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/types/RenderPostApproval.kt new file mode 100644 index 000000000..d9e3c4197 --- /dev/null +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/types/RenderPostApproval.kt @@ -0,0 +1,97 @@ +/** + * 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.types + +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.MutableState +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.res.stringResource +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.unit.dp +import com.vitorpamplona.amethyst.R +import com.vitorpamplona.amethyst.model.Note +import com.vitorpamplona.amethyst.ui.note.LoadAddressableNote +import com.vitorpamplona.amethyst.ui.note.NoteCompose +import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel +import com.vitorpamplona.amethyst.ui.theme.replyModifier +import com.vitorpamplona.quartz.events.CommunityPostApprovalEvent + +@Composable +fun RenderPostApproval( + note: Note, + makeItShort: Boolean, + canPreview: Boolean, + backgroundColor: MutableState, + accountViewModel: AccountViewModel, + nav: (String) -> Unit, +) { + if (note.replyTo.isNullOrEmpty()) return + + val noteEvent = note.event as? CommunityPostApprovalEvent ?: return + + Column(Modifier.fillMaxWidth()) { + noteEvent.communities().forEach { + LoadAddressableNote(it, accountViewModel) { + it?.let { + NoteCompose( + it, + parentBackgroundColor = backgroundColor, + accountViewModel = accountViewModel, + nav = nav, + ) + } + } + } + + Text( + text = stringResource(id = R.string.community_approved_posts), + fontWeight = FontWeight.Bold, + maxLines = 1, + overflow = TextOverflow.Ellipsis, + modifier = + Modifier + .fillMaxWidth() + .padding(5.dp), + textAlign = TextAlign.Center, + ) + + note.replyTo?.forEach { + NoteCompose( + it, + modifier = MaterialTheme.colorScheme.replyModifier, + unPackReply = false, + makeItShort = true, + isQuotedNote = true, + parentBackgroundColor = backgroundColor, + accountViewModel = accountViewModel, + nav = nav, + ) + } + } +} diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/note/types/Report.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/types/Report.kt new file mode 100644 index 000000000..50902b48d --- /dev/null +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/types/Report.kt @@ -0,0 +1,108 @@ +/** + * 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.types + +import androidx.compose.foundation.border +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.material3.MaterialTheme +import androidx.compose.runtime.Composable +import androidx.compose.runtime.MutableState +import androidx.compose.runtime.remember +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.unit.dp +import com.vitorpamplona.amethyst.R +import com.vitorpamplona.amethyst.model.Note +import com.vitorpamplona.amethyst.ui.components.TranslatableRichTextViewer +import com.vitorpamplona.amethyst.ui.note.NoteCompose +import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel +import com.vitorpamplona.amethyst.ui.theme.QuoteBorder +import com.vitorpamplona.amethyst.ui.theme.subtleBorder +import com.vitorpamplona.quartz.events.EmptyTagList +import com.vitorpamplona.quartz.events.ReportEvent + +@Composable +fun RenderReport( + note: Note, + backgroundColor: MutableState, + accountViewModel: AccountViewModel, + nav: (String) -> Unit, +) { + val noteEvent = note.event as? ReportEvent ?: return + + val base = remember { (noteEvent.reportedPost() + noteEvent.reportedAuthor()) } + + val reportType = + base + .map { + when (it.reportType) { + ReportEvent.ReportType.EXPLICIT -> stringResource(R.string.explicit_content) + ReportEvent.ReportType.NUDITY -> stringResource(R.string.nudity) + ReportEvent.ReportType.PROFANITY -> stringResource(R.string.profanity_hateful_speech) + ReportEvent.ReportType.SPAM -> stringResource(R.string.spam) + ReportEvent.ReportType.IMPERSONATION -> stringResource(R.string.impersonation) + ReportEvent.ReportType.ILLEGAL -> stringResource(R.string.illegal_behavior) + ReportEvent.ReportType.OTHER -> stringResource(R.string.other) + } + } + .toSet() + .joinToString(", ") + + val content = + remember { + reportType + (note.event?.content()?.ifBlank { null }?.let { ": $it" } ?: "") + } + + TranslatableRichTextViewer( + content = content, + canPreview = true, + modifier = Modifier, + tags = EmptyTagList, + backgroundColor = backgroundColor, + accountViewModel = accountViewModel, + nav = nav, + ) + + note.replyTo?.lastOrNull()?.let { + NoteCompose( + baseNote = it, + isQuotedNote = true, + modifier = + Modifier + .padding(top = 5.dp) + .fillMaxWidth() + .clip(shape = QuoteBorder) + .border( + 1.dp, + MaterialTheme.colorScheme.subtleBorder, + QuoteBorder, + ), + unPackReply = false, + makeItShort = true, + parentBackgroundColor = backgroundColor, + accountViewModel = accountViewModel, + nav = nav, + ) + } +} diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/note/types/Text.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/types/Text.kt new file mode 100644 index 000000000..cfea566bf --- /dev/null +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/types/Text.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.types + +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.MutableState +import androidx.compose.runtime.State +import androidx.compose.runtime.derivedStateOf +import androidx.compose.runtime.getValue +import androidx.compose.runtime.remember +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.text.style.TextOverflow +import com.vitorpamplona.amethyst.model.Note +import com.vitorpamplona.amethyst.ui.components.GenericLoadable +import com.vitorpamplona.amethyst.ui.components.SensitivityWarning +import com.vitorpamplona.amethyst.ui.components.TranslatableRichTextViewer +import com.vitorpamplona.amethyst.ui.note.LoadDecryptedContent +import com.vitorpamplona.amethyst.ui.note.elements.DisplayUncitedHashtags +import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel +import com.vitorpamplona.amethyst.ui.theme.placeholderText +import com.vitorpamplona.quartz.events.EmptyTagList +import com.vitorpamplona.quartz.events.TextNoteEvent +import com.vitorpamplona.quartz.events.toImmutableListOfLists +import kotlinx.collections.immutable.persistentListOf +import kotlinx.collections.immutable.toImmutableList + +@Composable +fun RenderTextEvent( + note: Note, + makeItShort: Boolean, + canPreview: Boolean, + backgroundColor: MutableState, + editState: State>, + accountViewModel: AccountViewModel, + nav: (String) -> Unit, +) { + LoadDecryptedContent( + note, + accountViewModel, + ) { body -> + val eventContent by + remember(note.event) { + derivedStateOf { + val subject = (note.event as? TextNoteEvent)?.subject()?.ifEmpty { null } + val newBody = + if (editState.value is GenericLoadable.Loaded) { + val state = + (editState.value as? GenericLoadable.Loaded)?.loaded?.modificationToShow + state?.value?.event?.content() ?: body + } else { + body + } + + if (!subject.isNullOrBlank() && !newBody.split("\n")[0].contains(subject)) { + "### $subject\n$newBody" + } else { + newBody + } + } + } + + val isAuthorTheLoggedUser = + remember(note.event) { accountViewModel.isLoggedUser(note.author) } + + if (makeItShort && isAuthorTheLoggedUser) { + Text( + text = eventContent, + color = MaterialTheme.colorScheme.placeholderText, + maxLines = 2, + overflow = TextOverflow.Ellipsis, + ) + } else { + SensitivityWarning( + note = note, + accountViewModel = accountViewModel, + ) { + val modifier = remember(note) { Modifier.fillMaxWidth() } + val tags = + remember(note) { note.event?.tags()?.toImmutableListOfLists() ?: EmptyTagList } + + TranslatableRichTextViewer( + content = eventContent, + canPreview = canPreview && !makeItShort, + modifier = modifier, + tags = tags, + backgroundColor = backgroundColor, + accountViewModel = accountViewModel, + nav = nav, + ) + } + + if (note.event?.hasHashtags() == true) { + val hashtags = + remember(note.event) { + note.event?.hashtags()?.toImmutableList() ?: persistentListOf() + } + DisplayUncitedHashtags(hashtags, eventContent, nav) + } + } + } +} diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/note/types/TextModification.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/types/TextModification.kt new file mode 100644 index 000000000..41412de94 --- /dev/null +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/types/TextModification.kt @@ -0,0 +1,279 @@ +/** + * 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.types + +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.material3.Button +import androidx.compose.material3.Card +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.MutableState +import androidx.compose.runtime.Stable +import androidx.compose.runtime.derivedStateOf +import androidx.compose.runtime.getValue +import androidx.compose.runtime.livedata.observeAsState +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.TextStyle +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.unit.sp +import com.vitorpamplona.amethyst.R +import com.vitorpamplona.amethyst.model.Note +import com.vitorpamplona.amethyst.ui.actions.EditPostView +import com.vitorpamplona.amethyst.ui.components.GenericLoadable +import com.vitorpamplona.amethyst.ui.components.LoadNote +import com.vitorpamplona.amethyst.ui.components.TranslatableRichTextViewer +import com.vitorpamplona.amethyst.ui.navigation.routeFor +import com.vitorpamplona.amethyst.ui.note.NoteBody +import com.vitorpamplona.amethyst.ui.note.observeEdits +import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel +import com.vitorpamplona.amethyst.ui.theme.Size10dp +import com.vitorpamplona.amethyst.ui.theme.StdVertSpacer +import com.vitorpamplona.amethyst.ui.theme.imageModifier +import com.vitorpamplona.amethyst.ui.theme.innerPostModifier +import com.vitorpamplona.quartz.events.EmptyTagList +import com.vitorpamplona.quartz.events.TextNoteModificationEvent +import kotlinx.collections.immutable.persistentListOf + +@Composable +fun RenderTextModificationEvent( + note: Note, + makeItShort: Boolean, + canPreview: Boolean, + backgroundColor: MutableState, + accountViewModel: AccountViewModel, + nav: (String) -> Unit, +) { + val noteEvent = note.event as? TextNoteModificationEvent ?: return + val noteAuthor = note.author ?: return + + val wantsToEditPost = + remember { + mutableStateOf(false) + } + + val isAuthorTheLoggedUser = + remember { + val authorOfTheOriginalNote = + noteEvent.editedNote()?.let { accountViewModel.getNoteIfExists(it)?.author } + + mutableStateOf(accountViewModel.isLoggedUser(authorOfTheOriginalNote)) + } + + noteEvent.editedNote()?.let { + LoadNote(baseNoteHex = it, accountViewModel = accountViewModel) { baseOriginalNote -> + baseOriginalNote?.let { + } + } + } + + Card( + modifier = MaterialTheme.colorScheme.imageModifier, + ) { + Column(Modifier.fillMaxWidth().padding(Size10dp)) { + Text( + text = stringResource(id = R.string.proposal_to_edit), + style = + TextStyle( + fontSize = 18.sp, + fontWeight = FontWeight.Bold, + ), + ) + + Spacer(modifier = StdVertSpacer) + + noteEvent.summary()?.let { + TranslatableRichTextViewer( + content = it, + canPreview = canPreview && !makeItShort, + modifier = Modifier.fillMaxWidth(), + tags = EmptyTagList, + backgroundColor = backgroundColor, + accountViewModel = accountViewModel, + nav = nav, + ) + Spacer(modifier = StdVertSpacer) + } + + noteEvent.editedNote()?.let { + LoadNote(baseNoteHex = it, accountViewModel = accountViewModel) { baseNote -> + baseNote?.let { + val noteState by baseNote.live().metadata.observeAsState() + + val editStateOriginalNote = + observeEdits(baseNote = baseNote, accountViewModel = accountViewModel) + + val editState = + remember(note) { + derivedStateOf { + val loadable = + editStateOriginalNote.value as? GenericLoadable.Loaded + + val state = EditState() + + val latestChangeByAuthor = + if (loadable != null && loadable.loaded.hasModificationsToShow()) { + loadable.loaded.latestBefore(note.createdAt()) + } else { + null + } + + state.updateModifications( + listOfNotNull( + latestChangeByAuthor, + note, + ), + ) + + GenericLoadable.Loaded(state) + } + } + + LaunchedEffect(key1 = noteState) { + val newAuthor = accountViewModel.isLoggedUser(noteState?.note?.author) + + if (isAuthorTheLoggedUser.value != newAuthor) { + isAuthorTheLoggedUser.value = newAuthor + } + } + + Column( + modifier = + MaterialTheme.colorScheme.innerPostModifier.padding(Size10dp) + .clickable { + routeFor( + baseNote, + accountViewModel.userProfile(), + )?.let { nav(it) } + }, + ) { + NoteBody( + baseNote = baseNote, + showAuthorPicture = true, + unPackReply = false, + makeItShort = false, + canPreview = true, + showSecondRow = false, + backgroundColor = backgroundColor, + editState = editState, + accountViewModel = accountViewModel, + nav = nav, + ) + + if (wantsToEditPost.value) { + EditPostView( + onClose = { + wantsToEditPost.value = false + }, + edit = baseNote, + versionLookingAt = note, + accountViewModel = accountViewModel, + nav = nav, + ) + } + } + } + } + } + + if (isAuthorTheLoggedUser.value) { + Spacer(modifier = StdVertSpacer) + + Button( + onClick = { wantsToEditPost.value = true }, + modifier = Modifier.fillMaxWidth(), + ) { + Text(text = stringResource(id = R.string.accept_the_suggestion)) + } + } + } + } +} + +@Stable +class EditState() { + private var modificationsList: List = persistentListOf() + private var modificationToShowIndex: Int = -1 + + val modificationToShow: MutableState = mutableStateOf(null) + val showingVersion: MutableState = mutableStateOf(0) + + fun hasModificationsToShow(): Boolean = modificationsList.isNotEmpty() + + fun isOriginal(): Boolean = modificationToShowIndex < 0 + + fun isLatest(): Boolean = modificationToShowIndex == modificationsList.lastIndex + + fun originalVersionId() = 0 + + fun lastVersionId() = modificationsList.size + + fun versionId() = modificationToShowIndex + 1 + + fun latest() = modificationsList.lastOrNull() + + fun latestBefore(createdAt: Long?): Note? { + if (createdAt == null) return latest() + return modificationsList.lastOrNull { (it.createdAt() ?: Long.MAX_VALUE) < createdAt } + } + + fun nextModification() { + if (modificationToShowIndex < 0) { + modificationToShowIndex = 0 + modificationToShow.value = modificationsList.getOrNull(0) + } else { + modificationToShowIndex++ + if (modificationToShowIndex >= modificationsList.size) { + modificationToShowIndex = -1 + modificationToShow.value = null + } else { + modificationToShow.value = modificationsList.getOrNull(modificationToShowIndex) + } + } + + showingVersion.value = versionId() + } + + fun updateModifications(newModifications: List) { + if (modificationsList != newModifications) { + modificationsList = newModifications + + if (newModifications.isEmpty()) { + modificationToShow.value = null + modificationToShowIndex = -1 + } else { + modificationToShowIndex = newModifications.lastIndex + modificationToShow.value = newModifications.last() + } + } + + showingVersion.value = versionId() + } +} diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/note/types/Video.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/types/Video.kt new file mode 100644 index 000000000..e82869266 --- /dev/null +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/types/Video.kt @@ -0,0 +1,187 @@ +/** + * 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.types + +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.MutableState +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.layout.ContentScale +import androidx.compose.ui.platform.LocalUriHandler +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.unit.dp +import coil.compose.AsyncImage +import com.vitorpamplona.amethyst.R +import com.vitorpamplona.amethyst.commons.BaseMediaContent +import com.vitorpamplona.amethyst.commons.MediaUrlImage +import com.vitorpamplona.amethyst.commons.MediaUrlVideo +import com.vitorpamplona.amethyst.commons.RichTextParser +import com.vitorpamplona.amethyst.model.Note +import com.vitorpamplona.amethyst.ui.components.SensitivityWarning +import com.vitorpamplona.amethyst.ui.components.TranslatableRichTextViewer +import com.vitorpamplona.amethyst.ui.components.ZoomableContentView +import com.vitorpamplona.amethyst.ui.note.elements.DefaultImageHeader +import com.vitorpamplona.amethyst.ui.note.elements.DisplayUncitedHashtags +import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel +import com.vitorpamplona.amethyst.ui.theme.imageModifier +import com.vitorpamplona.quartz.events.EmptyTagList +import com.vitorpamplona.quartz.events.VideoEvent +import com.vitorpamplona.quartz.events.toImmutableListOfLists +import kotlinx.collections.immutable.toImmutableList + +@Composable +fun VideoDisplay( + note: Note, + makeItShort: Boolean, + canPreview: Boolean, + backgroundColor: MutableState, + accountViewModel: AccountViewModel, + nav: (String) -> Unit, +) { + val event = (note.event as? VideoEvent) ?: return + val fullUrl = event.url() ?: return + + val title = event.title() + val summary = event.content.ifBlank { null }?.takeIf { title != it } + val image = event.thumb() ?: event.image() + val isYouTube = fullUrl.contains("youtube.com") || fullUrl.contains("youtu.be") + val tags = remember(note) { note.event?.tags()?.toImmutableListOfLists() ?: EmptyTagList } + + val content by + remember(note) { + val blurHash = event.blurhash() + val hash = event.hash() + val dimensions = event.dimensions() + val description = event.content.ifBlank { null } ?: event.alt() + val isImage = RichTextParser.isImageUrl(fullUrl) + val uri = note.toNostrUri() + + mutableStateOf( + if (isImage) { + MediaUrlImage( + url = fullUrl, + description = description, + hash = hash, + blurhash = blurHash, + dim = dimensions, + uri = uri, + ) + } else { + MediaUrlVideo( + url = fullUrl, + description = description, + hash = hash, + dim = dimensions, + uri = uri, + authorName = note.author?.toBestDisplayName(), + artworkUri = event.thumb() ?: event.image(), + ) + }, + ) + } + + SensitivityWarning(note = note, accountViewModel = accountViewModel) { + Column( + modifier = + Modifier + .fillMaxWidth() + .padding(top = 5.dp), + horizontalAlignment = Alignment.CenterHorizontally, + ) { + if (isYouTube) { + val uri = LocalUriHandler.current + Row( + modifier = Modifier.clickable { runCatching { uri.openUri(fullUrl) } }, + ) { + image?.let { + AsyncImage( + model = it, + contentDescription = + stringResource( + R.string.preview_card_image_for, + it, + ), + contentScale = ContentScale.FillWidth, + modifier = MaterialTheme.colorScheme.imageModifier, + ) + } + ?: DefaultImageHeader(note, accountViewModel) + } + } else { + ZoomableContentView( + content = content, + roundedCorner = true, + accountViewModel = accountViewModel, + ) + } + + title?.let { + Text( + text = it, + fontWeight = FontWeight.Bold, + maxLines = 3, + overflow = TextOverflow.Ellipsis, + modifier = + Modifier + .fillMaxWidth() + .padding(top = 5.dp), + ) + } + + summary?.let { + TranslatableRichTextViewer( + content = it, + canPreview = canPreview && !makeItShort, + modifier = Modifier.fillMaxWidth(), + tags = tags, + backgroundColor = backgroundColor, + accountViewModel = accountViewModel, + nav = nav, + ) + } + + if (event.hasHashtags()) { + Row( + Modifier.fillMaxWidth(), + ) { + DisplayUncitedHashtags( + remember(event) { event.hashtags().toImmutableList() }, + summary ?: "", + nav, + ) + } + } + } + } +} diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/ChatroomFeedView.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/ChatroomFeedView.kt index da5cf1f55..89c5c6b52 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/ChatroomFeedView.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/ChatroomFeedView.kt @@ -27,7 +27,7 @@ import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.LazyListState import androidx.compose.foundation.lazy.itemsIndexed -import androidx.compose.material3.Divider +import androidx.compose.material3.HorizontalDivider import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect @@ -150,7 +150,7 @@ fun NewSubject(note: Note) { @Composable fun NewSubject(newSubject: String) { Row(verticalAlignment = Alignment.CenterVertically) { - Divider( + HorizontalDivider( modifier = Modifier.weight(1f), ) Text( @@ -159,7 +159,7 @@ fun NewSubject(newSubject: String) { fontSize = Font14SP, modifier = HalfPadding, ) - Divider( + HorizontalDivider( modifier = Modifier.weight(1f), ) } diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/ThreadFeedView.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/ThreadFeedView.kt index 9822f1d67..9ca328593 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/ThreadFeedView.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/ThreadFeedView.kt @@ -40,7 +40,7 @@ import androidx.compose.foundation.lazy.rememberLazyListState import androidx.compose.foundation.text.KeyboardOptions import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.MoreVert -import androidx.compose.material3.Divider +import androidx.compose.material3.HorizontalDivider import androidx.compose.material3.Icon import androidx.compose.material3.IconButton import androidx.compose.material3.LocalTextStyle @@ -90,53 +90,52 @@ import com.vitorpamplona.amethyst.ui.components.InlineCarrousel import com.vitorpamplona.amethyst.ui.components.LoadNote import com.vitorpamplona.amethyst.ui.components.ObserveDisplayNip05Status import com.vitorpamplona.amethyst.ui.components.mockAccountViewModel -import com.vitorpamplona.amethyst.ui.elements.DisplayFollowingCommunityInPost -import com.vitorpamplona.amethyst.ui.elements.DisplayFollowingHashtagsInPost -import com.vitorpamplona.amethyst.ui.elements.DisplayPoW -import com.vitorpamplona.amethyst.ui.elements.DisplayReward -import com.vitorpamplona.amethyst.ui.elements.DisplayZapSplits -import com.vitorpamplona.amethyst.ui.elements.Reward -import com.vitorpamplona.amethyst.ui.navigation.routeFor import com.vitorpamplona.amethyst.ui.navigation.routeToMessage -import com.vitorpamplona.amethyst.ui.note.AudioHeader -import com.vitorpamplona.amethyst.ui.note.AudioTrackHeader -import com.vitorpamplona.amethyst.ui.note.BadgeDisplay import com.vitorpamplona.amethyst.ui.note.BlankNote -import com.vitorpamplona.amethyst.ui.note.CreateImageHeader -import com.vitorpamplona.amethyst.ui.note.DisplayEditStatus -import com.vitorpamplona.amethyst.ui.note.DisplayHighlight -import com.vitorpamplona.amethyst.ui.note.DisplayLocation -import com.vitorpamplona.amethyst.ui.note.DisplayOts -import com.vitorpamplona.amethyst.ui.note.DisplayPeopleList -import com.vitorpamplona.amethyst.ui.note.DisplayRelaySet -import com.vitorpamplona.amethyst.ui.note.EditState -import com.vitorpamplona.amethyst.ui.note.FileHeaderDisplay -import com.vitorpamplona.amethyst.ui.note.FileStorageHeaderDisplay import com.vitorpamplona.amethyst.ui.note.HiddenNote import com.vitorpamplona.amethyst.ui.note.LoadAddressableNote -import com.vitorpamplona.amethyst.ui.note.LoadAndDisplayUser import com.vitorpamplona.amethyst.ui.note.NoteAuthorPicture import com.vitorpamplona.amethyst.ui.note.NoteCompose -import com.vitorpamplona.amethyst.ui.note.NoteDropDownMenu import com.vitorpamplona.amethyst.ui.note.NoteQuickActionMenu import com.vitorpamplona.amethyst.ui.note.NoteUsernameDisplay import com.vitorpamplona.amethyst.ui.note.ReactionsRow -import com.vitorpamplona.amethyst.ui.note.RenderAppDefinition -import com.vitorpamplona.amethyst.ui.note.RenderEmojiPack -import com.vitorpamplona.amethyst.ui.note.RenderFhirResource -import com.vitorpamplona.amethyst.ui.note.RenderGitIssueEvent -import com.vitorpamplona.amethyst.ui.note.RenderGitPatchEvent -import com.vitorpamplona.amethyst.ui.note.RenderGitRepositoryEvent -import com.vitorpamplona.amethyst.ui.note.RenderPinListEvent -import com.vitorpamplona.amethyst.ui.note.RenderPoll -import com.vitorpamplona.amethyst.ui.note.RenderPostApproval import com.vitorpamplona.amethyst.ui.note.RenderRepost -import com.vitorpamplona.amethyst.ui.note.RenderTextEvent -import com.vitorpamplona.amethyst.ui.note.RenderTextModificationEvent -import com.vitorpamplona.amethyst.ui.note.VideoDisplay +import com.vitorpamplona.amethyst.ui.note.elements.DefaultImageHeader +import com.vitorpamplona.amethyst.ui.note.elements.DisplayEditStatus +import com.vitorpamplona.amethyst.ui.note.elements.DisplayFollowingCommunityInPost +import com.vitorpamplona.amethyst.ui.note.elements.DisplayFollowingHashtagsInPost +import com.vitorpamplona.amethyst.ui.note.elements.DisplayLocation +import com.vitorpamplona.amethyst.ui.note.elements.DisplayOts +import com.vitorpamplona.amethyst.ui.note.elements.DisplayPoW +import com.vitorpamplona.amethyst.ui.note.elements.DisplayReward +import com.vitorpamplona.amethyst.ui.note.elements.DisplayZapSplits +import com.vitorpamplona.amethyst.ui.note.elements.ForkInformationRow +import com.vitorpamplona.amethyst.ui.note.elements.NoteDropDownMenu +import com.vitorpamplona.amethyst.ui.note.elements.Reward import com.vitorpamplona.amethyst.ui.note.observeEdits import com.vitorpamplona.amethyst.ui.note.showAmount import com.vitorpamplona.amethyst.ui.note.timeAgo +import com.vitorpamplona.amethyst.ui.note.types.AudioHeader +import com.vitorpamplona.amethyst.ui.note.types.AudioTrackHeader +import com.vitorpamplona.amethyst.ui.note.types.BadgeDisplay +import com.vitorpamplona.amethyst.ui.note.types.DisplayHighlight +import com.vitorpamplona.amethyst.ui.note.types.DisplayPeopleList +import com.vitorpamplona.amethyst.ui.note.types.DisplayRelaySet +import com.vitorpamplona.amethyst.ui.note.types.EditState +import com.vitorpamplona.amethyst.ui.note.types.FileHeaderDisplay +import com.vitorpamplona.amethyst.ui.note.types.FileStorageHeaderDisplay +import com.vitorpamplona.amethyst.ui.note.types.RenderAppDefinition +import com.vitorpamplona.amethyst.ui.note.types.RenderEmojiPack +import com.vitorpamplona.amethyst.ui.note.types.RenderFhirResource +import com.vitorpamplona.amethyst.ui.note.types.RenderGitIssueEvent +import com.vitorpamplona.amethyst.ui.note.types.RenderGitPatchEvent +import com.vitorpamplona.amethyst.ui.note.types.RenderGitRepositoryEvent +import com.vitorpamplona.amethyst.ui.note.types.RenderPinListEvent +import com.vitorpamplona.amethyst.ui.note.types.RenderPoll +import com.vitorpamplona.amethyst.ui.note.types.RenderPostApproval +import com.vitorpamplona.amethyst.ui.note.types.RenderTextEvent +import com.vitorpamplona.amethyst.ui.note.types.RenderTextModificationEvent +import com.vitorpamplona.amethyst.ui.note.types.VideoDisplay import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel import com.vitorpamplona.amethyst.ui.screen.loggedIn.ChannelHeader import com.vitorpamplona.amethyst.ui.screen.loggedIn.ThinSendButton @@ -625,7 +624,7 @@ fun NoteMaster( ReactionsRow(note, true, editState, accountViewModel, nav) - Divider( + HorizontalDivider( thickness = DividerThickness, ) } @@ -665,7 +664,7 @@ private fun RenderClassifiedsReaderForThread( ) } } else { - CreateImageHeader(note, accountViewModel) + DefaultImageHeader(note, accountViewModel) } Row( @@ -973,24 +972,3 @@ private fun RenderWikiHeaderForThread( } } } - -@Composable -fun ForkInformationRow( - originalVersion: Note, - modifier: Modifier = Modifier, - accountViewModel: AccountViewModel, - nav: (String) -> Unit, -) { - val noteState by originalVersion.live().metadata.observeAsState() - val note = noteState?.note ?: return - val author = note.author ?: return - val route = remember(note) { routeFor(note, accountViewModel.userProfile()) } - - if (route != null) { - Row(modifier) { - Text(stringResource(id = R.string.forked_from)) - Spacer(modifier = StdHorzSpacer) - LoadAndDisplayUser(author, route, nav) - } - } -} diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/ChannelScreen.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/ChannelScreen.kt index 86f9e325d..5f9b58238 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/ChannelScreen.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/ChannelScreen.kt @@ -50,10 +50,10 @@ import androidx.compose.material.icons.filled.EditNote import androidx.compose.material.icons.filled.Share import androidx.compose.material3.Button import androidx.compose.material3.ButtonDefaults -import androidx.compose.material3.Divider import androidx.compose.material3.DropdownMenu import androidx.compose.material3.DropdownMenuItem import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.HorizontalDivider import androidx.compose.material3.Icon import androidx.compose.material3.IconButton import androidx.compose.material3.LocalTextStyle @@ -121,18 +121,18 @@ import com.vitorpamplona.amethyst.ui.components.RobohashFallbackAsyncImage import com.vitorpamplona.amethyst.ui.components.SensitivityWarning import com.vitorpamplona.amethyst.ui.components.TranslatableRichTextViewer import com.vitorpamplona.amethyst.ui.components.ZoomableContentView -import com.vitorpamplona.amethyst.ui.elements.DisplayUncitedHashtags import com.vitorpamplona.amethyst.ui.navigation.routeFor import com.vitorpamplona.amethyst.ui.note.ChatroomMessageCompose import com.vitorpamplona.amethyst.ui.note.ClickableUserPicture import com.vitorpamplona.amethyst.ui.note.LikeReaction import com.vitorpamplona.amethyst.ui.note.LoadChannel -import com.vitorpamplona.amethyst.ui.note.MoreOptionsButton import com.vitorpamplona.amethyst.ui.note.NoteAuthorPicture import com.vitorpamplona.amethyst.ui.note.NoteUsernameDisplay import com.vitorpamplona.amethyst.ui.note.UserPicture import com.vitorpamplona.amethyst.ui.note.UsernameDisplay import com.vitorpamplona.amethyst.ui.note.ZapReaction +import com.vitorpamplona.amethyst.ui.note.elements.DisplayUncitedHashtags +import com.vitorpamplona.amethyst.ui.note.elements.MoreOptionsButton import com.vitorpamplona.amethyst.ui.note.timeAgo import com.vitorpamplona.amethyst.ui.note.timeAgoShort import com.vitorpamplona.amethyst.ui.screen.NostrChannelFeedViewModel @@ -640,7 +640,7 @@ fun ChannelHeader( } if (showBottomDiviser) { - Divider( + HorizontalDivider( thickness = DividerThickness, ) } diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/ChatroomListScreen.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/ChatroomListScreen.kt index d5c0b5985..be3c1f6f7 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/ChatroomListScreen.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/ChatroomListScreen.kt @@ -33,9 +33,9 @@ import androidx.compose.foundation.pager.HorizontalPager import androidx.compose.foundation.pager.rememberPagerState import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.MoreVert -import androidx.compose.material3.Divider import androidx.compose.material3.DropdownMenu import androidx.compose.material3.DropdownMenuItem +import androidx.compose.material3.HorizontalDivider import androidx.compose.material3.Icon import androidx.compose.material3.IconButton import androidx.compose.material3.MaterialTheme @@ -170,7 +170,7 @@ fun ChatroomListTwoPane( Box(Modifier.padding(Size20dp), contentAlignment = Alignment.Center) { ChannelFabColumn(accountViewModel, nav) } - Divider( + HorizontalDivider( modifier = Modifier.fillMaxHeight() // fill the max height .width(DividerThickness), diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/ChatroomScreen.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/ChatroomScreen.kt index 1e621e109..048738d96 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/ChatroomScreen.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/ChatroomScreen.kt @@ -45,7 +45,7 @@ import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.EditNote import androidx.compose.material.icons.filled.Send import androidx.compose.material3.Button -import androidx.compose.material3.Divider +import androidx.compose.material3.HorizontalDivider import androidx.compose.material3.Icon import androidx.compose.material3.IconButton import androidx.compose.material3.LocalTextStyle @@ -603,7 +603,7 @@ fun ChatroomHeader( } } - Divider( + HorizontalDivider( thickness = DividerThickness, ) } @@ -643,7 +643,7 @@ fun GroupChatroomHeader( } } - Divider( + HorizontalDivider( thickness = DividerThickness, ) } diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/GeoHashScreen.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/GeoHashScreen.kt index e1f4515b0..725154bbd 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/GeoHashScreen.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/GeoHashScreen.kt @@ -27,7 +27,7 @@ import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.fillMaxHeight import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding -import androidx.compose.material3.Divider +import androidx.compose.material3.HorizontalDivider import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.DisposableEffect @@ -157,7 +157,7 @@ fun GeoHashHeader( } } - Divider( + HorizontalDivider( thickness = DividerThickness, ) } diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/HashtagScreen.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/HashtagScreen.kt index de08249ff..2132d88ae 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/HashtagScreen.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/HashtagScreen.kt @@ -27,7 +27,7 @@ import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.fillMaxHeight import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding -import androidx.compose.material3.Divider +import androidx.compose.material3.HorizontalDivider import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.DisposableEffect @@ -160,7 +160,7 @@ fun HashtagHeader( } } - Divider( + HorizontalDivider( thickness = DividerThickness, ) } diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/HiddenUsersScreen.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/HiddenUsersScreen.kt index 73346dd8d..a2ff9649a 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/HiddenUsersScreen.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/HiddenUsersScreen.kt @@ -34,7 +34,7 @@ import androidx.compose.foundation.text.KeyboardOptions import androidx.compose.material3.Button import androidx.compose.material3.ButtonDefaults import androidx.compose.material3.Checkbox -import androidx.compose.material3.Divider +import androidx.compose.material3.HorizontalDivider import androidx.compose.material3.MaterialTheme import androidx.compose.material3.OutlinedTextField import androidx.compose.material3.ScrollableTabRow @@ -67,7 +67,7 @@ import androidx.lifecycle.distinctUntilChanged import androidx.lifecycle.map import androidx.lifecycle.viewmodel.compose.viewModel import com.vitorpamplona.amethyst.R -import com.vitorpamplona.amethyst.ui.elements.AddButton +import com.vitorpamplona.amethyst.ui.note.elements.AddButton import com.vitorpamplona.amethyst.ui.screen.NostrHiddenAccountsFeedViewModel import com.vitorpamplona.amethyst.ui.screen.NostrHiddenWordsFeedViewModel import com.vitorpamplona.amethyst.ui.screen.NostrSpammerAccountsFeedViewModel @@ -177,7 +177,7 @@ fun HiddenUsersScreen( edgePadding = 8.dp, selectedTabIndex = pagerState.currentPage, modifier = TabRowHeight, - divider = { Divider(thickness = DividerThickness) }, + divider = { HorizontalDivider(thickness = DividerThickness) }, ) { Tab( selected = pagerState.currentPage == 0, @@ -313,7 +313,7 @@ fun MutedWordHeader( } } - Divider( + HorizontalDivider( thickness = DividerThickness, ) } diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/NotificationScreen.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/NotificationScreen.kt index 6e6e5bc81..2ba1c7532 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/NotificationScreen.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/NotificationScreen.kt @@ -27,7 +27,7 @@ import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.fillMaxHeight import androidx.compose.foundation.layout.padding -import androidx.compose.material3.Divider +import androidx.compose.material3.HorizontalDivider import androidx.compose.material3.MaterialTheme import androidx.compose.runtime.Composable import androidx.compose.runtime.DisposableEffect @@ -230,7 +230,7 @@ fun SummaryBar(model: UserReactionsViewModel) { } } - Divider( + HorizontalDivider( thickness = DividerThickness, ) } diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/ProfileScreen.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/ProfileScreen.kt index 094682cbe..d12f2de31 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/ProfileScreen.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/ProfileScreen.kt @@ -57,9 +57,9 @@ import androidx.compose.material.icons.filled.Link import androidx.compose.material.icons.filled.MoreVert import androidx.compose.material3.Button import androidx.compose.material3.ButtonDefaults -import androidx.compose.material3.Divider import androidx.compose.material3.DropdownMenu import androidx.compose.material3.DropdownMenuItem +import androidx.compose.material3.HorizontalDivider import androidx.compose.material3.Icon import androidx.compose.material3.IconButton import androidx.compose.material3.LocalTextStyle @@ -476,7 +476,7 @@ private fun RenderScreen( selectedTabIndex = pagerState.currentPage, edgePadding = 8.dp, modifier = tabRowModifier, - divider = { Divider(thickness = DividerThickness) }, + divider = { HorizontalDivider(thickness = DividerThickness) }, ) { CreateAndRenderTabs(baseUser, pagerState) } @@ -819,7 +819,7 @@ private fun ProfileHeader( DrawAdditionalInfo(baseUser, appRecommendations, accountViewModel, nav) - Divider(modifier = Modifier.padding(top = 6.dp)) + HorizontalDivider(modifier = Modifier.padding(top = 6.dp)) } } @@ -1870,7 +1870,7 @@ fun UserProfileDropDownMenu( ) if (accountViewModel.userProfile() != user) { - Divider() + HorizontalDivider(thickness = DividerThickness) if (accountViewModel.account.isHidden(user)) { DropdownMenuItem( text = { Text(stringResource(R.string.unblock_user)) }, @@ -1888,7 +1888,7 @@ fun UserProfileDropDownMenu( }, ) } - Divider() + HorizontalDivider(thickness = DividerThickness) DropdownMenuItem( text = { Text(stringResource(id = R.string.report_spam_scam)) }, onClick = { diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/ReportNoteDialog.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/ReportNoteDialog.kt index 69c921b61..4186d0720 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/ReportNoteDialog.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/ReportNoteDialog.kt @@ -33,8 +33,8 @@ import androidx.compose.material.icons.filled.Block import androidx.compose.material.icons.filled.Report import androidx.compose.material3.Button import androidx.compose.material3.ButtonDefaults -import androidx.compose.material3.Divider import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.HorizontalDivider import androidx.compose.material3.Icon import androidx.compose.material3.IconButton import androidx.compose.material3.MaterialTheme @@ -61,6 +61,7 @@ import androidx.compose.ui.window.DialogProperties import com.vitorpamplona.amethyst.R import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.ui.note.ArrowBackIcon +import com.vitorpamplona.amethyst.ui.theme.DividerThickness import com.vitorpamplona.amethyst.ui.theme.WarningColor import com.vitorpamplona.quartz.events.ReportEvent import kotlinx.collections.immutable.toImmutableList @@ -126,7 +127,7 @@ fun ReportNoteDialog( ) SpacerH16() - Divider(color = MaterialTheme.colorScheme.onSurface, thickness = 0.25.dp) + HorizontalDivider(color = MaterialTheme.colorScheme.onSurface, thickness = DividerThickness) SpacerH16() SectionHeader(text = stringResource(R.string.report_dialog_report_btn)) diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/SearchScreen.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/SearchScreen.kt index 57cc6bbca..437c0e12b 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/SearchScreen.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/SearchScreen.kt @@ -36,8 +36,8 @@ import androidx.compose.foundation.lazy.itemsIndexed import androidx.compose.foundation.lazy.rememberLazyListState import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.text.KeyboardOptions -import androidx.compose.material3.Divider import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.HorizontalDivider import androidx.compose.material3.IconButton import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text @@ -446,7 +446,7 @@ fun HashtagLine( } } - Divider( + HorizontalDivider( modifier = Modifier.padding(top = 10.dp), thickness = DividerThickness, ) @@ -481,7 +481,7 @@ fun UserLine( } } - Divider( + HorizontalDivider( modifier = Modifier.padding(top = 10.dp), thickness = DividerThickness, ) diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/VideoScreen.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/VideoScreen.kt index 06fb614be..59eb824e3 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/VideoScreen.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/VideoScreen.kt @@ -70,18 +70,18 @@ import com.vitorpamplona.amethyst.ui.actions.NewPostView import com.vitorpamplona.amethyst.ui.components.ObserveDisplayNip05Status import com.vitorpamplona.amethyst.ui.navigation.routeFor import com.vitorpamplona.amethyst.ui.note.BoostReaction -import com.vitorpamplona.amethyst.ui.note.FileHeaderDisplay -import com.vitorpamplona.amethyst.ui.note.FileStorageHeaderDisplay import com.vitorpamplona.amethyst.ui.note.HiddenNote import com.vitorpamplona.amethyst.ui.note.LikeReaction import com.vitorpamplona.amethyst.ui.note.NoteAuthorPicture -import com.vitorpamplona.amethyst.ui.note.NoteDropDownMenu import com.vitorpamplona.amethyst.ui.note.NoteUsernameDisplay import com.vitorpamplona.amethyst.ui.note.RenderRelay import com.vitorpamplona.amethyst.ui.note.ReplyReaction import com.vitorpamplona.amethyst.ui.note.ViewCountReaction import com.vitorpamplona.amethyst.ui.note.WatchForReports import com.vitorpamplona.amethyst.ui.note.ZapReaction +import com.vitorpamplona.amethyst.ui.note.elements.NoteDropDownMenu +import com.vitorpamplona.amethyst.ui.note.types.FileHeaderDisplay +import com.vitorpamplona.amethyst.ui.note.types.FileStorageHeaderDisplay import com.vitorpamplona.amethyst.ui.screen.FeedEmpty import com.vitorpamplona.amethyst.ui.screen.FeedError import com.vitorpamplona.amethyst.ui.screen.FeedState diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/theme/Shape.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/theme/Shape.kt index 8b39b9e70..585516526 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/theme/Shape.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/theme/Shape.kt @@ -193,6 +193,10 @@ val IconRowModifier = Modifier.fillMaxWidth().padding(vertical = 15.dp, horizont val emptyLineItemModifier = Modifier.height(Size75dp).fillMaxWidth() +val imageHeaderBannerSize = Modifier.fillMaxWidth().height(150.dp) + +val authorNotePictureForImageHeader = Modifier.size(75.dp).padding(10.dp) + val normalNoteModifier = Modifier.fillMaxWidth() .padding( diff --git a/app/src/play/java/com/vitorpamplona/amethyst/ui/components/TranslatableRichTextViewer.kt b/app/src/play/java/com/vitorpamplona/amethyst/ui/components/TranslatableRichTextViewer.kt index 41f4eb250..5c1111c69 100644 --- a/app/src/play/java/com/vitorpamplona/amethyst/ui/components/TranslatableRichTextViewer.kt +++ b/app/src/play/java/com/vitorpamplona/amethyst/ui/components/TranslatableRichTextViewer.kt @@ -31,9 +31,9 @@ import androidx.compose.foundation.layout.size import androidx.compose.foundation.text.ClickableText import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.Check -import androidx.compose.material3.Divider import androidx.compose.material3.DropdownMenu import androidx.compose.material3.DropdownMenuItem +import androidx.compose.material3.HorizontalDivider import androidx.compose.material3.Icon import androidx.compose.material3.LocalTextStyle import androidx.compose.material3.MaterialTheme @@ -60,6 +60,7 @@ import androidx.core.os.ConfigurationCompat import com.vitorpamplona.amethyst.R import com.vitorpamplona.amethyst.service.lang.LanguageTranslatorService import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel +import com.vitorpamplona.amethyst.ui.theme.DividerThickness import com.vitorpamplona.amethyst.ui.theme.lessImportantLink import com.vitorpamplona.quartz.events.ImmutableListOfLists import kotlinx.coroutines.Dispatchers @@ -243,7 +244,7 @@ private fun TranslationMessage( } }, ) - Divider() + HorizontalDivider(thickness = DividerThickness) DropdownMenuItem( text = { if (accountViewModel.account.preferenceBetween(source, target) == source) { @@ -300,7 +301,7 @@ private fun TranslationMessage( } }, ) - Divider() + HorizontalDivider(thickness = DividerThickness) val languageList = ConfigurationCompat.getLocales(Resources.getSystem().configuration) for (i in 0 until languageList.size()) { From f6ffb87e20c4bfc508cb2992a888d8c7d602c3c7 Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Tue, 5 Mar 2024 16:09:50 -0500 Subject: [PATCH 90/98] Avoids allocating Strings for the JSON conversion when checking if a filter has changed. --- .../amethyst/service/NostrDataSource.kt | 6 +- .../amethyst/service/relays/Subscription.kt | 43 +++++++----- .../amethyst/service/relays/TypedFilter.kt | 68 +------------------ .../amethyst/ui/screen/FeedView.kt | 3 +- 4 files changed, 32 insertions(+), 88 deletions(-) diff --git a/app/src/main/java/com/vitorpamplona/amethyst/service/NostrDataSource.kt b/app/src/main/java/com/vitorpamplona/amethyst/service/NostrDataSource.kt index 47cb0af5d..0a1623e01 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/service/NostrDataSource.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/service/NostrDataSource.kt @@ -221,7 +221,7 @@ abstract class NostrDataSource(val debugName: String) { // saves the channels that are currently active val activeSubscriptions = subscriptions.values.filter { it.typedFilters != null } // saves the current content to only update if it changes - val currentFilters = activeSubscriptions.associate { it.id to it.toJson() } + val currentFilters = activeSubscriptions.associate { it.id to it.typedFilters } changingFilters.getAndSet(true) @@ -245,7 +245,7 @@ abstract class NostrDataSource(val debugName: String) { Client.close(updatedSubscription.id) } else { // was active and is still active, check if it has changed. - if (updatedSubscription.toJson() != currentFilters[updatedSubscription.id]) { + if (updatedSubscription.hasChangedFiltersFrom(currentFilters[updatedSubscription.id])) { Client.close(updatedSubscription.id) if (active) { Client.sendFilter(updatedSubscription.id, updatedSubscriptionNewFilters) @@ -265,7 +265,7 @@ abstract class NostrDataSource(val debugName: String) { // was not active and is still not active, does nothing } else { // was not active and becomes active, sends the filter. - if (updatedSubscription.toJson() != currentFilters[updatedSubscription.id]) { + if (updatedSubscription.hasChangedFiltersFrom(currentFilters[updatedSubscription.id])) { if (active) { Log.d( this@NostrDataSource.javaClass.simpleName, diff --git a/app/src/main/java/com/vitorpamplona/amethyst/service/relays/Subscription.kt b/app/src/main/java/com/vitorpamplona/amethyst/service/relays/Subscription.kt index 9de8de758..ebd0ce388 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/service/relays/Subscription.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/service/relays/Subscription.kt @@ -20,8 +20,6 @@ */ package com.vitorpamplona.amethyst.service.relays -import com.fasterxml.jackson.databind.JsonNode -import com.vitorpamplona.quartz.events.Event import java.util.UUID data class Subscription( @@ -37,23 +35,36 @@ data class Subscription( onEOSE?.let { it(time, relay) } } - fun toJson(): String { - return Event.mapper.writeValueAsString(toJsonObject()) - } + fun hasChangedFiltersFrom(otherFilters: List?): Boolean { + if (typedFilters == null && otherFilters == null) return false + if (typedFilters?.size != otherFilters?.size) return true - fun toJsonObject(): JsonNode { - val factory = Event.mapper.nodeFactory + typedFilters?.forEachIndexed { index, typedFilter -> + val otherFilter = otherFilters?.getOrNull(index) ?: return true - return factory.objectNode().apply { - put("id", id) - typedFilters?.also { filters -> - replace( - "typedFilters", - factory.arrayNode(filters.size).apply { - filters.forEach { filter -> add(filter.toJsonObject()) } - }, - ) + // Does not check SINCE on purpose. Avoids replacing the filter if SINCE was all that changed. + // fast check + if (typedFilter.filter.authors?.size != otherFilter.filter.authors?.size || + typedFilter.filter.ids?.size != otherFilter.filter.ids?.size || + typedFilter.filter.tags?.size != otherFilter.filter.tags?.size || + typedFilter.filter.kinds?.size != otherFilter.filter.kinds?.size || + typedFilter.filter.limit != otherFilter.filter.limit || + typedFilter.filter.search?.length != otherFilter.filter.search?.length || + typedFilter.filter.until != otherFilter.filter.until + ) { + return true + } + + // deep check + if (typedFilter.filter.ids != otherFilter.filter.ids || + typedFilter.filter.authors != otherFilter.filter.authors || + typedFilter.filter.tags != otherFilter.filter.tags || + typedFilter.filter.kinds != otherFilter.filter.kinds || + typedFilter.filter.search != otherFilter.filter.search + ) { + return true } } + return false } } diff --git a/app/src/main/java/com/vitorpamplona/amethyst/service/relays/TypedFilter.kt b/app/src/main/java/com/vitorpamplona/amethyst/service/relays/TypedFilter.kt index ec3c9e05c..4817e7b30 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/service/relays/TypedFilter.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/service/relays/TypedFilter.kt @@ -20,73 +20,7 @@ */ package com.vitorpamplona.amethyst.service.relays -import com.fasterxml.jackson.databind.JsonNode -import com.fasterxml.jackson.databind.node.ArrayNode -import com.vitorpamplona.quartz.events.Event - class TypedFilter( val types: Set, val filter: JsonFilter, -) { - fun toJson(): String { - return Event.mapper.writeValueAsString(toJsonObject()) - } - - fun toJsonObject(): JsonNode { - val factory = Event.mapper.nodeFactory - - return factory.objectNode().apply { - replace("types", typesToJson(types)) - replace("filter", filterToJson(filter)) - } - } - - fun typesToJson(types: Set): ArrayNode { - val factory = Event.mapper.nodeFactory - return factory.arrayNode(types.size).apply { types.forEach { add(it.name.lowercase()) } } - } - - fun filterToJson(filter: JsonFilter): JsonNode { - val factory = Event.mapper.nodeFactory - return factory.objectNode().apply { - filter.ids?.run { - replace( - "ids", - factory.arrayNode(filter.ids.size).apply { filter.ids.forEach { add(it) } }, - ) - } - filter.authors?.run { - replace( - "authors", - factory.arrayNode(filter.authors.size).apply { filter.authors.forEach { add(it) } }, - ) - } - filter.kinds?.run { - replace( - "kinds", - factory.arrayNode(filter.kinds.size).apply { filter.kinds.forEach { add(it) } }, - ) - } - filter.tags?.run { - entries.forEach { kv -> - replace( - "#${kv.key}", - factory.arrayNode(kv.value.size).apply { kv.value.forEach { add(it) } }, - ) - } - } - /* - Does not include since in the json comparison - filter.since?.run { - val jsonObjectSince = JsonObject() - entries.forEach { sincePairs -> - jsonObjectSince.addProperty(sincePairs.key, "${sincePairs.value}") - } - jsonObject.add("since", jsonObjectSince) - }*/ - filter.until?.run { put("until", filter.until) } - filter.limit?.run { put("limit", filter.limit) } - filter.search?.run { put("search", filter.search) } - } - } -} +) diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/FeedView.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/FeedView.kt index 920977642..72e389c3a 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/FeedView.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/FeedView.kt @@ -58,7 +58,6 @@ import com.vitorpamplona.amethyst.ui.note.NoteCompose import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel import com.vitorpamplona.amethyst.ui.theme.FeedPadding import com.vitorpamplona.amethyst.ui.theme.StdVertSpacer -import kotlin.time.ExperimentalTime @Composable fun RefresheableFeedView( @@ -205,7 +204,7 @@ private fun WatchScrollToTop( } } -@OptIn(ExperimentalFoundationApi::class, ExperimentalTime::class) +@OptIn(ExperimentalFoundationApi::class) @Composable private fun FeedLoaded( state: FeedState.Loaded, From dc3574bb6e74e9f075d6d5e003d117ea963c6863 Mon Sep 17 00:00:00 2001 From: Crowdin Bot Date: Tue, 5 Mar 2024 21:12:16 +0000 Subject: [PATCH 91/98] New Crowdin translations by GitHub Action --- app/src/main/res/values-hu/strings.xml | 41 ++++++++++++++++++++++++++ 1 file changed, 41 insertions(+) diff --git a/app/src/main/res/values-hu/strings.xml b/app/src/main/res/values-hu/strings.xml index d4715f21a..ce13985fd 100644 --- a/app/src/main/res/values-hu/strings.xml +++ b/app/src/main/res/values-hu/strings.xml @@ -6,6 +6,7 @@ Profilkép QR kód beolvasása Mutasd + Ez a bejegyzés el lett rejtve, mert említ rejtett felhasználóidat vagy szavaidat A bejegyzést nem megfelelőként jelölte meg Bejegyzés nem található Csatorna Kép @@ -16,6 +17,7 @@ Spam Megszemélyesítés Illegális viselkedés + Egyéb Ismeretlen Csomópont Ikon Ismeretlen Szerző @@ -23,6 +25,9 @@ A felhasználó PubKulcs másolása A bejegyzés azonosítójának másolása Közvetít + Időbélyegezni + Időbélyeg: függőben lévő megerősítések + OTS: Függőben Törlés Kérése Tiltás és Jelentés @@ -43,7 +48,12 @@ Megtekintések száma Megosztás Megosztva + szerkesztve + szerkesztés #%1$s + eredeti Idézet + Fork + Szerkesztési javaslat Új összeg sats-ban Hozzáad "válasz erre " @@ -647,4 +657,35 @@ Az npub megjelenítése QR-kódként Érvénytelen cím Az Amethyst kapott egy URI-t a megnyitáshoz, de az érvénytelen volt: %1$s + Zap a fejlesztőknek! + Az adományod hozzájárul ahhoz, hogy változást érjünk el. Minden sat számít! + Adományozz Most + hozta neked: + Ezt a verziót hozta neked: + %1$s verzió + Köszönöm! + Max Limit + Korlátozott írások + Fork-olva innen + FORK + Git-tár: %1$s + Web: + Klónoz: + OTS: %1$s + Időbélyeg igazolás + Bizonyíték van arra, hogy ezt a bejegyzést valamikor %1$s előtt írták alá. A bizonyítékot ezen a napon és időpontban a Bitcoin blokkláncába bélyegezték. + Bejegyzés szerkesztése + Javaslat egy bejegyzés javítására + Változások összefoglalása + Gyors javítások… + Fogadd el a Javaslatot + Letöltés + Feliratozás bekapcsolva + Feliratozás kikapcsolva + Lezárt üzenetek kikapcsolva. Kattints a lezárt üzenetek bekapcsolásához + Lezárt üzenetek bekapcsolva. Kattints a lezárt üzenetek kikapcsolásához + Küldés + Felhasználónév lejátszása hangként + QR-kód beolvasása + Keresd fel a harmadik féltől származó pénztárcaszolgáltatót, az Alby-t From 0dd553ae55bfbc1417365fc32698e03ff43f506a Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Tue, 5 Mar 2024 16:45:41 -0500 Subject: [PATCH 92/98] Fixes search on binary content. --- .../main/java/com/vitorpamplona/amethyst/model/LocalCache.kt | 2 ++ .../java/com/vitorpamplona/amethyst/ui/components/VideoView.kt | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) 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 76ba4fade..aa25df2be 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/model/LocalCache.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/model/LocalCache.kt @@ -1655,6 +1655,8 @@ object LocalCache { it.event !is CommunityPostApprovalEvent && it.event !is ReactionEvent && it.event !is GiftWrapEvent && + it.event !is SealedGossipEvent && + it.event !is OtsEvent && it.event !is LnZapEvent && it.event !is LnZapRequestEvent ) && diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/components/VideoView.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/components/VideoView.kt index 40001604b..3d7e00abc 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/components/VideoView.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/components/VideoView.kt @@ -282,7 +282,7 @@ fun VideoView( } @Composable -@androidx.annotation.OptIn(androidx.media3.common.util.UnstableApi::class) +@OptIn(androidx.media3.common.util.UnstableApi::class) fun VideoViewInner( videoUri: String, defaultToStart: Boolean = false, From da41fbb4c993a9e23d8b0d4b4c5a78f50d8920bf Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Tue, 5 Mar 2024 19:05:03 -0500 Subject: [PATCH 93/98] Updates secp256k1KmpJniAndroid --- gradle/libs.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index ea4f8fd15..6ac65c0e2 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -33,7 +33,7 @@ mockk = "1.13.10" navigationCompose = "2.7.7" okhttp = "5.0.0-alpha.12" runner = "1.5.2" -secp256k1KmpJniAndroid = "0.14.0" +secp256k1KmpJniAndroid = "0.15.0" securityCryptoKtx = "1.1.0-alpha06" spotless = "6.25.0" translate = "17.0.2" From b8619e3b6118e69d559a7bc8647f384cdd04ea95 Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Wed, 6 Mar 2024 10:32:08 -0500 Subject: [PATCH 94/98] - User Metadata clean up upon receipt instead of in every rendering. - Simpler/Faster UserDisplay renderings - Removes co-routing use for Hashtags. --- .../com/vitorpamplona/amethyst/model/User.kt | 1 + .../amethyst/ui/note/NoteCompose.kt | 14 ++- .../amethyst/ui/note/UsernameDisplay.kt | 96 ++----------------- .../ui/note/elements/DisplayHashtags.kt | 12 +-- .../quartz/events/ContactListEvent.kt | 35 ++++++- 5 files changed, 49 insertions(+), 109 deletions(-) diff --git a/app/src/main/java/com/vitorpamplona/amethyst/model/User.kt b/app/src/main/java/com/vitorpamplona/amethyst/model/User.kt index 30ded640a..52ccbacd5 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/model/User.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/model/User.kt @@ -310,6 +310,7 @@ class User(val pubkeyHex: String) { info?.latestMetadata = latestMetadata info?.updatedMetadataAt = latestMetadata.createdAt info?.tags = latestMetadata.tags.toImmutableListOfLists() + info?.cleanBlankNames() if (newUserInfo.lud16.isNullOrBlank()) { info?.lud06?.let { 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 9ad4e2e6d..44687bac2 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 @@ -76,6 +76,7 @@ import com.vitorpamplona.amethyst.ui.note.elements.DisplayZapSplits import com.vitorpamplona.amethyst.ui.note.elements.MoreOptionsButton import com.vitorpamplona.amethyst.ui.note.elements.Reward import com.vitorpamplona.amethyst.ui.note.elements.ShowForkInformation +import com.vitorpamplona.amethyst.ui.note.elements.TimeAgo import com.vitorpamplona.amethyst.ui.note.types.BadgeDisplay import com.vitorpamplona.amethyst.ui.note.types.CommunityHeader import com.vitorpamplona.amethyst.ui.note.types.DisplayPeopleList @@ -1121,11 +1122,8 @@ fun FirstUserInfoRow( accountViewModel: AccountViewModel, nav: (String) -> Unit, ) { - Row(verticalAlignment = CenterVertically, modifier = remember { UserNameRowHeight }) { - val isRepost by - remember(baseNote) { - derivedStateOf { baseNote.event is RepostEvent || baseNote.event is GenericRepostEvent } - } + Row(verticalAlignment = CenterVertically, modifier = UserNameRowHeight) { + val isRepost = baseNote.event is RepostEvent || baseNote.event is GenericRepostEvent val isCommunityPost by remember(baseNote) { @@ -1139,9 +1137,9 @@ fun FirstUserInfoRow( if (showAuthorPicture) { NoteAuthorPicture(baseNote, nav, accountViewModel, Size25dp) Spacer(HalfPadding) - NoteUsernameDisplay(baseNote, remember { Modifier.weight(1f) }, textColor = textColor) + NoteUsernameDisplay(baseNote, Modifier.weight(1f), textColor = textColor) } else { - NoteUsernameDisplay(baseNote, remember { Modifier.weight(1f) }, textColor = textColor) + NoteUsernameDisplay(baseNote, Modifier.weight(1f), textColor = textColor) } if (isRepost) { @@ -1158,7 +1156,7 @@ fun FirstUserInfoRow( } } - com.vitorpamplona.amethyst.ui.note.elements.TimeAgo(baseNote) + TimeAgo(baseNote) MoreOptionsButton(baseNote, editState, accountViewModel, nav) } diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/note/UsernameDisplay.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/UsernameDisplay.kt index 3c2e42730..7fd4de335 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/note/UsernameDisplay.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/UsernameDisplay.kt @@ -29,7 +29,6 @@ import androidx.compose.material3.IconButton import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text import androidx.compose.runtime.Composable -import androidx.compose.runtime.derivedStateOf import androidx.compose.runtime.getValue import androidx.compose.runtime.livedata.observeAsState import androidx.compose.runtime.remember @@ -72,67 +71,27 @@ fun UsernameDisplay( fontWeight: FontWeight = FontWeight.Bold, textColor: Color = Color.Unspecified, ) { - val npubDisplay by remember { derivedStateOf { baseUser.pubkeyDisplayHex() } } - val userMetadata by baseUser.live().userMetadataInfo.observeAsState(baseUser.info) Crossfade(targetState = userMetadata, modifier = weight, label = "UsernameDisplay") { - if (it != null) { - UserNameDisplay( - it.bestUsername(), - it.bestDisplayName(), - npubDisplay, - it.tags, - weight, - showPlayButton, - fontWeight, - textColor, - ) + val name = it?.bestDisplayName() ?: it?.bestUsername() + if (name != null) { + UserDisplay(name, it?.tags, weight, showPlayButton, fontWeight, textColor) } else { - NPubDisplay(npubDisplay, weight, fontWeight, textColor) + NPubDisplay(baseUser, weight, fontWeight, textColor) } } } @Composable -private fun UserNameDisplay( - bestUserName: String?, - bestDisplayName: String?, - npubDisplay: String, - tags: ImmutableListOfLists?, - modifier: Modifier, - showPlayButton: Boolean = true, - fontWeight: FontWeight = FontWeight.Bold, - textColor: Color = Color.Unspecified, -) { - if (bestUserName != null && bestDisplayName != null && bestDisplayName != bestUserName) { - UserAndUsernameDisplay( - bestDisplayName.trim(), - tags, - bestUserName.trim(), - modifier, - showPlayButton, - fontWeight, - textColor, - ) - } else if (bestDisplayName != null) { - UserDisplay(bestDisplayName.trim(), tags, modifier, showPlayButton, fontWeight, textColor) - } else if (bestUserName != null) { - UserDisplay(bestUserName.trim(), tags, modifier, showPlayButton, fontWeight, textColor) - } else { - NPubDisplay(npubDisplay, modifier, fontWeight, textColor) - } -} - -@Composable -fun NPubDisplay( - npubDisplay: String, +private fun NPubDisplay( + user: User, modifier: Modifier, fontWeight: FontWeight = FontWeight.Bold, textColor: Color = Color.Unspecified, ) { Text( - text = npubDisplay, + text = remember { user.pubkeyDisplayHex() }, fontWeight = fontWeight, modifier = modifier, maxLines = 1, @@ -160,41 +119,7 @@ private fun UserDisplay( modifier = modifier, color = textColor, ) - if (showPlayButton) { - Spacer(StdHorzSpacer) - DrawPlayName(bestDisplayName) - } - } -} -@Composable -private fun UserAndUsernameDisplay( - bestDisplayName: String, - tags: ImmutableListOfLists?, - bestUserName: String, - modifier: Modifier, - showPlayButton: Boolean = true, - fontWeight: FontWeight = FontWeight.Bold, - textColor: Color = Color.Unspecified, -) { - Row(modifier = modifier, verticalAlignment = Alignment.CenterVertically) { - CreateTextWithEmoji( - text = bestDisplayName, - tags = tags, - fontWeight = fontWeight, - maxLines = 1, - modifier = modifier, - color = textColor, - ) - /* - CreateTextWithEmoji( - text = remember { "@$bestUserName" }, - tags = tags, - color = MaterialTheme.colorScheme.placeholderText, - maxLines = 1, - overflow = TextOverflow.Ellipsis, - - )*/ if (showPlayButton) { Spacer(StdHorzSpacer) DrawPlayName(bestDisplayName) @@ -207,12 +132,7 @@ fun DrawPlayName(name: String) { val context = LocalContext.current val lifecycleOwner = LocalLifecycleOwner.current - DrawPlayNameIcon { speak(name, context, lifecycleOwner) } -} - -@Composable -fun DrawPlayNameIcon(onClick: () -> Unit) { - IconButton(onClick = onClick, modifier = StdButtonSizeModifier) { + IconButton(onClick = { speak(name, context, lifecycleOwner) }, modifier = StdButtonSizeModifier) { PlayIcon( modifier = StdButtonSizeModifier, tint = MaterialTheme.colorScheme.placeholderText, diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/note/elements/DisplayHashtags.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/elements/DisplayHashtags.kt index bd286ea84..54d3d424c 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/note/elements/DisplayHashtags.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/elements/DisplayHashtags.kt @@ -37,8 +37,6 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.text.AnnotatedString import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.launch @Composable fun DisplayFollowingHashtagsInPost( @@ -50,13 +48,11 @@ fun DisplayFollowingHashtagsInPost( var firstTag by remember(baseNote) { mutableStateOf(null) } LaunchedEffect(key1 = userFollowState) { - launch(Dispatchers.Default) { - val followingTags = userFollowState?.user?.cachedFollowingTagSet() ?: emptySet() - val newFirstTag = baseNote.event?.firstIsTaggedHashes(followingTags) + val followingTags = userFollowState?.user?.cachedFollowingTagSet() ?: emptySet() + val newFirstTag = baseNote.event?.firstIsTaggedHashes(followingTags) - if (firstTag != newFirstTag) { - launch(Dispatchers.Main) { firstTag = newFirstTag } - } + if (firstTag != newFirstTag) { + firstTag = newFirstTag } } diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/events/ContactListEvent.kt b/quartz/src/main/java/com/vitorpamplona/quartz/events/ContactListEvent.kt index e57df43d8..b46607c12 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/events/ContactListEvent.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/events/ContactListEvent.kt @@ -429,25 +429,50 @@ class UserMetadata { } fun lnAddress(): String? { - return (lud16?.trim() ?: lud06?.trim())?.ifBlank { null } + return lud16 ?: lud06 } fun bestUsername(): String? { - return name?.ifBlank { null } ?: username?.ifBlank { null } + return name ?: username } fun bestDisplayName(): String? { - return displayName?.ifBlank { null } + return displayName } fun nip05(): String? { - return nip05?.ifBlank { null } + return nip05 } fun profilePicture(): String? { - if (picture.isNullOrBlank()) picture = null return picture } + + fun cleanBlankNames() { + if (picture?.isNotEmpty() == true) picture = picture?.trim() + if (nip05?.isNotEmpty() == true) nip05 = nip05?.trim() + if (displayName?.isNotEmpty() == true) displayName = displayName?.trim() + if (name?.isNotEmpty() == true) name = name?.trim() + if (username?.isNotEmpty() == true) username = username?.trim() + if (lud06?.isNotEmpty() == true) lud06 = lud06?.trim() + if (lud16?.isNotEmpty() == true) lud16 = lud16?.trim() + + if (banner?.isNotEmpty() == true) banner = banner?.trim() + if (website?.isNotEmpty() == true) website = website?.trim() + if (domain?.isNotEmpty() == true) domain = domain?.trim() + + if (picture?.isBlank() == true) picture = null + if (nip05?.isBlank() == true) nip05 = null + if (displayName?.isBlank() == true) displayName = null + if (name?.isBlank() == true) name = null + if (username?.isBlank() == true) username = null + if (lud06?.isBlank() == true) lud06 = null + if (lud16?.isBlank() == true) lud16 = null + + if (banner?.isBlank() == true) banner = null + if (website?.isBlank() == true) website = null + if (domain?.isBlank() == true) domain = null + } } @Stable class ImmutableListOfLists(val lists: Array>) From 5f76cdf721e475733a829305bdbcc59217f0ba49 Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Wed, 6 Mar 2024 11:31:03 -0500 Subject: [PATCH 95/98] Center the text of notes that couldn't be found. --- .../vitorpamplona/amethyst/ui/note/BadgeCompose.kt | 2 +- .../vitorpamplona/amethyst/ui/note/BlankNote.kt | 8 +++++--- .../vitorpamplona/amethyst/ui/note/NoteCompose.kt | 2 +- .../vitorpamplona/amethyst/ui/screen/FeedView.kt | 14 ++++++++++++++ 4 files changed, 21 insertions(+), 5 deletions(-) diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/note/BadgeCompose.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/BadgeCompose.kt index d6fef2970..f85d8fbaf 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/note/BadgeCompose.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/BadgeCompose.kt @@ -84,7 +84,7 @@ fun BadgeCompose( val scope = rememberCoroutineScope() if (note == null) { - BlankNote(Modifier, isInnerNote) + BlankNote(Modifier, !isInnerNote) } else { val defaultBackgroundColor = MaterialTheme.colorScheme.background val backgroundColor = remember { mutableStateOf(defaultBackgroundColor) } diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/note/BlankNote.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/BlankNote.kt index 6774fde06..878dec216 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/note/BlankNote.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/BlankNote.kt @@ -36,6 +36,7 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import com.vitorpamplona.amethyst.R @@ -62,7 +63,7 @@ fun BlankNotePreview() { @Composable fun BlankNote( modifier: Modifier = Modifier, - showDivider: Boolean = false, + showDivider: Boolean = true, idHex: String? = null, ) { Column(modifier = modifier) { @@ -74,7 +75,7 @@ fun BlankNote( start = 20.dp, end = 20.dp, bottom = 8.dp, - top = 15.dp, + top = 8.dp, ), verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.Center, @@ -83,10 +84,11 @@ fun BlankNote( text = stringResource(R.string.post_not_found) + if (idHex != null) ": $idHex" else "", modifier = Modifier.padding(30.dp), color = Color.Gray, + textAlign = TextAlign.Center, ) } - if (!showDivider) { + if (showDivider) { HorizontalDivider( modifier = Modifier.padding(vertical = 10.dp), thickness = DividerThickness, 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 44687bac2..679cd84b1 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 @@ -217,7 +217,7 @@ fun NoteCompose( onLongClick = showPopup, ) }, - isBoostedNote || isQuotedNote, + !isBoostedNote && !isQuotedNote, ) } } diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/FeedView.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/FeedView.kt index 72e389c3a..4330347e0 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/FeedView.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/FeedView.kt @@ -231,6 +231,20 @@ private fun FeedLoaded( nav = nav, ) } + + /*var see by + remember { + mutableStateOf(false) + } + + if (see) { + } else { + Row(defaultModifier) { + Button(onClick = { see = true }) { + Text("Show") + } + } + }*/ } } } From 203899461306420819b6810a962cc637697b7611 Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Wed, 6 Mar 2024 14:25:19 -0500 Subject: [PATCH 96/98] Holds the state of expanded text button between edits and translations. --- .../amethyst/ui/actions/NotifyRequestDialog.kt | 1 + .../ui/components/ExpandableRichTextViewer.kt | 13 +++++++++++-- .../amethyst/ui/note/ChatroomMessageCompose.kt | 2 ++ .../amethyst/ui/note/MultiSetCompose.kt | 1 + .../com/vitorpamplona/amethyst/ui/note/PollNote.kt | 2 ++ .../amethyst/ui/note/types/AppDefinition.kt | 1 + .../amethyst/ui/note/types/AudioTrack.kt | 1 + .../amethyst/ui/note/types/CommunityHeader.kt | 7 ++++--- .../com/vitorpamplona/amethyst/ui/note/types/Git.kt | 2 ++ .../amethyst/ui/note/types/Highlight.kt | 1 + .../vitorpamplona/amethyst/ui/note/types/PinList.kt | 1 + .../vitorpamplona/amethyst/ui/note/types/Poll.kt | 1 + .../amethyst/ui/note/types/PrivateMessage.kt | 2 ++ .../vitorpamplona/amethyst/ui/note/types/Report.kt | 1 + .../vitorpamplona/amethyst/ui/note/types/Text.kt | 1 + .../amethyst/ui/note/types/TextModification.kt | 1 + .../vitorpamplona/amethyst/ui/note/types/Video.kt | 1 + .../amethyst/ui/screen/loggedIn/ChannelScreen.kt | 1 + .../amethyst/ui/screen/loggedIn/ProfileScreen.kt | 1 + .../ui/components/TranslatableRichTextViewer.kt | 4 ++++ 20 files changed, 40 insertions(+), 5 deletions(-) diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/NotifyRequestDialog.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/NotifyRequestDialog.kt index c19d7531c..672e7acb8 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/NotifyRequestDialog.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/NotifyRequestDialog.kt @@ -68,6 +68,7 @@ fun NotifyRequestDialog( Modifier.fillMaxWidth(), EmptyTagList, background, + textContent, accountViewModel, nav, ) diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/components/ExpandableRichTextViewer.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/components/ExpandableRichTextViewer.kt index 477e7ac2b..5a48184a2 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/components/ExpandableRichTextViewer.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/components/ExpandableRichTextViewer.kt @@ -20,6 +20,7 @@ */ package com.vitorpamplona.amethyst.ui.components +import android.util.LruCache import androidx.compose.foundation.background import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box @@ -51,6 +52,10 @@ import com.vitorpamplona.amethyst.ui.theme.ButtonPadding import com.vitorpamplona.amethyst.ui.theme.secondaryButtonBackground import com.vitorpamplona.quartz.events.ImmutableListOfLists +object ShowFullTextCache { + val cache = LruCache(20) +} + @Composable fun ExpandableRichTextViewer( content: String, @@ -58,10 +63,11 @@ fun ExpandableRichTextViewer( modifier: Modifier, tags: ImmutableListOfLists, backgroundColor: MutableState, + id: String, accountViewModel: AccountViewModel, nav: (String) -> Unit, ) { - var showFullText by remember { mutableStateOf(false) } + var showFullText by remember { mutableStateOf(ShowFullTextCache.cache[id] ?: false) } val whereToCut = remember(content) { ExpandableTextCutOffCalculator.indexToCutOff(content) } @@ -96,7 +102,10 @@ fun ExpandableRichTextViewer( .fillMaxWidth() .background(getGradient(backgroundColor)), ) { - ShowMoreButton { showFullText = !showFullText } + ShowMoreButton { + showFullText = !showFullText + ShowFullTextCache.cache.put(id, showFullText) + } } } } diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/note/ChatroomMessageCompose.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/ChatroomMessageCompose.kt index 4d647c181..38ce86483 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/note/ChatroomMessageCompose.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/ChatroomMessageCompose.kt @@ -641,6 +641,7 @@ private fun RenderRegularTextNote( modifier = HalfTopPadding, tags = tags, backgroundColor = backgroundBubbleColor, + id = note.idHex, accountViewModel = accountViewModel, nav = nav, ) @@ -652,6 +653,7 @@ private fun RenderRegularTextNote( modifier = HalfTopPadding, tags = EmptyTagList, backgroundColor = backgroundBubbleColor, + id = note.idHex, accountViewModel = accountViewModel, nav = nav, ) diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/note/MultiSetCompose.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/MultiSetCompose.kt index 5a25e557e..b36e64c93 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/note/MultiSetCompose.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/MultiSetCompose.kt @@ -474,6 +474,7 @@ fun CrossfadeToDisplayComment( tags = EmptyTagList, modifier = textBoxModifier, backgroundColor = backgroundColor, + id = comment, accountViewModel = accountViewModel, nav = nav, ) diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/note/PollNote.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/PollNote.kt index e17404ba2..5b3d75f7c 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/note/PollNote.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/PollNote.kt @@ -274,6 +274,7 @@ private fun RenderOptionAfterVote( Modifier, tags, backgroundColor, + poolOption.descriptor, accountViewModel, nav, ) @@ -307,6 +308,7 @@ private fun RenderOptionBeforeVote( remember { Modifier.padding(15.dp) }, tags, backgroundColor, + id = description, accountViewModel, nav, ) diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/note/types/AppDefinition.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/types/AppDefinition.kt index 607354e1b..fcffd450b 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/note/types/AppDefinition.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/types/AppDefinition.kt @@ -234,6 +234,7 @@ fun RenderAppDefinition( canPreview = false, tags = tags, backgroundColor = backgroundColor, + id = note.idHex, accountViewModel = accountViewModel, nav = nav, ) diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/note/types/AudioTrack.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/types/AudioTrack.kt index 3a70d2d6b..78f442277 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/note/types/AudioTrack.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/types/AudioTrack.kt @@ -218,6 +218,7 @@ fun AudioHeader( canPreview = true, tags = tags, backgroundColor = background, + id = note.idHex, accountViewModel = accountViewModel, nav = nav, ) diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/note/types/CommunityHeader.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/types/CommunityHeader.kt index 3dcf3a8cb..d3d29c557 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/note/types/CommunityHeader.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/types/CommunityHeader.kt @@ -182,6 +182,7 @@ fun LongCommunityHeader( canPreview = false, tags = EmptyTagList, backgroundColor = background, + id = baseNote.idHex, accountViewModel = accountViewModel, nav = nav, ) @@ -189,9 +190,9 @@ fun LongCommunityHeader( if (summary != null && noteEvent.hasHashtags()) { DisplayUncitedHashtags( - remember(noteEvent) { noteEvent.hashtags().toImmutableList() }, - summary ?: "", - nav, + hashtags = remember(key1 = noteEvent) { noteEvent.hashtags().toImmutableList() }, + eventContent = summary, + nav = nav, ) } } diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/note/types/Git.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/types/Git.kt index bdf0986d1..9be1ef012 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/note/types/Git.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/types/Git.kt @@ -183,6 +183,7 @@ private fun RenderGitPatchEvent( modifier = modifier, tags = tags, backgroundColor = backgroundColor, + id = note.idHex, accountViewModel = accountViewModel, nav = nav, ) @@ -281,6 +282,7 @@ private fun RenderGitIssueEvent( modifier = modifier, tags = tags, backgroundColor = backgroundColor, + id = note.idHex, accountViewModel = accountViewModel, nav = nav, ) diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/note/types/Highlight.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/types/Highlight.kt index 7fe5f703b..db227ced8 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/note/types/Highlight.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/types/Highlight.kt @@ -107,6 +107,7 @@ fun DisplayHighlight( remember { Modifier.fillMaxWidth() }, EmptyTagList, backgroundColor, + id = quote, accountViewModel, nav, ) diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/note/types/PinList.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/types/PinList.kt index b5261a7dd..4ab4876bb 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/note/types/PinList.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/types/PinList.kt @@ -107,6 +107,7 @@ fun RenderPinListEvent( canPreview = true, tags = EmptyTagList, backgroundColor = backgroundColor, + id = baseNote.idHex, accountViewModel = accountViewModel, nav = nav, ) diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/note/types/Poll.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/types/Poll.kt index 2dfc1ed79..f9bd72547 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/note/types/Poll.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/types/Poll.kt @@ -73,6 +73,7 @@ fun RenderPoll( modifier = remember { Modifier.fillMaxWidth() }, tags = tags, backgroundColor = backgroundColor, + id = note.idHex, accountViewModel = accountViewModel, nav = nav, ) diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/note/types/PrivateMessage.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/types/PrivateMessage.kt index 537a9ee6d..84fbc70de 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/note/types/PrivateMessage.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/types/PrivateMessage.kt @@ -84,6 +84,7 @@ fun RenderPrivateMessage( modifier = modifier, tags = tags, backgroundColor = backgroundColor, + id = note.idHex, accountViewModel = accountViewModel, nav = nav, ) @@ -111,6 +112,7 @@ fun RenderPrivateMessage( Modifier.fillMaxWidth(), EmptyTagList, backgroundColor, + id = note.idHex, accountViewModel, nav, ) diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/note/types/Report.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/types/Report.kt index 50902b48d..7075af94c 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/note/types/Report.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/types/Report.kt @@ -80,6 +80,7 @@ fun RenderReport( modifier = Modifier, tags = EmptyTagList, backgroundColor = backgroundColor, + id = note.idHex, accountViewModel = accountViewModel, nav = nav, ) diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/note/types/Text.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/types/Text.kt index cfea566bf..538e4f6a3 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/note/types/Text.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/types/Text.kt @@ -106,6 +106,7 @@ fun RenderTextEvent( modifier = modifier, tags = tags, backgroundColor = backgroundColor, + id = note.idHex, accountViewModel = accountViewModel, nav = nav, ) diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/note/types/TextModification.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/types/TextModification.kt index 41412de94..2920ae174 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/note/types/TextModification.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/types/TextModification.kt @@ -116,6 +116,7 @@ fun RenderTextModificationEvent( modifier = Modifier.fillMaxWidth(), tags = EmptyTagList, backgroundColor = backgroundColor, + id = note.idHex, accountViewModel = accountViewModel, nav = nav, ) diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/note/types/Video.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/types/Video.kt index e82869266..4e532de82 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/note/types/Video.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/types/Video.kt @@ -166,6 +166,7 @@ fun VideoDisplay( modifier = Modifier.fillMaxWidth(), tags = tags, backgroundColor = backgroundColor, + id = note.idHex, accountViewModel = accountViewModel, nav = nav, ) diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/ChannelScreen.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/ChannelScreen.kt index 5f9b58238..7286cfc0b 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/ChannelScreen.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/ChannelScreen.kt @@ -811,6 +811,7 @@ fun LongChannelHeader( canPreview = false, tags = tags, backgroundColor = background, + id = baseChannel.idHex, accountViewModel = accountViewModel, nav = nav, ) diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/ProfileScreen.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/ProfileScreen.kt index d12f2de31..f1b118ed1 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/ProfileScreen.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/ProfileScreen.kt @@ -1103,6 +1103,7 @@ private fun DrawAdditionalInfo( canPreview = false, tags = EmptyTagList, backgroundColor = background, + id = it, accountViewModel = accountViewModel, nav = nav, ) diff --git a/app/src/play/java/com/vitorpamplona/amethyst/ui/components/TranslatableRichTextViewer.kt b/app/src/play/java/com/vitorpamplona/amethyst/ui/components/TranslatableRichTextViewer.kt index 5c1111c69..cff6738d5 100644 --- a/app/src/play/java/com/vitorpamplona/amethyst/ui/components/TranslatableRichTextViewer.kt +++ b/app/src/play/java/com/vitorpamplona/amethyst/ui/components/TranslatableRichTextViewer.kt @@ -74,6 +74,7 @@ fun TranslatableRichTextViewer( modifier: Modifier = Modifier, tags: ImmutableListOfLists, backgroundColor: MutableState, + id: String, accountViewModel: AccountViewModel, nav: (String) -> Unit, ) { @@ -98,6 +99,7 @@ fun TranslatableRichTextViewer( modifier, tags, backgroundColor, + id, accountViewModel, nav, ) @@ -112,6 +114,7 @@ private fun RenderText( modifier: Modifier, tags: ImmutableListOfLists, backgroundColor: MutableState, + id: String, accountViewModel: AccountViewModel, nav: (String) -> Unit, ) { @@ -130,6 +133,7 @@ private fun RenderText( modifier, tags, backgroundColor, + id, accountViewModel, nav, ) From c796cbd7beb2ff8679cdc8db215040dfb782ba14 Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Wed, 6 Mar 2024 14:27:22 -0500 Subject: [PATCH 97/98] Fixes the fdroid flavor --- .../amethyst/ui/components/TranslatableRichTextViewer.kt | 2 ++ 1 file changed, 2 insertions(+) diff --git a/app/src/fdroid/java/com/vitorpamplona/amethyst/ui/components/TranslatableRichTextViewer.kt b/app/src/fdroid/java/com/vitorpamplona/amethyst/ui/components/TranslatableRichTextViewer.kt index fe225dcaf..b5f4b216b 100644 --- a/app/src/fdroid/java/com/vitorpamplona/amethyst/ui/components/TranslatableRichTextViewer.kt +++ b/app/src/fdroid/java/com/vitorpamplona/amethyst/ui/components/TranslatableRichTextViewer.kt @@ -34,6 +34,7 @@ fun TranslatableRichTextViewer( modifier: Modifier = Modifier, tags: ImmutableListOfLists, backgroundColor: MutableState, + id: String, accountViewModel: AccountViewModel, nav: (String) -> Unit, ) = ExpandableRichTextViewer( @@ -42,6 +43,7 @@ fun TranslatableRichTextViewer( modifier, tags, backgroundColor, + id, accountViewModel, nav, ) From e6da3408794ee3e9d2c2ff353df6647419349520 Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Wed, 6 Mar 2024 15:02:59 -0500 Subject: [PATCH 98/98] resets the show full text flag after 20 views. --- .../amethyst/ui/components/ExpandableRichTextViewer.kt | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/components/ExpandableRichTextViewer.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/components/ExpandableRichTextViewer.kt index 5a48184a2..cc784e243 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/components/ExpandableRichTextViewer.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/components/ExpandableRichTextViewer.kt @@ -67,7 +67,15 @@ fun ExpandableRichTextViewer( accountViewModel: AccountViewModel, nav: (String) -> Unit, ) { - var showFullText by remember { mutableStateOf(ShowFullTextCache.cache[id] ?: false) } + var showFullText by remember { + val cached = ShowFullTextCache.cache[id] + if (cached == null) { + ShowFullTextCache.cache.put(id, false) + mutableStateOf(false) + } else { + mutableStateOf(cached) + } + } val whereToCut = remember(content) { ExpandableTextCutOffCalculator.indexToCutOff(content) }