From d2610032172bb26083d61ae2b8a09378b001d565 Mon Sep 17 00:00:00 2001 From: vivganes Date: Sun, 14 May 2023 23:39:42 +0530 Subject: [PATCH 01/22] fix #407 correct non-english hashtags --- .../vitorpamplona/amethyst/ui/screen/loggedIn/SearchScreen.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 0a4bdedd1..9cd24bf61 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 @@ -335,7 +335,7 @@ private fun SearchBar(accountViewModel: AccountViewModel, navController: NavCont } } -val hashtagSearch = Pattern.compile("(?:\\s|\\A)#([A-Za-z0-9_\\-]+)") +val hashtagSearch = Pattern.compile("(?:\\s|\\A)#([^\\s!@#\$%^&*()=+./,\\[{\\]};:'\"?><]+)") fun findHashtags(content: String): List { val matcher = hashtagSearch.matcher(content) From f84501e40f777f35c74c79ebdd74a01598794971 Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Sun, 14 May 2023 17:03:46 -0400 Subject: [PATCH 02/22] Improvements to Poll Caching system: Fixes https://github.com/vitorpamplona/amethyst/issues/406 --- .../com/vitorpamplona/amethyst/model/Note.kt | 6 +- .../amethyst/ui/components/BundledUpdate.kt | 6 +- .../amethyst/ui/note/PollNote.kt | 122 ++++++++---------- .../amethyst/ui/note/PollNoteViewModel.kt | 57 +++++--- 4 files changed, 104 insertions(+), 87 deletions(-) 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 c391efeb6..e8920e69f 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/model/Note.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/model/Note.kt @@ -411,9 +411,9 @@ class NoteLiveSet(u: Note) { class NoteLiveData(val note: Note) : LiveData(NoteState(note)) { // Refreshes observers in batches. private val bundler = BundledUpdate(300, Dispatchers.IO) { - if (hasActiveObservers()) { - postValue(NoteState(note)) - } + // if (hasObservers()) { + postValue(NoteState(note)) + // } } fun invalidateData() { diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/components/BundledUpdate.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/components/BundledUpdate.kt index 05651dcbc..2a1036fb9 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/components/BundledUpdate.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/components/BundledUpdate.kt @@ -71,9 +71,9 @@ class BundledInsert( delay(delay) - mySet.clear() - queue.drainTo(mySet) - onUpdate(mySet) + val mySet2 = mutableSetOf() + queue.drainTo(mySet2) + onUpdate(mySet2) } finally { withContext(NonCancellable) { onlyOneInBlock.set(false) 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 d5cbcd80b..12342c292 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 @@ -28,6 +28,7 @@ import androidx.compose.ui.unit.sp import androidx.compose.ui.window.Dialog import androidx.compose.ui.window.DialogProperties import androidx.compose.ui.window.Popup +import androidx.lifecycle.viewmodel.compose.viewModel import androidx.navigation.NavController import com.vitorpamplona.amethyst.R import com.vitorpamplona.amethyst.model.Note @@ -37,7 +38,6 @@ import com.vitorpamplona.amethyst.ui.theme.BitcoinOrange import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch import kotlinx.coroutines.withContext -import java.math.BigDecimal import java.util.* import kotlin.math.roundToInt @@ -49,19 +49,45 @@ fun PollNote( accountViewModel: AccountViewModel, navController: NavController ) { - val zapsState by baseNote.live().zaps.observeAsState() - val zappedNote = zapsState?.note ?: return - val accountState by accountViewModel.accountLiveData.observeAsState() - val account = accountState?.account ?: return + val account = remember(accountState) { accountState?.account } ?: return - val pollViewModel = PollNoteViewModel() - pollViewModel.load(account, zappedNote) + val pollViewModel: PollNoteViewModel = viewModel() - pollViewModel.pollEvent?.pollOptions()?.forEach { poll_op -> + LaunchedEffect(key1 = baseNote) { + pollViewModel.load(account, baseNote) + } + + PollNote( + baseNote = baseNote, + pollViewModel = pollViewModel, + canPreview = canPreview, + backgroundColor = backgroundColor, + accountViewModel = accountViewModel, + navController = navController + ) +} + +@Composable +fun PollNote( + baseNote: Note, + pollViewModel: PollNoteViewModel, + canPreview: Boolean, + backgroundColor: Color, + accountViewModel: AccountViewModel, + navController: NavController +) { + val zapsState by baseNote.live().zaps.observeAsState() + + LaunchedEffect(key1 = zapsState) { + withContext(Dispatchers.IO) { + pollViewModel.refreshTallies() + } + } + + pollViewModel.tallies.forEach { poll_op -> OptionNote( - poll_op.key, - poll_op.value, + poll_op, pollViewModel, baseNote, accountViewModel, @@ -74,8 +100,7 @@ fun PollNote( @Composable private fun OptionNote( - optionNumber: Int, - optionText: String, + poolOption: PollOption, pollViewModel: PollNoteViewModel, baseNote: Note, accountViewModel: AccountViewModel, @@ -88,32 +113,17 @@ private fun OptionNote( modifier = Modifier.padding(vertical = 3.dp) ) { if (!pollViewModel.canZap()) { - val defaultColor = MaterialTheme.colors.primary.copy(alpha = 0.32f) - var optionTally by remember { mutableStateOf(Pair(BigDecimal.ZERO, defaultColor)) } - - LaunchedEffect(key1 = optionNumber, key2 = pollViewModel) { - withContext(Dispatchers.IO) { - val myTally = pollViewModel.optionVoteTally(optionNumber) - val color = if ( - pollViewModel.consensusThreshold != null && - myTally >= pollViewModel.consensusThreshold!! - ) { - Color.Green.copy(alpha = 0.32f) - } else { - defaultColor - } - - if (myTally > optionTally.first || color != optionTally.second) { - optionTally = Pair(myTally, color) - } - } + val color = if (poolOption.consensusThreadhold) { + Color.Green.copy(alpha = 0.32f) + } else { + MaterialTheme.colors.primary.copy(alpha = 0.32f) } ZapVote( baseNote, + poolOption, accountViewModel, pollViewModel, - optionNumber, nonClickablePrepend = { Box( Modifier @@ -121,14 +131,14 @@ private fun OptionNote( .clip(shape = RoundedCornerShape(15.dp)) .border( 2.dp, - optionTally.second, + color, RoundedCornerShape(15.dp) ) ) { LinearProgressIndicator( modifier = Modifier.matchParentSize(), - color = optionTally.second, - progress = optionTally.first.toFloat() + color = color, + progress = poolOption.tally.toFloat() ) Row( @@ -141,7 +151,7 @@ private fun OptionNote( .width(40.dp) ) { Text( - text = "${(optionTally.first.toFloat() * 100).roundToInt()}%", + text = "${(poolOption.tally.toFloat() * 100).roundToInt()}%", fontWeight = FontWeight.Bold ) } @@ -152,7 +162,7 @@ private fun OptionNote( .padding(15.dp) ) { TranslatableRichTextViewer( - optionText, + poolOption.descriptor, canPreview, Modifier, pollViewModel.pollEvent?.tags(), @@ -170,9 +180,9 @@ private fun OptionNote( } else { ZapVote( baseNote, + poolOption, accountViewModel, pollViewModel, - optionNumber, nonClickablePrepend = {}, clickablePrepend = { Box( @@ -186,7 +196,7 @@ private fun OptionNote( ) ) { TranslatableRichTextViewer( - optionText, + poolOption.descriptor, canPreview, Modifier.padding(15.dp), pollViewModel.pollEvent?.tags(), @@ -205,9 +215,9 @@ private fun OptionNote( @OptIn(ExperimentalFoundationApi::class) fun ZapVote( baseNote: Note, + poolOption: PollOption, accountViewModel: AccountViewModel, pollViewModel: PollNoteViewModel, - pollOption: Int, modifier: Modifier = Modifier, nonClickablePrepend: @Composable () -> Unit, clickablePrepend: @Composable () -> Unit @@ -264,7 +274,7 @@ fun ZapVote( ) .show() } - } else if (pollViewModel.isVoteAmountAtomic() && pollViewModel.isPollOptionZappedBy(pollOption, accountViewModel.userProfile())) { + } else if (pollViewModel.isVoteAmountAtomic() && poolOption.zappedByLoggedIn) { // only allow one vote per option when min==max, i.e. atomic vote amount specified scope.launch { Toast @@ -281,7 +291,7 @@ fun ZapVote( accountViewModel.zap( baseNote, account.zapAmountChoices.first() * 1000, - pollOption, + poolOption.option, "", context, onError = { @@ -311,7 +321,7 @@ fun ZapVote( baseNote, accountViewModel, pollViewModel, - pollOption, + poolOption.option, onDismiss = { wantsToZap = false zappingProgress = 0f @@ -335,17 +345,7 @@ fun ZapVote( clickablePrepend() - var optionWasZappedByLoggedInUser by remember { mutableStateOf(false) } - - LaunchedEffect(key1 = zapsState) { - withContext(Dispatchers.IO) { - if (!optionWasZappedByLoggedInUser) { - optionWasZappedByLoggedInUser = pollViewModel.isPollOptionZappedBy(pollOption, accountViewModel.userProfile()) - } - } - } - - if (optionWasZappedByLoggedInUser) { + if (poolOption.zappedByLoggedIn) { zappingProgress = 1f Icon( imageVector = Icons.Default.Bolt, @@ -372,20 +372,10 @@ fun ZapVote( } } - var wasZappedByLoggedInUser by remember { mutableStateOf(false) } - - LaunchedEffect(key1 = zapsState) { - withContext(Dispatchers.IO) { - if (!wasZappedByLoggedInUser) { - wasZappedByLoggedInUser = accountViewModel.calculateIfNoteWasZappedByAccount(zappedNote) - } - } - } - // only show tallies after a user has zapped note - if (baseNote.author == accountViewModel.userProfile() || wasZappedByLoggedInUser) { + if (baseNote.author == accountViewModel.userProfile() || pollViewModel.wasZappedByLoggedInAccount) { Text( - showAmount(pollViewModel.zappedPollOptionAmount(pollOption)), + showAmount(poolOption.zappedValue), fontSize = 14.sp, color = MaterialTheme.colors.onSurface.copy(alpha = 0.32f), modifier = modifier 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 73c273806..fa142446b 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 @@ -1,5 +1,9 @@ package com.vitorpamplona.amethyst.ui.note +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.setValue +import androidx.lifecycle.ViewModel import com.vitorpamplona.amethyst.model.Account import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.model.User @@ -8,19 +12,30 @@ import java.math.BigDecimal import java.math.RoundingMode import java.util.* -class PollNoteViewModel { +data class PollOption( + val option: Int, + val descriptor: String, + val zappedValue: BigDecimal, + val tally: BigDecimal, + val consensusThreadhold: Boolean, + val zappedByLoggedIn: Boolean +) + +class PollNoteViewModel : ViewModel() { var account: Account? = null private var pollNote: Note? = null var pollEvent: PollNoteEvent? = null - private var pollOptions: Map? = null + var pollOptions: Map? = null var valueMaximum: Int? = null var valueMinimum: Int? = null private var closedAt: Int? = null var consensusThreshold: BigDecimal? = null var totalZapped: BigDecimal = BigDecimal.ZERO - var wasZappedByAuthor: Boolean = false + var wasZappedByLoggedInAccount: Boolean = false + + var tallies by mutableStateOf>(emptyList()) fun load(acc: Account, note: Note?) { account = acc @@ -32,15 +47,35 @@ class PollNoteViewModel { consensusThreshold = pollEvent?.getTagInt(CONSENSUS_THRESHOLD)?.toFloat()?.div(100)?.toBigDecimal() closedAt = pollEvent?.getTagInt(CLOSED_AT) + refreshTallies() + } + + fun refreshTallies() { totalZapped = totalZapped() - wasZappedByAuthor = note?.let { account?.calculateIfNoteWasZappedByAccount(it) } ?: false + wasZappedByLoggedInAccount = pollNote?.let { account?.calculateIfNoteWasZappedByAccount(it) } ?: false + + tallies = pollOptions?.keys?.map { + val zappedInOption = zappedPollOptionAmount(it) + + val myTally = if (totalZapped.compareTo(BigDecimal.ZERO) > 0) { + zappedInOption.divide(totalZapped, 2, RoundingMode.HALF_UP) + } else { + BigDecimal.ZERO + } + + val zappedByLoggedIn = account?.userProfile()?.let { it1 -> isPollOptionZappedBy(it, it1) } ?: false + + val consensus = consensusThreshold != null && myTally >= consensusThreshold!! + + PollOption(it, pollOptions?.get(it) ?: "", zappedInOption, myTally, consensus, zappedByLoggedIn) + } ?: emptyList() } fun canZap(): Boolean { val account = account ?: return false val user = account.userProfile() ?: return false val note = pollNote ?: return false - return user != note.author && !wasZappedByAuthor + return user != note.author && !wasZappedByLoggedInAccount } fun isVoteAmountAtomic() = valueMaximum != null && valueMinimum != null && valueMinimum == valueMaximum @@ -88,14 +123,6 @@ class PollNoteViewModel { return false } - fun optionVoteTally(op: Int): BigDecimal { - return if (totalZapped.compareTo(BigDecimal.ZERO) > 0) { - zappedPollOptionAmount(op).divide(totalZapped, 2, RoundingMode.HALF_UP) - } else { - BigDecimal.ZERO - } - } - fun isPollOptionZappedBy(option: Int, user: User): Boolean { return pollNote!!.zaps .any { @@ -105,7 +132,7 @@ class PollNoteViewModel { } } - fun zappedPollOptionAmount(option: Int): BigDecimal { + private fun zappedPollOptionAmount(option: Int): BigDecimal { return pollNote?.zaps?.values?.sumOf { val event = it?.event as? LnZapEvent if (event?.zappedPollOption() == option) { @@ -116,7 +143,7 @@ class PollNoteViewModel { } ?: BigDecimal(0) } - fun totalZapped(): BigDecimal { + private fun totalZapped(): BigDecimal { return pollNote?.zaps?.values?.sumOf { val zapEvent = (it?.event as? LnZapEvent) From f40b6c394ca0439e18710e46da66672a0978b256 Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Sun, 14 May 2023 17:22:22 -0400 Subject: [PATCH 03/22] v0.46.2 --- app/build.gradle | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/build.gradle b/app/build.gradle index 29060a914..b413c2b85 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -13,8 +13,8 @@ android { applicationId "com.vitorpamplona.amethyst" minSdk 26 targetSdk 33 - versionCode 155 - versionName "0.46.1" + versionCode 156 + versionName "0.46.2" testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" vectorDrawables { From cf9395d5e3827df2b65eaf498a018a166bc91d79 Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Sun, 14 May 2023 18:12:04 -0400 Subject: [PATCH 04/22] Avoids Notification duplicates --- .../EventNotificationConsumer.kt | 4 ++-- .../notifications/NotificationUtils.kt | 20 +++++++++---------- 2 files changed, 12 insertions(+), 12 deletions(-) diff --git a/app/src/main/java/com/vitorpamplona/amethyst/service/notifications/EventNotificationConsumer.kt b/app/src/main/java/com/vitorpamplona/amethyst/service/notifications/EventNotificationConsumer.kt index fd06198ae..03be8b972 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/service/notifications/EventNotificationConsumer.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/service/notifications/EventNotificationConsumer.kt @@ -46,7 +46,7 @@ class EventNotificationConsumer(private val applicationContext: Context) { val user = note.author?.toBestDisplayName() ?: "" val userPicture = note.author?.profilePicture() val noteUri = note.toNEvent() - notificationManager().sendDMNotification(content, user, userPicture, noteUri, applicationContext) + notificationManager().sendDMNotification(event.id, content, user, userPicture, noteUri, applicationContext) } } } @@ -88,7 +88,7 @@ class EventNotificationConsumer(private val applicationContext: Context) { } val userPicture = senderInfo?.first?.profilePicture() val noteUri = "nostr:Notifications" - notificationManager().sendZapNotification(content, title, userPicture, noteUri, applicationContext) + notificationManager().sendZapNotification(event.id, content, title, userPicture, noteUri, applicationContext) } } } diff --git a/app/src/main/java/com/vitorpamplona/amethyst/service/notifications/NotificationUtils.kt b/app/src/main/java/com/vitorpamplona/amethyst/service/notifications/NotificationUtils.kt index d8d5d5d0b..36a5bcb84 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/service/notifications/NotificationUtils.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/service/notifications/NotificationUtils.kt @@ -15,10 +15,6 @@ import com.vitorpamplona.amethyst.R import com.vitorpamplona.amethyst.ui.MainActivity object NotificationUtils { - - // Notification ID. - private var notificationId = 0 - private var dmChannel: NotificationChannel? = null private var zapChannel: NotificationChannel? = null @@ -63,6 +59,7 @@ object NotificationUtils { } fun NotificationManager.sendZapNotification( + id: String, messageBody: String, messageTitle: String, pictureUrl: String?, @@ -72,10 +69,11 @@ object NotificationUtils { val zapChannel = getOrCreateZapChannel(applicationContext) val channelId = applicationContext.getString(R.string.app_notification_zaps_channel_id) - sendNotification(messageBody, messageTitle, pictureUrl, uri, channelId, applicationContext) + sendNotification(id, messageBody, messageTitle, pictureUrl, uri, channelId, applicationContext) } fun NotificationManager.sendDMNotification( + id: String, messageBody: String, messageTitle: String, pictureUrl: String?, @@ -85,10 +83,11 @@ object NotificationUtils { val dmChannel = getOrCreateDMChannel(applicationContext) val channelId = applicationContext.getString(R.string.app_notification_dms_channel_id) - sendNotification(messageBody, messageTitle, pictureUrl, uri, channelId, applicationContext) + sendNotification(id, messageBody, messageTitle, pictureUrl, uri, channelId, applicationContext) } fun NotificationManager.sendNotification( + id: String, messageBody: String, messageTitle: String, pictureUrl: String?, @@ -104,6 +103,7 @@ object NotificationUtils { val imageLoader = ImageLoader(applicationContext) val imageResult = imageLoader.executeBlocking(request) sendNotification( + id = id, messageBody = messageBody, messageTitle = messageTitle, picture = imageResult.drawable as? BitmapDrawable, @@ -113,6 +113,7 @@ object NotificationUtils { ) } else { sendNotification( + id = id, messageBody = messageBody, messageTitle = messageTitle, picture = null, @@ -124,6 +125,7 @@ object NotificationUtils { } private fun NotificationManager.sendNotification( + id: String, messageBody: String, messageTitle: String, picture: BitmapDrawable?, @@ -137,7 +139,7 @@ object NotificationUtils { val contentPendingIntent = PendingIntent.getActivity( applicationContext, - notificationId, + id.hashCode(), contentIntent, PendingIntent.FLAG_MUTABLE ) @@ -171,9 +173,7 @@ object NotificationUtils { .setPriority(NotificationCompat.PRIORITY_HIGH) .setAutoCancel(true) - notify(notificationId, builder.build()) - - notificationId++ + notify(id.hashCode(), builder.build()) } /** From c3a31fd31369465099d97a1a4278913184f8555c Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Sun, 14 May 2023 18:20:28 -0400 Subject: [PATCH 05/22] Update PRIVACY.md Adds the collection of the push notification token to the Privacy.MD --- PRIVACY.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/PRIVACY.md b/PRIVACY.md index 7c481a0c4..50504e609 100644 --- a/PRIVACY.md +++ b/PRIVACY.md @@ -4,7 +4,7 @@ Effective as of Jan 20, 2023 for the distributed applications in the Play Store The Amethyst app for Android does not collect or process any personal information from its users. The app is used to connect to third-party Nostr servers (also called Relays) that may or may not collect personal information and are not covered by this privacy policy. Each third-party relay server comes equipped with its own privacy policy and terms of use that can be viewed through the app or through that server's website. The developers of this open source project or maintainers of the distribution channels (app stores) do not have access to the data located in the user's phone. Accounts are fully maintained by the user. We do not have control over them. -Data from connected accounts is only stored locally on the device when it is required for functionality and performance of Amethyst. This data is strictly confidental and cannot be accessed by other apps (on non-rooted devices). Phone data can be deleted by clearing Amethyst's local storage or uninstalling the app. +The app may collect a per-device token, your public key and a preferred Relay to connect to and provide push notification services through Google's Firebase Cloud Messaging. Other than that, the data from connected accounts is only stored locally on the device when it is required for functionality and performance of Amethyst. This data is strictly confidental and cannot be accessed by other apps (on non-rooted devices). Phone data can be deleted by clearing Amethyst's local storage or uninstalling the app. You cannot use the Amethyst app for Android to submit Objectionable Content to relays. Objectionable Content includes, but is not limited to: (i) sexually explicit materials; (ii) obscene, defamatory, libelous, slanderous, violent and/or unlawful content or profanity; (iii) content that infringes upon the rights of any third party, including copyright, trademark, privacy, publicity or other personal or proprietary right, or that is deceptive or fraudulent; (iv) content that promotes the use or sale of illegal or regulated substances, tobacco products, ammunition and/or firearms; and (v) illegal content related to gambling. From e5ff92c8045cbf03f2ee2db77d5ecb9bdc3f8ad7 Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Sun, 14 May 2023 18:21:33 -0400 Subject: [PATCH 06/22] v0.46.3 --- app/build.gradle | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/build.gradle b/app/build.gradle index b413c2b85..b6f37be86 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -13,8 +13,8 @@ android { applicationId "com.vitorpamplona.amethyst" minSdk 26 targetSdk 33 - versionCode 156 - versionName "0.46.2" + versionCode 157 + versionName "0.46.3" testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" vectorDrawables { From 814d05812aed8efa17863e6ac49802de7b3c7ffb Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Sun, 14 May 2023 18:59:33 -0400 Subject: [PATCH 07/22] Fixes image cropping opening the image dialog. --- .../amethyst/ui/components/ZoomableContentView.kt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/components/ZoomableContentView.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/components/ZoomableContentView.kt index bdecb38e2..6eed48772 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/components/ZoomableContentView.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/components/ZoomableContentView.kt @@ -219,7 +219,7 @@ private fun LocalImageView( it.aspectRatio(ratio, maxHeight.isFinite) } } - val contentScale = if (maxHeight.isFinite) ContentScale.Fit else ContentScale.FillWidth + val contentScale = if (maxHeight.isFinite) ContentScale.Inside else ContentScale.FillWidth if (content.localFile != null && content.localFile.exists()) { AsyncImage( @@ -285,7 +285,7 @@ private fun UrlImageView( this.aspectRatio(ratio, maxHeight.isFinite) } ?: this } - val contentScale = if (maxHeight.isFinite) ContentScale.Fit else ContentScale.FillWidth + val contentScale = if (maxHeight.isFinite) ContentScale.Inside else ContentScale.FillWidth AsyncImage( model = content.url, From 1c76b7a35c7b5ac2e18972ee540cf052daab203a Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Sun, 14 May 2023 18:59:49 -0400 Subject: [PATCH 08/22] Testing a not cropping version for videos. --- .../java/com/vitorpamplona/amethyst/ui/components/VideoView.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 7a6441fcb..11f0f70c5 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 @@ -89,7 +89,7 @@ fun VideoView(videoUri: Uri, description: String? = null, onDialog: ((Boolean) - ExoPlayer.Builder(context).build().apply { repeatMode = Player.REPEAT_MODE_ALL - videoScalingMode = C.VIDEO_SCALING_MODE_SCALE_TO_FIT_WITH_CROPPING + videoScalingMode = C.VIDEO_SCALING_MODE_SCALE_TO_FIT volume = 0f if (videoUri.scheme?.startsWith("file") == true) { setMediaItem(media) From bbea28c6dfa3c416a59beaaf574b752a5836c0b9 Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Sun, 14 May 2023 19:04:34 -0400 Subject: [PATCH 09/22] 0.46.4 --- app/build.gradle | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/build.gradle b/app/build.gradle index b6f37be86..ffb3e068c 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -13,8 +13,8 @@ android { applicationId "com.vitorpamplona.amethyst" minSdk 26 targetSdk 33 - versionCode 157 - versionName "0.46.3" + versionCode 158 + versionName "0.46.4" testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" vectorDrawables { From bf2949717c771af17d6e8aaf993094785826fcee Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Sun, 14 May 2023 20:24:11 -0400 Subject: [PATCH 10/22] Makes sure to only change the visible status when needed. --- .../com/vitorpamplona/amethyst/ui/components/VideoView.kt | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) 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 11f0f70c5..af8444a1f 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 @@ -175,7 +175,10 @@ fun Modifier.onVisibilityChanges(onVisibilityChanges: (Boolean) -> Unit): Modifi } onGloballyPositioned { coordinates -> - isVisible = coordinates.isCompletelyVisible(view) + val newIsVisible = coordinates.isCompletelyVisible(view) + if (isVisible != newIsVisible) { + isVisible = newIsVisible + } } } From 0406632e9d814c0903f768c3b6492b298d37be4c Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Sun, 14 May 2023 20:24:41 -0400 Subject: [PATCH 11/22] Moves internal content changes to derived states. --- .../ui/components/ExpandableRichTextViewer.kt | 13 +++++++++---- .../amethyst/ui/components/RichTextViewer.kt | 2 -- .../ui/components/TranslatableRichTextViewer.kt | 9 +++++++-- 3 files changed, 16 insertions(+), 8 deletions(-) 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 ba0d79464..6488a63b8 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 @@ -13,6 +13,7 @@ import androidx.compose.material.ButtonDefaults import androidx.compose.material.MaterialTheme import androidx.compose.material.Text import androidx.compose.runtime.Composable +import androidx.compose.runtime.derivedStateOf import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember @@ -50,10 +51,14 @@ fun ExpandableRichTextViewer( minOf(firstSpaceAfterCut, firstNewLineAfterCut) } - val text = if (showFullText) { - content - } else { - content.take(whereToCut) + val text by remember { + derivedStateOf { + if (showFullText) { + content + } else { + content.take(whereToCut) + } + } } Box { 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 17d165a3d..a833e977d 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 @@ -50,7 +50,6 @@ import java.net.MalformedURLException import java.net.URISyntaxException import java.net.URL import java.util.regex.Pattern -import kotlin.time.ExperimentalTime val imageExtensions = listOf("png", "jpg", "gif", "bmp", "jpeg", "webp", "svg") val videoExtensions = listOf("mp4", "avi", "wmv", "mpg", "amv", "webm", "mov", "mp3") @@ -113,7 +112,6 @@ class RichTextViewerState( val imageList: List ) -@OptIn(ExperimentalTime::class) @Composable private fun RenderRegular( content: String, 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 4c6e17a6c..f7b6a1d15 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 @@ -14,6 +14,7 @@ import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.Check import androidx.compose.runtime.Composable 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 @@ -56,7 +57,7 @@ fun TranslatableRichTextViewer( var langSettingsPopupExpanded by remember { mutableStateOf(false) } val accountState by accountViewModel.accountLanguagesLiveData.observeAsState() - val account = accountState?.account ?: return + val account = remember(accountState) { accountState?.account } ?: return val scope = rememberCoroutineScope() @@ -78,7 +79,11 @@ fun TranslatableRichTextViewer( } } - val toBeViewed = if (showOriginal) content else translatedTextState.value.result ?: content + val toBeViewed by remember { + derivedStateOf { + if (showOriginal) content else translatedTextState.value.result ?: content + } + } Column() { ExpandableRichTextViewer( From 7cf0291b5a1657729b80411241a68b5809d8cdc0 Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Sun, 14 May 2023 21:05:58 -0400 Subject: [PATCH 12/22] Applies remember functions to more parameters. --- .../amethyst/ui/components/UrlPreviewCard.kt | 13 ++++++++----- .../amethyst/ui/components/VideoView.kt | 4 ++-- .../amethyst/ui/components/ZoomableContentView.kt | 14 +++++++++----- 3 files changed, 19 insertions(+), 12 deletions(-) diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/components/UrlPreviewCard.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/components/UrlPreviewCard.kt index 561d86ab3..88de0195b 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/components/UrlPreviewCard.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/components/UrlPreviewCard.kt @@ -10,6 +10,7 @@ import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material.MaterialTheme import androidx.compose.material.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 @@ -41,13 +42,15 @@ fun UrlPreviewCard( ) ) { Column { - val validatedUrl = URL(previewInfo.url) + val validatedUrl = remember { URL(previewInfo.url) } // correctly treating relative images - val imageUrl = if (previewInfo.image.startsWith("/")) { - URL(validatedUrl, previewInfo.image).toString() - } else { - previewInfo.image + val imageUrl = remember { + if (previewInfo.image.startsWith("/")) { + URL(validatedUrl, previewInfo.image).toString() + } else { + previewInfo.image + } } AsyncImage( 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 af8444a1f..40d1b72c7 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 @@ -116,9 +116,9 @@ fun VideoView(videoUri: Uri, description: String? = null, onDialog: ((Boolean) - .defaultMinSize(minHeight = 70.dp) .align(Alignment.Center) .onVisibilityChanges { visible -> - if (visible) { + if (visible && !exoPlayer.isPlaying) { exoPlayer.play() - } else { + } else if (!visible && exoPlayer.isPlaying) { exoPlayer.pause() } }, diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/components/ZoomableContentView.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/components/ZoomableContentView.kt index 6eed48772..d54b41ecc 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/components/ZoomableContentView.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/components/ZoomableContentView.kt @@ -280,12 +280,16 @@ private fun UrlImageView( } BoxWithConstraints(contentAlignment = Alignment.Center) { - val myModifier = mainImageModifier.run { - aspectRatio(content.dim)?.let { ratio -> - this.aspectRatio(ratio, maxHeight.isFinite) - } ?: this + val myModifier = remember { + mainImageModifier.run { + aspectRatio(content.dim)?.let { ratio -> + this.aspectRatio(ratio, maxHeight.isFinite) + } ?: this + } + } + val contentScale = remember { + if (maxHeight.isFinite) ContentScale.Inside else ContentScale.FillWidth } - val contentScale = if (maxHeight.isFinite) ContentScale.Inside else ContentScale.FillWidth AsyncImage( model = content.url, From 4a8680e5824936c2f3762c81fad6283ae3e7ae0a Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Sun, 14 May 2023 21:06:06 -0400 Subject: [PATCH 13/22] 0.46.5 --- app/build.gradle | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/build.gradle b/app/build.gradle index ffb3e068c..cdfdbec46 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -13,8 +13,8 @@ android { applicationId "com.vitorpamplona.amethyst" minSdk 26 targetSdk 33 - versionCode 158 - versionName "0.46.4" + versionCode 159 + versionName "0.46.5" testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" vectorDrawables { From 32913915ce38e516d95011ddc651673efb8c03da Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Sun, 14 May 2023 22:22:22 -0400 Subject: [PATCH 14/22] Making sure remember does not interfere with translations. --- .../amethyst/ui/components/ExpandableRichTextViewer.kt | 2 +- .../ui/components/TranslatableRichTextViewer.kt | 10 +++++----- 2 files changed, 6 insertions(+), 6 deletions(-) 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 6488a63b8..8d622093f 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 @@ -51,7 +51,7 @@ fun ExpandableRichTextViewer( minOf(firstSpaceAfterCut, firstNewLineAfterCut) } - val text by remember { + val text by remember(content) { derivedStateOf { if (showFullText) { content 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 f7b6a1d15..8e55bfcca 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 @@ -49,7 +49,7 @@ fun TranslatableRichTextViewer( accountViewModel: AccountViewModel, navController: NavController ) { - val translatedTextState = remember { + var translatedTextState by remember { mutableStateOf(ResultOrError(content, null, null, null)) } @@ -73,7 +73,7 @@ fun TranslatableRichTextViewer( val preference = account.preferenceBetween(task.result.sourceLang!!, task.result.targetLang!!) showOriginal = preference == task.result.sourceLang } - translatedTextState.value = task.result + translatedTextState = task.result } } } @@ -81,7 +81,7 @@ fun TranslatableRichTextViewer( val toBeViewed by remember { derivedStateOf { - if (showOriginal) content else translatedTextState.value.result ?: content + if (showOriginal) content else translatedTextState.result ?: content } } @@ -96,8 +96,8 @@ fun TranslatableRichTextViewer( navController ) - val target = translatedTextState.value.targetLang - val source = translatedTextState.value.sourceLang + val target = translatedTextState.targetLang + val source = translatedTextState.sourceLang if (source != null && target != null) { if (source != target) { From db6ebe609c0b6dcaf77b7fd6c275f094df44ce60 Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Sun, 14 May 2023 22:22:53 -0400 Subject: [PATCH 15/22] Forces aspect ratio while obeying maxWidth and Height of the frame. --- .../ui/components/ZoomableContentView.kt | 40 ++++++++++++------- 1 file changed, 25 insertions(+), 15 deletions(-) diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/components/ZoomableContentView.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/components/ZoomableContentView.kt index d54b41ecc..055f31624 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/components/ZoomableContentView.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/components/ZoomableContentView.kt @@ -17,9 +17,11 @@ import androidx.compose.foundation.layout.aspectRatio 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.padding import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.width +import androidx.compose.foundation.layout.widthIn import androidx.compose.foundation.pager.PagerState import androidx.compose.foundation.shape.CircleShape import androidx.compose.foundation.shape.RoundedCornerShape @@ -209,17 +211,21 @@ private fun LocalImageView( mutableStateOf(null) } - val ratio = remember { - aspectRatio(content.dim) - } - BoxWithConstraints(contentAlignment = Alignment.Center) { - val myModifier = mainImageModifier.also { - if (ratio != null) { - it.aspectRatio(ratio, maxHeight.isFinite) - } + val myModifier = remember { + mainImageModifier + .widthIn(max = maxWidth) + .heightIn(max = maxHeight) + .run { + aspectRatio(content.dim)?.let { ratio -> + this.aspectRatio(ratio, false) + } ?: this + } + } + + val contentScale = remember { + if (maxHeight.isFinite) ContentScale.Fit else ContentScale.FillWidth } - val contentScale = if (maxHeight.isFinite) ContentScale.Inside else ContentScale.FillWidth if (content.localFile != null && content.localFile.exists()) { AsyncImage( @@ -281,14 +287,18 @@ private fun UrlImageView( BoxWithConstraints(contentAlignment = Alignment.Center) { val myModifier = remember { - mainImageModifier.run { - aspectRatio(content.dim)?.let { ratio -> - this.aspectRatio(ratio, maxHeight.isFinite) - } ?: this - } + mainImageModifier + .widthIn(max = maxWidth) + .heightIn(max = maxHeight) + .run { + aspectRatio(content.dim)?.let { ratio -> + this.aspectRatio(ratio, false) + } ?: this + } } + val contentScale = remember { - if (maxHeight.isFinite) ContentScale.Inside else ContentScale.FillWidth + if (maxHeight.isFinite) ContentScale.Fit else ContentScale.FillWidth } AsyncImage( From c973d941cc4669e6b1928ea6b2ba82ecf176bc0a Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Sun, 14 May 2023 22:23:37 -0400 Subject: [PATCH 16/22] v0.46.6 --- app/build.gradle | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/build.gradle b/app/build.gradle index cdfdbec46..666eae245 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -13,8 +13,8 @@ android { applicationId "com.vitorpamplona.amethyst" minSdk 26 targetSdk 33 - versionCode 159 - versionName "0.46.5" + versionCode 160 + versionName "0.46.6" testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" vectorDrawables { From 32927040e30e366374c9ec32c7c46a973d0c7ea0 Mon Sep 17 00:00:00 2001 From: greenart7c3 <115044884+greenart7c3@users.noreply.github.com> Date: Mon, 15 May 2023 11:17:42 -0300 Subject: [PATCH 17/22] add longPress to edit tor/orbot settings --- .../amethyst/ui/navigation/DrawerContent.kt | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 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 a25d4c8e2..8b0501619 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 @@ -1,9 +1,11 @@ package com.vitorpamplona.amethyst.ui.navigation +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.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row @@ -294,6 +296,12 @@ fun ListContent( title = textTorProxy, icon = R.drawable.ic_tor, tint = MaterialTheme.colors.onBackground, + onLongClick = { + coroutineScope.launch { + scaffoldState.drawerState.close() + } + conectOrbotDialogOpen = true + }, onClick = { if (checked) { disconnectTorDialog = true @@ -401,12 +409,16 @@ fun NavigationRow( }) } +@OptIn(ExperimentalFoundationApi::class) @Composable -fun IconRow(title: String, icon: Int, tint: Color, onClick: () -> Unit) { +fun IconRow(title: String, icon: Int, tint: Color, onClick: () -> Unit, onLongClick: (() -> Unit)? = null) { Row( modifier = Modifier .fillMaxWidth() - .clickable(onClick = onClick) + .combinedClickable( + onClick = onClick, + onLongClick = onLongClick + ) ) { Row( modifier = Modifier From 817a9c3e8d21ff94151287121a7ac6078d98053b Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Mon, 15 May 2023 11:15:32 -0400 Subject: [PATCH 18/22] Adds support for AudioTrack Types from (https://zapstr.live/) --- .../amethyst/model/LocalCache.kt | 13 +++ .../amethyst/service/NostrGlobalDataSource.kt | 3 +- .../service/NostrHashtagDataSource.kt | 3 +- .../amethyst/service/NostrHomeDataSource.kt | 5 +- .../service/NostrSingleEventDataSource.kt | 7 +- .../service/NostrUserProfileDataSource.kt | 2 +- .../amethyst/service/model/AudioTrackEvent.kt | 61 +++++++++++ .../service/model/BadgeDefinitionEvent.kt | 6 +- .../service/model/BadgeProfilesEvent.kt | 6 +- .../amethyst/service/model/Event.kt | 6 + .../service/model/GeneralListEvent.kt | 6 +- .../service/model/LnZapRequestEvent.kt | 2 +- .../service/model/LongTextNoteEvent.kt | 6 +- .../amethyst/service/model/MuteListEvent.kt | 6 +- .../amethyst/service/model/ReactionEvent.kt | 2 +- .../amethyst/service/model/ReportEvent.kt | 2 +- .../amethyst/service/model/RepostEvent.kt | 2 +- .../amethyst/ui/components/VideoView.kt | 50 ++++++++- .../amethyst/ui/dal/GlobalFeedFilter.kt | 2 +- .../ui/dal/HomeNewThreadFeedFilter.kt | 3 +- .../amethyst/ui/note/NoteCompose.kt | 103 ++++++++++++++++++ .../amethyst/ui/screen/ThreadFeedView.kt | 3 + 22 files changed, 266 insertions(+), 33 deletions(-) create mode 100644 app/src/main/java/com/vitorpamplona/amethyst/service/model/AudioTrackEvent.kt 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 7f7800a05..ed3e9d0cf 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/model/LocalCache.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/model/LocalCache.kt @@ -281,6 +281,18 @@ object LocalCache { refreshObservers(note) } + private fun consume(event: AudioTrackEvent) { + val note = getOrCreateAddressableNote(event.address()) + val author = getOrCreateUser(event.pubKey) + + // Already processed this event. + if (note.event != null) return + + note.loadEvent(event, author, emptyList()) + + refreshObservers(note) + } + fun consume(event: BadgeDefinitionEvent) { val note = getOrCreateAddressableNote(event.address()) val author = getOrCreateUser(event.pubKey) @@ -920,6 +932,7 @@ object LocalCache { try { when (event) { + is AudioTrackEvent -> consume(event) is BadgeAwardEvent -> consume(event) is BadgeDefinitionEvent -> consume(event) is BadgeProfilesEvent -> consume(event) diff --git a/app/src/main/java/com/vitorpamplona/amethyst/service/NostrGlobalDataSource.kt b/app/src/main/java/com/vitorpamplona/amethyst/service/NostrGlobalDataSource.kt index ebf3425db..735036304 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/service/NostrGlobalDataSource.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/service/NostrGlobalDataSource.kt @@ -1,5 +1,6 @@ package com.vitorpamplona.amethyst.service +import com.vitorpamplona.amethyst.service.model.AudioTrackEvent import com.vitorpamplona.amethyst.service.model.ChannelMessageEvent import com.vitorpamplona.amethyst.service.model.HighlightEvent import com.vitorpamplona.amethyst.service.model.LongTextNoteEvent @@ -13,7 +14,7 @@ object NostrGlobalDataSource : NostrDataSource("GlobalFeed") { fun createGlobalFilter() = TypedFilter( types = setOf(FeedType.GLOBAL), filter = JsonFilter( - kinds = listOf(TextNoteEvent.kind, PollNoteEvent.kind, ChannelMessageEvent.kind, LongTextNoteEvent.kind, HighlightEvent.kind), + kinds = listOf(TextNoteEvent.kind, PollNoteEvent.kind, ChannelMessageEvent.kind, AudioTrackEvent.kind, LongTextNoteEvent.kind, HighlightEvent.kind), limit = 200 ) ) diff --git a/app/src/main/java/com/vitorpamplona/amethyst/service/NostrHashtagDataSource.kt b/app/src/main/java/com/vitorpamplona/amethyst/service/NostrHashtagDataSource.kt index e4233c106..9b278e09c 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/service/NostrHashtagDataSource.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/service/NostrHashtagDataSource.kt @@ -3,6 +3,7 @@ package com.vitorpamplona.amethyst.service import androidx.compose.ui.text.capitalize import com.vitorpamplona.amethyst.service.model.ChannelMessageEvent import com.vitorpamplona.amethyst.service.model.LongTextNoteEvent +import com.vitorpamplona.amethyst.service.model.PollNoteEvent import com.vitorpamplona.amethyst.service.model.TextNoteEvent import com.vitorpamplona.amethyst.service.relays.COMMON_FEED_TYPES import com.vitorpamplona.amethyst.service.relays.JsonFilter @@ -25,7 +26,7 @@ object NostrHashtagDataSource : NostrDataSource("SingleHashtagFeed") { hashToLoad.capitalize() ) ), - kinds = listOf(TextNoteEvent.kind, ChannelMessageEvent.kind, LongTextNoteEvent.kind), + kinds = listOf(TextNoteEvent.kind, ChannelMessageEvent.kind, LongTextNoteEvent.kind, LongTextNoteEvent.kind, PollNoteEvent.kind), limit = 200 ) ) diff --git a/app/src/main/java/com/vitorpamplona/amethyst/service/NostrHomeDataSource.kt b/app/src/main/java/com/vitorpamplona/amethyst/service/NostrHomeDataSource.kt index d83151205..a4277b576 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/service/NostrHomeDataSource.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/service/NostrHomeDataSource.kt @@ -2,6 +2,7 @@ package com.vitorpamplona.amethyst.service import com.vitorpamplona.amethyst.model.Account import com.vitorpamplona.amethyst.model.UserState +import com.vitorpamplona.amethyst.service.model.AudioTrackEvent import com.vitorpamplona.amethyst.service.model.HighlightEvent import com.vitorpamplona.amethyst.service.model.LongTextNoteEvent import com.vitorpamplona.amethyst.service.model.PollNoteEvent @@ -56,7 +57,7 @@ object NostrHomeDataSource : NostrDataSource("HomeFeed") { return TypedFilter( types = setOf(FeedType.FOLLOWS), filter = JsonFilter( - kinds = listOf(TextNoteEvent.kind, LongTextNoteEvent.kind, PollNoteEvent.kind, HighlightEvent.kind), + kinds = listOf(TextNoteEvent.kind, LongTextNoteEvent.kind, PollNoteEvent.kind, HighlightEvent.kind, AudioTrackEvent.kind), authors = followSet, limit = 400, since = latestEOSEs.users[account.userProfile()]?.followList?.get(account.defaultHomeFollowList)?.relayList @@ -72,7 +73,7 @@ object NostrHomeDataSource : NostrDataSource("HomeFeed") { return TypedFilter( types = setOf(FeedType.FOLLOWS), filter = JsonFilter( - kinds = listOf(TextNoteEvent.kind, LongTextNoteEvent.kind, HighlightEvent.kind), + kinds = listOf(TextNoteEvent.kind, LongTextNoteEvent.kind, HighlightEvent.kind, AudioTrackEvent.kind), tags = mapOf( "t" to hashToLoad.map { listOf(it, it.lowercase(), it.uppercase(), it.capitalize()) 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 e1b663965..41edd2886 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/service/NostrSingleEventDataSource.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/service/NostrSingleEventDataSource.kt @@ -29,7 +29,7 @@ object NostrSingleEventDataSource : NostrDataSource("SingleEventFeed") { ReactionEvent.kind, RepostEvent.kind, ReportEvent.kind, LnZapEvent.kind, LnZapRequestEvent.kind, BadgeAwardEvent.kind, BadgeDefinitionEvent.kind, BadgeProfilesEvent.kind, - PollNoteEvent.kind + PollNoteEvent.kind, AudioTrackEvent.kind ), tags = mapOf("a" to listOf(aTag.toTag())), since = it.lastReactionsDownloadTime @@ -80,7 +80,8 @@ object NostrSingleEventDataSource : NostrDataSource("SingleEventFeed") { LnZapEvent.kind, LnZapRequestEvent.kind, PollNoteEvent.kind, - HighlightEvent.kind + HighlightEvent.kind, + AudioTrackEvent.kind ), tags = mapOf("e" to listOf(it.idHex)), since = it.lastReactionsDownloadTime @@ -119,7 +120,7 @@ object NostrSingleEventDataSource : NostrDataSource("SingleEventFeed") { BadgeDefinitionEvent.kind, BadgeAwardEvent.kind, BadgeProfilesEvent.kind, PrivateDmEvent.kind, FileHeaderEvent.kind, FileStorageEvent.kind, FileStorageHeaderEvent.kind, - HighlightEvent.kind + HighlightEvent.kind, AudioTrackEvent.kind ), ids = interestedEvents.toList() ) diff --git a/app/src/main/java/com/vitorpamplona/amethyst/service/NostrUserProfileDataSource.kt b/app/src/main/java/com/vitorpamplona/amethyst/service/NostrUserProfileDataSource.kt index 49d4e2bc4..2246c7c80 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/service/NostrUserProfileDataSource.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/service/NostrUserProfileDataSource.kt @@ -28,7 +28,7 @@ object NostrUserProfileDataSource : NostrDataSource("UserProfileFeed") { TypedFilter( types = COMMON_FEED_TYPES, filter = JsonFilter( - kinds = listOf(TextNoteEvent.kind, RepostEvent.kind, LongTextNoteEvent.kind, PollNoteEvent.kind, HighlightEvent.kind), + kinds = listOf(TextNoteEvent.kind, RepostEvent.kind, LongTextNoteEvent.kind, AudioTrackEvent.kind, PollNoteEvent.kind, HighlightEvent.kind), authors = listOf(it.pubkeyHex), limit = 200 ) diff --git a/app/src/main/java/com/vitorpamplona/amethyst/service/model/AudioTrackEvent.kt b/app/src/main/java/com/vitorpamplona/amethyst/service/model/AudioTrackEvent.kt new file mode 100644 index 000000000..82dc30aa0 --- /dev/null +++ b/app/src/main/java/com/vitorpamplona/amethyst/service/model/AudioTrackEvent.kt @@ -0,0 +1,61 @@ +package com.vitorpamplona.amethyst.service.model + +import com.vitorpamplona.amethyst.model.HexKey +import com.vitorpamplona.amethyst.model.toHexKey +import nostr.postr.Utils +import java.util.Date + +class AudioTrackEvent( + id: HexKey, + pubKey: HexKey, + createdAt: Long, + tags: List>, + content: String, + sig: HexKey +) : Event(id, pubKey, createdAt, kind, tags, content, sig), AddressableEvent { + + override fun dTag() = tags.firstOrNull { it.size > 1 && it[0] == "d" }?.get(1) ?: "" + override fun address() = ATag(kind, pubKey, dTag(), null) + + fun participants() = tags.filter { it.size > 1 && it[0] == "p" }.map { Participant(it[1], it.getOrNull(2)) } + fun type() = tags.firstOrNull { it.size > 1 && it[0] == TYPE }?.get(1) + fun price() = tags.firstOrNull { it.size > 1 && it[0] == PRICE }?.get(1) + fun cover() = tags.firstOrNull { it.size > 1 && it[0] == COVER }?.get(1) + fun subject() = tags.firstOrNull { it.size > 1 && it[0] == SUBJECT }?.get(1) + fun media() = tags.firstOrNull { it.size > 1 && it[0] == MEDIA }?.get(1) + + companion object { + const val kind = 31337 + + private const val TYPE = "c" + private const val PRICE = "price" + private const val COVER = "cover" + private const val SUBJECT = "subject" + private const val MEDIA = "media" + + fun create( + type: String, + media: String, + price: String? = null, + cover: String? = null, + subject: String? = null, + privateKey: ByteArray, + createdAt: Long = Date().time / 1000 + ): AudioTrackEvent { + val tags = listOfNotNull( + listOf(MEDIA, media), + listOf(TYPE, type), + price?.let { listOf(PRICE, it) }, + cover?.let { listOf(COVER, it) }, + subject?.let { listOf(SUBJECT, it) } + ) + + val pubKey = Utils.pubkeyCreate(privateKey).toHexKey() + val id = generateId(pubKey, createdAt, kind, tags, "") + val sig = Utils.sign(id, privateKey) + return AudioTrackEvent(id.toHexKey(), pubKey, createdAt, tags, "", sig.toHexKey()) + } + } +} + +data class Participant(val key: String, val role: String?) diff --git a/app/src/main/java/com/vitorpamplona/amethyst/service/model/BadgeDefinitionEvent.kt b/app/src/main/java/com/vitorpamplona/amethyst/service/model/BadgeDefinitionEvent.kt index 42f559877..5e8be5350 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/service/model/BadgeDefinitionEvent.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/service/model/BadgeDefinitionEvent.kt @@ -9,9 +9,9 @@ class BadgeDefinitionEvent( tags: List>, content: String, sig: HexKey -) : Event(id, pubKey, createdAt, kind, tags, content, sig) { - fun dTag() = tags.firstOrNull { it.size > 1 && it[0] == "d" }?.get(1) ?: "" - fun address() = ATag(kind, pubKey, dTag(), null) +) : Event(id, pubKey, createdAt, kind, tags, content, sig), AddressableEvent { + override fun dTag() = tags.firstOrNull { it.size > 1 && it[0] == "d" }?.get(1) ?: "" + override fun address() = ATag(kind, pubKey, dTag(), null) fun name() = tags.firstOrNull { it.size > 1 && it[0] == "name" }?.get(1) fun thumb() = tags.firstOrNull { it.size > 1 && it[0] == "thumb" }?.get(1) diff --git a/app/src/main/java/com/vitorpamplona/amethyst/service/model/BadgeProfilesEvent.kt b/app/src/main/java/com/vitorpamplona/amethyst/service/model/BadgeProfilesEvent.kt index 4995c413b..f616bb7a4 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/service/model/BadgeProfilesEvent.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/service/model/BadgeProfilesEvent.kt @@ -9,7 +9,7 @@ class BadgeProfilesEvent( tags: List>, content: String, sig: HexKey -) : Event(id, pubKey, createdAt, kind, tags, content, sig) { +) : Event(id, pubKey, createdAt, kind, tags, content, sig), AddressableEvent { fun badgeAwardEvents() = tags.filter { it.firstOrNull() == "e" }.mapNotNull { it.getOrNull(1) } fun badgeAwardDefinitions() = tags.filter { it.firstOrNull() == "a" }.mapNotNull { val aTagValue = it.getOrNull(1) @@ -18,8 +18,8 @@ class BadgeProfilesEvent( if (aTagValue != null) ATag.parse(aTagValue, relay) else null } - fun dTag() = tags.filter { it.firstOrNull() == "d" }.mapNotNull { it.getOrNull(1) }.firstOrNull() ?: "" - fun address() = ATag(kind, pubKey, dTag(), null) + override fun dTag() = tags.filter { it.firstOrNull() == "d" }.mapNotNull { it.getOrNull(1) }.firstOrNull() ?: "" + override fun address() = ATag(kind, pubKey, dTag(), null) companion object { const val kind = 30008 diff --git a/app/src/main/java/com/vitorpamplona/amethyst/service/model/Event.kt b/app/src/main/java/com/vitorpamplona/amethyst/service/model/Event.kt index 6747b1f13..38a99ac76 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/service/model/Event.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/service/model/Event.kt @@ -220,6 +220,7 @@ open class Event( fun fromJson(json: JsonElement, lenient: Boolean = false): Event = gson.fromJson(json, Event::class.java).getRefinedEvent(lenient) fun Event.getRefinedEvent(lenient: Boolean = false): Event = when (kind) { + AudioTrackEvent.kind -> AudioTrackEvent(id, pubKey, createdAt, tags, content, sig) BadgeAwardEvent.kind -> BadgeAwardEvent(id, pubKey, createdAt, tags, content, sig) BadgeDefinitionEvent.kind -> BadgeDefinitionEvent(id, pubKey, createdAt, tags, content, sig) BadgeProfilesEvent.kind -> BadgeProfilesEvent(id, pubKey, createdAt, tags, content, sig) @@ -282,3 +283,8 @@ open class Event( } } } + +interface AddressableEvent { + fun dTag(): String + fun address(): ATag +} diff --git a/app/src/main/java/com/vitorpamplona/amethyst/service/model/GeneralListEvent.kt b/app/src/main/java/com/vitorpamplona/amethyst/service/model/GeneralListEvent.kt index cbb8b2fac..57cadcd66 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/service/model/GeneralListEvent.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/service/model/GeneralListEvent.kt @@ -14,9 +14,9 @@ abstract class GeneralListEvent( tags: List>, content: String, sig: HexKey -) : Event(id, pubKey, createdAt, kind, tags, content, sig) { - fun dTag() = tags.filter { it.firstOrNull() == "d" }.mapNotNull { it.getOrNull(1) }.firstOrNull() ?: "" - fun address() = ATag(kind, pubKey, dTag(), null) +) : Event(id, pubKey, createdAt, kind, tags, content, sig), AddressableEvent { + override fun dTag() = tags.filter { it.firstOrNull() == "d" }.mapNotNull { it.getOrNull(1) }.firstOrNull() ?: "" + override fun address() = ATag(kind, pubKey, dTag(), null) fun category() = dTag() fun bookmarkedPosts() = taggedEvents() diff --git a/app/src/main/java/com/vitorpamplona/amethyst/service/model/LnZapRequestEvent.kt b/app/src/main/java/com/vitorpamplona/amethyst/service/model/LnZapRequestEvent.kt index 8586849a6..eeb2bb81a 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/service/model/LnZapRequestEvent.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/service/model/LnZapRequestEvent.kt @@ -71,7 +71,7 @@ class LnZapRequestEvent( listOf("p", originalNote.pubKey()), listOf("relays") + relays ) - if (originalNote is LongTextNoteEvent) { + if (originalNote is AddressableEvent) { tags = tags + listOf(listOf("a", originalNote.address().toTag())) } if (pollOption != null && pollOption >= 0) { diff --git a/app/src/main/java/com/vitorpamplona/amethyst/service/model/LongTextNoteEvent.kt b/app/src/main/java/com/vitorpamplona/amethyst/service/model/LongTextNoteEvent.kt index 46f136248..529c96fd3 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/service/model/LongTextNoteEvent.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/service/model/LongTextNoteEvent.kt @@ -12,9 +12,9 @@ class LongTextNoteEvent( tags: List>, content: String, sig: HexKey -) : BaseTextNoteEvent(id, pubKey, createdAt, kind, tags, content, sig) { - fun dTag() = tags.filter { it.firstOrNull() == "d" }.mapNotNull { it.getOrNull(1) }.firstOrNull() ?: "" - fun address() = ATag(kind, pubKey, dTag(), null) +) : BaseTextNoteEvent(id, pubKey, createdAt, kind, tags, content, sig), AddressableEvent { + override fun dTag() = tags.filter { it.firstOrNull() == "d" }.mapNotNull { it.getOrNull(1) }.firstOrNull() ?: "" + override fun address() = ATag(kind, pubKey, dTag(), null) fun topics() = tags.filter { it.firstOrNull() == "t" }.mapNotNull { it.getOrNull(1) } fun title() = tags.filter { it.firstOrNull() == "title" }.mapNotNull { it.getOrNull(1) }.firstOrNull() diff --git a/app/src/main/java/com/vitorpamplona/amethyst/service/model/MuteListEvent.kt b/app/src/main/java/com/vitorpamplona/amethyst/service/model/MuteListEvent.kt index a9f639a36..977488836 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/service/model/MuteListEvent.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/service/model/MuteListEvent.kt @@ -15,9 +15,9 @@ class MuteListEvent( tags: List>, content: String, sig: HexKey -) : Event(id, pubKey, createdAt, kind, tags, content, sig) { - fun dTag() = tags.filter { it.firstOrNull() == "d" }.mapNotNull { it.getOrNull(1) }.firstOrNull() ?: "" - fun address() = ATag(kind, pubKey, dTag(), null) +) : Event(id, pubKey, createdAt, kind, tags, content, sig), AddressableEvent { + override fun dTag() = tags.filter { it.firstOrNull() == "d" }.mapNotNull { it.getOrNull(1) }.firstOrNull() ?: "" + override fun address() = ATag(kind, pubKey, dTag(), null) fun plainContent(privKey: ByteArray): String? { return try { diff --git a/app/src/main/java/com/vitorpamplona/amethyst/service/model/ReactionEvent.kt b/app/src/main/java/com/vitorpamplona/amethyst/service/model/ReactionEvent.kt index 13188520c..91b34d4e7 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/service/model/ReactionEvent.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/service/model/ReactionEvent.kt @@ -32,7 +32,7 @@ class ReactionEvent( val pubKey = Utils.pubkeyCreate(privateKey).toHexKey() var tags = listOf(listOf("e", originalNote.id()), listOf("p", originalNote.pubKey())) - if (originalNote is LongTextNoteEvent) { + if (originalNote is AddressableEvent) { tags = tags + listOf(listOf("a", originalNote.address().toTag())) } diff --git a/app/src/main/java/com/vitorpamplona/amethyst/service/model/ReportEvent.kt b/app/src/main/java/com/vitorpamplona/amethyst/service/model/ReportEvent.kt index d3bd9dd97..b05e55870 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/service/model/ReportEvent.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/service/model/ReportEvent.kt @@ -63,7 +63,7 @@ class ReportEvent( val pubKey = Utils.pubkeyCreate(privateKey).toHexKey() var tags: List> = listOf(reportPostTag, reportAuthorTag) - if (reportedPost is LongTextNoteEvent) { + if (reportedPost is AddressableEvent) { tags = tags + listOf(listOf("a", reportedPost.address().toTag())) } diff --git a/app/src/main/java/com/vitorpamplona/amethyst/service/model/RepostEvent.kt b/app/src/main/java/com/vitorpamplona/amethyst/service/model/RepostEvent.kt index d5e0f1eb9..c00c67617 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/service/model/RepostEvent.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/service/model/RepostEvent.kt @@ -36,7 +36,7 @@ class RepostEvent( val pubKey = Utils.pubkeyCreate(privateKey).toHexKey() var tags: List> = boostedPost.tags().plus(listOf(replyToPost, replyToAuthor)) - if (boostedPost is LongTextNoteEvent) { + if (boostedPost is AddressableEvent) { tags = tags + listOf(listOf("a", boostedPost.address().toTag())) } 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 40d1b72c7..909b366a7 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 @@ -1,6 +1,8 @@ package com.vitorpamplona.amethyst.ui.components +import android.graphics.drawable.Drawable import android.net.Uri +import android.util.Log import android.view.View import android.view.ViewGroup import android.widget.FrameLayout @@ -28,6 +30,7 @@ import androidx.compose.runtime.MutableState import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.rememberUpdatedState import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment @@ -46,6 +49,8 @@ import androidx.compose.ui.viewinterop.AndroidView import androidx.core.net.toUri import androidx.lifecycle.Lifecycle import androidx.lifecycle.LifecycleEventObserver +import coil.imageLoader +import coil.request.ImageRequest import com.google.android.exoplayer2.C import com.google.android.exoplayer2.ExoPlayer import com.google.android.exoplayer2.MediaItem @@ -55,6 +60,8 @@ import com.google.android.exoplayer2.source.ProgressiveMediaSource import com.google.android.exoplayer2.ui.AspectRatioFrameLayout import com.google.android.exoplayer2.ui.StyledPlayerView import com.vitorpamplona.amethyst.VideoCache +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch import java.io.File public var muted = mutableStateOf(true) @@ -62,20 +69,55 @@ public var muted = mutableStateOf(true) @Composable fun VideoView(localFile: File?, description: String? = null, onDialog: ((Boolean) -> Unit)? = null) { if (localFile != null) { - VideoView(localFile.toUri(), description, onDialog) + VideoView(localFile.toUri(), description, null, onDialog) } } @Composable fun VideoView(videoUri: String, description: String? = null, onDialog: ((Boolean) -> Unit)? = null) { - VideoView(Uri.parse(videoUri), description, onDialog) + VideoView(Uri.parse(videoUri), description, null, onDialog) } @Composable -fun VideoView(videoUri: Uri, description: String? = null, onDialog: ((Boolean) -> Unit)? = null) { +fun VideoView(videoUri: String, description: String? = null, thumbUri: String, onDialog: ((Boolean) -> Unit)? = null) { + var loadingFinished by remember { mutableStateOf>(Pair(false, null)) } + + val context = LocalContext.current + val scope = rememberCoroutineScope() + + LaunchedEffect(Unit) { + scope.launch(Dispatchers.IO) { + try { + val request = ImageRequest.Builder(context).data(thumbUri).build() + val myCover = context.imageLoader.execute(request).drawable + if (myCover != null) { + loadingFinished = Pair(true, myCover) + } else { + loadingFinished = Pair(true, null) + } + } catch (e: Exception) { + Log.e("VideoView", "Fail to load cover $thumbUri", e) + loadingFinished = Pair(true, null) + } + } + } + + if (loadingFinished.first) { + if (loadingFinished.second != null) { + VideoView(Uri.parse(videoUri), description, loadingFinished.second, onDialog) + } else { + VideoView(videoUri, description, onDialog) + } + } +} + +@Composable +fun VideoView(videoUri: Uri, description: String? = null, thumb: Drawable? = null, onDialog: ((Boolean) -> Unit)? = null) { val context = LocalContext.current val lifecycleOwner = rememberUpdatedState(LocalLifecycleOwner.current) + println("loading audio with artwork $thumb") + val exoPlayer = remember(videoUri) { val mediaBuilder = MediaItem.Builder().setUri(videoUri) @@ -130,6 +172,7 @@ fun VideoView(videoUri: Uri, description: String? = null, onDialog: ((Boolean) - ViewGroup.LayoutParams.WRAP_CONTENT ) controllerAutoShow = false + thumb?.let { defaultArtwork = thumb } hideController() resizeMode = if (maxHeight.isFinite) AspectRatioFrameLayout.RESIZE_MODE_FIT else AspectRatioFrameLayout.RESIZE_MODE_FIXED_WIDTH onDialog?.let { innerOnDialog -> @@ -146,7 +189,6 @@ fun VideoView(videoUri: Uri, description: String? = null, onDialog: ((Boolean) - muted.value = !muted.value } } - ) { val observer = LifecycleEventObserver { _, event -> when (event) { diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/dal/GlobalFeedFilter.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/dal/GlobalFeedFilter.kt index aa37f11ab..e048b2e35 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/dal/GlobalFeedFilter.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/dal/GlobalFeedFilter.kt @@ -27,7 +27,7 @@ object GlobalFeedFilter : AdditiveFeedFilter() { return collection .asSequence() .filter { - it.event is BaseTextNoteEvent && it.replyTo.isNullOrEmpty() + (it.event is BaseTextNoteEvent || it.event is AudioTrackEvent) && it.replyTo.isNullOrEmpty() } .filter { val channel = it.channelHex() diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/dal/HomeNewThreadFeedFilter.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/dal/HomeNewThreadFeedFilter.kt index fcd175020..055513180 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/dal/HomeNewThreadFeedFilter.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/dal/HomeNewThreadFeedFilter.kt @@ -3,6 +3,7 @@ package com.vitorpamplona.amethyst.ui.dal import com.vitorpamplona.amethyst.model.Account import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.amethyst.model.Note +import com.vitorpamplona.amethyst.service.model.AudioTrackEvent import com.vitorpamplona.amethyst.service.model.HighlightEvent import com.vitorpamplona.amethyst.service.model.LongTextNoteEvent import com.vitorpamplona.amethyst.service.model.PollNoteEvent @@ -30,7 +31,7 @@ object HomeNewThreadFeedFilter : AdditiveFeedFilter() { return collection .asSequence() .filter { it -> - (it.event is TextNoteEvent || it.event is RepostEvent || it.event is LongTextNoteEvent || it.event is PollNoteEvent || it.event is HighlightEvent) && + (it.event is TextNoteEvent || it.event is RepostEvent || it.event is LongTextNoteEvent || it.event is PollNoteEvent || it.event is HighlightEvent || it.event is AudioTrackEvent) && (it.author?.pubkeyHex in followingKeySet || (it.event?.isTaggedHashes(followingTagSet) ?: false)) && // && account.isAcceptable(it) // This filter follows only. No need to check if acceptable it.author?.let { !account.isHidden(it.pubkeyHex) } ?: true && 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 d1d224dbb..f028c9025 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 @@ -66,6 +66,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.capitalize import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.text.style.TextOverflow @@ -87,6 +88,7 @@ import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.model.NoteState import com.vitorpamplona.amethyst.model.User +import com.vitorpamplona.amethyst.service.model.AudioTrackEvent import com.vitorpamplona.amethyst.service.model.BadgeAwardEvent import com.vitorpamplona.amethyst.service.model.BadgeDefinitionEvent import com.vitorpamplona.amethyst.service.model.ChannelCreateEvent @@ -97,6 +99,7 @@ import com.vitorpamplona.amethyst.service.model.FileHeaderEvent import com.vitorpamplona.amethyst.service.model.FileStorageHeaderEvent import com.vitorpamplona.amethyst.service.model.HighlightEvent import com.vitorpamplona.amethyst.service.model.LongTextNoteEvent +import com.vitorpamplona.amethyst.service.model.Participant import com.vitorpamplona.amethyst.service.model.PeopleListEvent import com.vitorpamplona.amethyst.service.model.PollNoteEvent import com.vitorpamplona.amethyst.service.model.PrivateDmEvent @@ -113,6 +116,7 @@ import com.vitorpamplona.amethyst.ui.components.RobohashAsyncImage import com.vitorpamplona.amethyst.ui.components.RobohashAsyncImageProxy import com.vitorpamplona.amethyst.ui.components.RobohashFallbackAsyncImage import com.vitorpamplona.amethyst.ui.components.TranslatableRichTextViewer +import com.vitorpamplona.amethyst.ui.components.VideoView import com.vitorpamplona.amethyst.ui.components.ZoomableContent import com.vitorpamplona.amethyst.ui.components.ZoomableContentView import com.vitorpamplona.amethyst.ui.components.ZoomableLocalImage @@ -132,6 +136,7 @@ import nostr.postr.toNpub import java.io.File import java.math.BigDecimal import java.net.URL +import java.util.Locale import kotlin.time.ExperimentalTime import kotlin.time.measureTimedValue @@ -376,6 +381,10 @@ fun NoteComposeInner( RenderPeopleList(noteState, backgroundColor, accountViewModel, navController) } + is AudioTrackEvent -> { + RenderAudioTrack(note, loggedIn, accountViewModel, navController) + } + is PrivateDmEvent -> { RenderPrivateMessage(note, makeItShort, isAcceptableAndCanPreview.second, backgroundColor, accountViewModel, navController) } @@ -868,6 +877,25 @@ private fun RenderRepost( } } +@Composable +private fun RenderAudioTrack( + note: Note, + loggedIn: User, + accountViewModel: AccountViewModel, + navController: NavController +) { + val noteEvent = note.event as? AudioTrackEvent ?: return + + AudioTrackHeader(noteEvent, note, loggedIn, navController) + + ReactionsRow(note, accountViewModel, navController) + + Divider( + modifier = Modifier.padding(top = 10.dp), + thickness = 0.25.dp + ) +} + @Composable private fun RenderLongFormContent( note: Note, @@ -1553,6 +1581,81 @@ fun FileStorageHeaderDisplay(baseNote: Note) { } } +@Composable +fun AudioTrackHeader(noteEvent: AudioTrackEvent, note: Note, loggedIn: User, navController: NavController) { + 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) { + withContext(Dispatchers.IO) { + participantUsers = participants.mapNotNull { part -> LocalCache.checkGetOrCreateUser(part.key)?.let { Pair(part, 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 { + navController.navigate("User/${it.second.pubkeyHex}") + } + ) { + UserPicture(it.second, loggedIn, 25.dp) + 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.colors.onSurface.copy(alpha = 0.32f), + maxLines = 1 + ) + } + } + } + + media?.let { media -> + Row( + verticalAlignment = Alignment.CenterVertically, + modifier = Modifier.padding(10.dp) + ) { + cover?.let { cover -> + VideoView( + videoUri = media, + description = noteEvent.subject(), + thumbUri = cover + ) + } + ?: VideoView( + videoUri = media, + noteEvent.subject() + ) + } + } + } + } +} + @Composable private fun LongFormHeader(noteEvent: LongTextNoteEvent, note: Note, loggedIn: User) { Row( 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 63fb4d985..b0909124e 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 @@ -53,6 +53,7 @@ import androidx.navigation.NavController import coil.compose.AsyncImage import com.vitorpamplona.amethyst.R import com.vitorpamplona.amethyst.model.Note +import com.vitorpamplona.amethyst.service.model.AudioTrackEvent import com.vitorpamplona.amethyst.service.model.BadgeDefinitionEvent import com.vitorpamplona.amethyst.service.model.HighlightEvent import com.vitorpamplona.amethyst.service.model.LongTextNoteEvent @@ -352,6 +353,8 @@ fun NoteMaster( Column() { if (noteEvent is PeopleListEvent) { DisplayPeopleList(noteState, MaterialTheme.colors.background, accountViewModel, navController) + } else if (noteEvent is AudioTrackEvent) { + AudioTrackHeader(noteEvent, note, account.userProfile(), navController) } else if (noteEvent is HighlightEvent) { DisplayHighlight( noteEvent.quote(), From 595cab363021d10246016c81f33773f27e265ba0 Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Mon, 15 May 2023 11:16:50 -0400 Subject: [PATCH 19/22] v0.47.0 --- app/build.gradle | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/build.gradle b/app/build.gradle index 666eae245..a1cf46a63 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -13,8 +13,8 @@ android { applicationId "com.vitorpamplona.amethyst" minSdk 26 targetSdk 33 - versionCode 160 - versionName "0.46.6" + versionCode 161 + versionName "0.47.0" testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" vectorDrawables { From e07b4d49ef46e21676db034df71483ff4561eff5 Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Mon, 15 May 2023 13:10:33 -0400 Subject: [PATCH 20/22] Fixes NPE on opening the QuickAction on a "Post Not Found" event. --- .../com/vitorpamplona/amethyst/ui/note/NoteQuickActionMenu.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 67517b543..6ed81cf8e 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 @@ -128,7 +128,7 @@ fun NoteQuickActionMenu(note: Note, popupExpanded: Boolean, onDismiss: () -> Uni if (popupExpanded) { val isOwnNote = accountViewModel.isLoggedUser(note.author) - val isFollowingUser = !isOwnNote && accountViewModel.isFollowing(note.author!!) + val isFollowingUser = !isOwnNote && accountViewModel.isFollowing(note.author) Popup(onDismissRequest = onDismiss) { Card( From 426a495eb47bc30872686a67b789fdd57b278535 Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Mon, 15 May 2023 13:17:31 -0400 Subject: [PATCH 21/22] Adds zapstr.live and listr.lol as shareable links --- .../vitorpamplona/amethyst/ui/note/NoteQuickActionMenu.kt | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) 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 6ed81cf8e..8cd9c51af 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 @@ -62,6 +62,8 @@ import androidx.core.graphics.ColorUtils import com.vitorpamplona.amethyst.R import com.vitorpamplona.amethyst.model.AddressableNote import com.vitorpamplona.amethyst.model.Note +import com.vitorpamplona.amethyst.service.model.AudioTrackEvent +import com.vitorpamplona.amethyst.service.model.PeopleListEvent import com.vitorpamplona.amethyst.ui.components.SelectTextDialog import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel import com.vitorpamplona.amethyst.ui.screen.loggedIn.ReportNoteDialog @@ -81,8 +83,12 @@ val externalLinkForNote = { note: Note -> if (note is AddressableNote) { if (note.event?.getReward() != null) { "https://nostrbounties.com/b/${note.address().toNAddr()}" + } else if (note.event is PeopleListEvent) { + "https://listr.lol/a/${note.address()?.toNAddr()}" + } else if (note.event is AudioTrackEvent) { + "https://zapstr.live/?track=${note.address()?.toNAddr()}" } else { - "https://habla.news/a/${note.address().toNAddr()}" + "https://habla.news/a/${note.address()?.toNAddr()}" } } else { "https://snort.social/e/${note.toNEvent()}" From 1db6b4793f5e73b439ca7a5863a49c221a5aaf8e Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Mon, 15 May 2023 13:25:21 -0400 Subject: [PATCH 22/22] Adds NIP-94 shareable link to filestr --- .../vitorpamplona/amethyst/ui/note/NoteQuickActionMenu.kt | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) 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 8cd9c51af..02369406b 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 @@ -63,6 +63,7 @@ import com.vitorpamplona.amethyst.R import com.vitorpamplona.amethyst.model.AddressableNote import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.service.model.AudioTrackEvent +import com.vitorpamplona.amethyst.service.model.FileHeaderEvent import com.vitorpamplona.amethyst.service.model.PeopleListEvent import com.vitorpamplona.amethyst.ui.components.SelectTextDialog import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel @@ -91,7 +92,11 @@ val externalLinkForNote = { note: Note -> "https://habla.news/a/${note.address()?.toNAddr()}" } } else { - "https://snort.social/e/${note.toNEvent()}" + if (note.event is FileHeaderEvent) { + "https://filestr.vercel.app/e/${note.toNEvent()}" + } else { + "https://snort.social/e/${note.toNEvent()}" + } } }