From 43c32f0b0ee5f1c1cde41ce396cdb3e8207c6515 Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Mon, 21 Aug 2023 13:51:03 -0400 Subject: [PATCH 01/29] Moves last scope to a class-bound variable. --- .../vitorpamplona/amethyst/service/NostrDataSource.kt | 9 ++++++--- 1 file changed, 6 insertions(+), 3 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 0c4e6fb31..54e4cac10 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/service/NostrDataSource.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/service/NostrDataSource.kt @@ -10,12 +10,15 @@ import com.vitorpamplona.quartz.events.Event import com.vitorpamplona.quartz.utils.TimeUtils import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.Job +import kotlinx.coroutines.SupervisorJob +import kotlinx.coroutines.cancel import kotlinx.coroutines.launch import java.util.UUID import kotlin.Error abstract class NostrDataSource(val debugName: String) { + private val scope = CoroutineScope(Dispatchers.IO + SupervisorJob()) + private var subscriptions = mapOf() data class Counter(var counter: Int) @@ -76,6 +79,7 @@ abstract class NostrDataSource(val debugName: String) { fun destroy() { stop() Client.unsubscribe(clientListener) + scope.cancel() bundler.cancel() } @@ -117,8 +121,7 @@ abstract class NostrDataSource(val debugName: String) { } fun resetFilters() { - val scope = CoroutineScope(Job() + Dispatchers.IO) - scope.launch { + scope.launch(Dispatchers.IO) { resetFiltersSuspend() } } From 251f4854fc22327bab0400e35d2492363913dc37 Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Mon, 21 Aug 2023 14:23:36 -0400 Subject: [PATCH 02/29] Copies to Clipboard the GiftWrap ID and not the inner message ID --- .../com/vitorpamplona/amethyst/model/Note.kt | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) 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 855e2ed2b..7871305b4 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/model/Note.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/model/Note.kt @@ -72,7 +72,22 @@ open class Note(val idHex: String) { open fun idNote() = id().toNote() open fun toNEvent(): String { - return Nip19.createNEvent(idHex, author?.pubkeyHex, event?.kind(), relays.firstOrNull()) + val myEvent = event + return if (myEvent is WrappedEvent) { + val host = myEvent.host + if (host != null) { + Nip19.createNEvent( + host.id, + host.pubKey, + host.kind(), + relays.firstOrNull() + ) + } else { + Nip19.createNEvent(idHex, author?.pubkeyHex, event?.kind(), relays.firstOrNull()) + } + } else { + Nip19.createNEvent(idHex, author?.pubkeyHex, event?.kind(), relays.firstOrNull()) + } } fun toNostrUri(): String { From 9777c709a14a9c67c921bfcdddbc69c3191ccb5e Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Mon, 21 Aug 2023 15:42:45 -0400 Subject: [PATCH 03/29] - Fixes some of the TopBar padding issues. - Adds more info tap on single-user chat rooms - Moves creation date to a new line. --- .../amethyst/ui/navigation/AppTopBar.kt | 32 ++++++-- .../amethyst/ui/note/NoteCompose.kt | 67 +++++++++------- .../amethyst/ui/note/UserCompose.kt | 11 +-- .../ui/screen/loggedIn/ChannelScreen.kt | 78 ++++++++++++------- .../ui/screen/loggedIn/ChatroomScreen.kt | 26 ++++--- app/src/main/res/values/strings.xml | 2 + 6 files changed, 133 insertions(+), 83 deletions(-) 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 89c4e19eb..6691b02ea 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 @@ -91,6 +91,7 @@ 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.screen.equalImmutableLists import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel @@ -108,7 +109,6 @@ import com.vitorpamplona.amethyst.ui.screen.loggedIn.SpinnerSelectionDialog import com.vitorpamplona.amethyst.ui.theme.BottomTopHeight 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.Size22Modifier import com.vitorpamplona.amethyst.ui.theme.Size34dp import com.vitorpamplona.amethyst.ui.theme.Size40dp @@ -231,7 +231,7 @@ private fun CommunityTopBar( ShortCommunityHeader(baseNote, fontWeight = FontWeight.Medium, accountViewModel, nav) }, extendableRow = { - LongCommunityHeader(baseNote, accountViewModel, nav) + LongCommunityHeader(baseNote = baseNote, accountViewModel = accountViewModel, nav = nav) }, popBack = navPopBack ) @@ -297,6 +297,17 @@ private fun RenderRoomTopBar( } } }, + extendableRow = { + LoadUser(baseUserHex = room.users.first()) { + if (it != null) { + UserCompose( + baseUser = it, + accountViewModel = accountViewModel, + nav = nav + ) + } + } + }, popBack = navPopBack ) } else { @@ -308,10 +319,17 @@ private fun RenderRoomTopBar( size = Size34dp ) - RoomNameOnlyDisplay(room, Modifier.padding(start = 10.dp).weight(1f), fontWeight = FontWeight.Medium, accountViewModel.userProfile()) + RoomNameOnlyDisplay( + room, + Modifier + .padding(start = 10.dp) + .weight(1f), + fontWeight = FontWeight.Medium, + accountViewModel.userProfile() + ) }, extendableRow = { - LongRoomHeader(room, accountViewModel, nav) + LongRoomHeader(room = room, accountViewModel = accountViewModel, nav = nav) }, popBack = navPopBack ) @@ -342,7 +360,7 @@ private fun ChannelTopBar( ) }, extendableRow = { - LongChannelHeader(baseChannel, accountViewModel, nav) + LongChannelHeader(baseChannel = baseChannel, accountViewModel = accountViewModel, nav = nav) }, popBack = navPopBack ) @@ -861,9 +879,7 @@ fun MyExtensibleTopAppBar( if (expanded.value && extendableRow != null) { Row( - Modifier - .fillMaxWidth() - .padding(bottom = Size10dp), + Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.Start, verticalAlignment = Alignment.CenterVertically ) { 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 2a0179820..6ef5f5c5d 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 @@ -113,6 +113,7 @@ import com.vitorpamplona.amethyst.ui.screen.loggedIn.ChannelHeader 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.ButtonBorder import com.vitorpamplona.amethyst.ui.theme.DividerThickness @@ -124,6 +125,7 @@ import com.vitorpamplona.amethyst.ui.theme.HalfStartPadding import com.vitorpamplona.amethyst.ui.theme.HalfVertSpacer 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.Size20Modifier @@ -517,7 +519,7 @@ fun CommunityHeader( Column(Modifier.fillMaxWidth()) { Column( verticalArrangement = Arrangement.Center, - modifier = modifier.clickable { + modifier = Modifier.clickable { if (sendToCommunity) { routeFor(baseNote, accountViewModel.userProfile())?.let { nav(it) @@ -534,7 +536,7 @@ fun CommunityHeader( ) if (expanded.value) { - LongCommunityHeader(baseNote, accountViewModel, nav) + LongCommunityHeader(baseNote = baseNote, lineModifier = modifier, accountViewModel = accountViewModel, nav = nav) } } @@ -547,23 +549,24 @@ fun CommunityHeader( } @Composable -fun LongCommunityHeader(baseNote: AddressableNote, accountViewModel: AccountViewModel, nav: (String) -> Unit) { +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( - Modifier - .fillMaxWidth() - .padding(top = 10.dp) + lineModifier ) { val summary = remember(noteState) { noteEvent.description()?.ifBlank { null } } Column( - Modifier - .weight(1f) - .padding(start = 10.dp) + Modifier.weight(1f) ) { Row(verticalAlignment = Alignment.CenterVertically) { val defaultBackground = MaterialTheme.colors.background @@ -598,17 +601,14 @@ fun LongCommunityHeader(baseNote: AddressableNote, accountViewModel: AccountView } rules?.let { - Spacer(DoubleVertSpacer) Row( - Modifier - .fillMaxWidth() - .padding(start = 10.dp) + lineModifier ) { val defaultBackground = MaterialTheme.colors.background val background = remember { mutableStateOf(defaultBackground) } - val tags = remember(noteEvent) { noteEvent?.tags()?.toImmutableListOfLists() ?: ImmutableListOfLists() } + val tags = remember(noteEvent) { noteEvent.tags().toImmutableListOfLists() } TranslatableRichTextViewer( content = it, @@ -621,12 +621,8 @@ fun LongCommunityHeader(baseNote: AddressableNote, accountViewModel: AccountView } } - Spacer(DoubleVertSpacer) - Row( - Modifier - .fillMaxWidth() - .padding(start = 10.dp), + lineModifier, verticalAlignment = Alignment.CenterVertically ) { Text( @@ -639,8 +635,6 @@ fun LongCommunityHeader(baseNote: AddressableNote, accountViewModel: AccountView NoteAuthorPicture(baseNote, nav, accountViewModel, Size25dp) Spacer(DoubleHorzSpacer) NoteUsernameDisplay(baseNote, remember { Modifier.weight(1f) }) - TimeAgo(baseNote) - MoreOptionsButton(baseNote, accountViewModel) } var participantUsers by remember(baseNote) { @@ -664,12 +658,9 @@ fun LongCommunityHeader(baseNote: AddressableNote, accountViewModel: AccountView participantUsers.forEach { Row( - Modifier - .fillMaxWidth() - .padding(start = 10.dp, top = 10.dp) - .clickable { - nav("User/${it.second.pubkeyHex}") - }, + lineModifier.clickable { + nav("User/${it.second.pubkeyHex}") + }, verticalAlignment = Alignment.CenterVertically ) { it.first.role?.let { it1 -> @@ -686,6 +677,21 @@ fun LongCommunityHeader(baseNote: AddressableNote, accountViewModel: AccountView 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, accountViewModel) + } } @Composable @@ -3424,7 +3430,12 @@ fun AudioHeader(noteEvent: AudioHeaderEvent, note: Note, accountViewModel: Accou } content?.let { - Row(verticalAlignment = CenterVertically, modifier = Modifier.fillMaxWidth().padding(top = 5.dp)) { + Row( + verticalAlignment = CenterVertically, + modifier = Modifier + .fillMaxWidth() + .padding(top = 5.dp) + ) { TranslatableRichTextViewer( content = it, canPreview = true, 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 620d6c5af..32b53d4ba 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 @@ -13,18 +13,12 @@ import androidx.compose.ui.unit.dp import com.vitorpamplona.amethyst.model.User import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel import com.vitorpamplona.amethyst.ui.theme.Size55dp +import com.vitorpamplona.amethyst.ui.theme.StdPadding @Composable fun UserCompose( baseUser: User, - overallModifier: Modifier = remember { - Modifier - .padding( - start = 12.dp, - end = 12.dp, - top = 10.dp - ) - }, + overallModifier: Modifier = StdPadding, showDiviser: Boolean = true, accountViewModel: AccountViewModel, nav: (String) -> Unit @@ -56,7 +50,6 @@ fun UserCompose( if (showDiviser) { Divider( - modifier = Modifier.padding(top = 10.dp), thickness = 0.25.dp ) } 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 a653717cd..00ad2cc63 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 @@ -105,12 +105,12 @@ 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.TimeAgo 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.routeFor import com.vitorpamplona.amethyst.ui.note.timeAgo +import com.vitorpamplona.amethyst.ui.note.timeAgoShort import com.vitorpamplona.amethyst.ui.screen.NostrChannelFeedViewModel import com.vitorpamplona.amethyst.ui.screen.RefreshingChatroomFeedView import com.vitorpamplona.amethyst.ui.screen.equalImmutableLists @@ -580,7 +580,7 @@ fun ChannelHeader( ) if (expanded.value) { - LongChannelHeader(baseChannel, accountViewModel, nav) + LongChannelHeader(baseChannel = baseChannel, accountViewModel = accountViewModel, nav = nav) } } @@ -722,6 +722,7 @@ fun ShortChannelHeader( @Composable fun LongChannelHeader( baseChannel: Channel, + lineModifier: Modifier = Modifier.padding(horizontal = 10.dp, vertical = 5.dp), accountViewModel: AccountViewModel, nav: (String) -> Unit ) { @@ -731,18 +732,14 @@ fun LongChannelHeader( } ?: return Row( - Modifier - .fillMaxWidth() - .padding(top = 10.dp) + lineModifier ) { val summary = remember(channelState) { channel.summary()?.ifBlank { null } } Column( - Modifier - .weight(1f) - .padding(start = 10.dp) + Modifier.weight(1f) ) { Row(verticalAlignment = Alignment.CenterVertically) { val defaultBackground = MaterialTheme.colors.background @@ -769,7 +766,9 @@ fun LongChannelHeader( } if (baseChannel is LiveActivitiesChannel) { - val hashtags = remember(baseChannel.info) { baseChannel.info?.hashtags()?.toImmutableList() ?: persistentListOf() } + val hashtags = remember(baseChannel.info) { + baseChannel.info?.hashtags()?.toImmutableList() ?: persistentListOf() + } DisplayUncitedHashtags(hashtags, summary ?: "", nav) } } @@ -784,28 +783,37 @@ fun LongChannelHeader( } } - Spacer(DoubleVertSpacer) - - Row( - Modifier - .fillMaxWidth() - .padding(start = 10.dp), - verticalAlignment = Alignment.CenterVertically - ) { - LoadNote(baseNoteHex = channel.idHex) { - it?.let { + LoadNote(baseNoteHex = channel.idHex) { loadingNote -> + loadingNote?.let { note -> + Row( + lineModifier, + verticalAlignment = Alignment.CenterVertically + ) { Text( text = stringResource(id = R.string.owner), maxLines = 1, overflow = TextOverflow.Ellipsis, - modifier = Modifier.width(55.dp) + modifier = Modifier.width(75.dp) ) Spacer(DoubleHorzSpacer) - NoteAuthorPicture(it, nav, accountViewModel, Size25dp) + NoteAuthorPicture(note, nav, accountViewModel, Size25dp) Spacer(DoubleHorzSpacer) - NoteUsernameDisplay(it, remember { Modifier.weight(1f) }) - TimeAgo(it) - MoreOptionsButton(it, accountViewModel) + NoteUsernameDisplay(note, 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(note, remember { Modifier.weight(1f) }) + MoreOptionsButton(note, accountViewModel) } } } @@ -831,9 +839,7 @@ fun LongChannelHeader( participantUsers.forEach { Row( - Modifier - .fillMaxWidth() - .padding(start = 10.dp, top = 10.dp) + lineModifier .clickable { nav("User/${it.second.pubkeyHex}") }, @@ -856,6 +862,24 @@ fun LongChannelHeader( } } +@Composable +fun NormalTimeAgo(baseNote: Note, modifier: Modifier) { + val nowStr = stringResource(id = R.string.now) + + val time by remember(baseNote) { + derivedStateOf { + timeAgoShort(baseNote.createdAt() ?: 0, nowStr) + } + } + + Text( + text = time, + maxLines = 1, + overflow = TextOverflow.Ellipsis, + modifier = modifier + ) +} + @Composable private fun ShortChannelActionOptions( channel: PublicChatChannel, 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 d25c3197e..17c3f4758 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 @@ -5,7 +5,6 @@ import androidx.compose.animation.Crossfade import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxHeight @@ -550,7 +549,7 @@ fun GroupChatroomHeader( } if (expanded.value) { - LongRoomHeader(room, accountViewModel, nav) + LongRoomHeader(room = room, accountViewModel = accountViewModel, nav = nav) } } @@ -687,15 +686,18 @@ fun NewSubjectView(onClose: () -> Unit, accountViewModel: AccountViewModel, room } @Composable -fun LongRoomHeader(room: ChatroomKey, accountViewModel: AccountViewModel, nav: (String) -> Unit) { +fun LongRoomHeader( + room: ChatroomKey, + lineModifier: Modifier = StdPadding, + accountViewModel: AccountViewModel, + nav: (String) -> Unit +) { val list = remember(room) { room.users.toPersistentList() } Row( - modifier = Modifier - .padding(top = 10.dp) - .fillMaxWidth(), + modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.Center, verticalAlignment = Alignment.CenterVertically ) { @@ -712,16 +714,18 @@ fun LongRoomHeader(room: ChatroomKey, accountViewModel: AccountViewModel, nav: ( } LazyColumn( - modifier = Modifier.fillMaxHeight(), - contentPadding = PaddingValues( - bottom = 10.dp - ), + modifier = Modifier, state = rememberLazyListState() ) { itemsIndexed(list, key = { _, item -> item }) { _, item -> LoadUser(baseUserHex = item) { if (it != null) { - UserCompose(baseUser = it, accountViewModel = accountViewModel, nav = nav) + UserCompose( + baseUser = it, + overallModifier = lineModifier, + accountViewModel = accountViewModel, + nav = nav + ) } } } diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 9558dbd7f..834c4a682 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -546,4 +546,6 @@ Copy URL to clipboard Copy Note ID to clipboard + + Created at From 655a563f61233874da7210b0632db7087b77589a Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Mon, 21 Aug 2023 16:09:22 -0400 Subject: [PATCH 04/29] Moves community description and rules to a single Translateable text --- .../amethyst/ui/navigation/AppTopBar.kt | 8 ++- .../amethyst/ui/note/NoteCompose.kt | 55 ++++++++++--------- app/src/main/res/values/strings.xml | 1 + 3 files changed, 36 insertions(+), 28 deletions(-) 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 6691b02ea..b2976ada3 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 @@ -15,6 +15,8 @@ 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.rememberScrollState +import androidx.compose.foundation.verticalScroll import androidx.compose.material.AppBarDefaults import androidx.compose.material.ContentAlpha import androidx.compose.material.Divider @@ -108,6 +110,7 @@ import com.vitorpamplona.amethyst.ui.screen.loggedIn.ShowVideoStreaming import com.vitorpamplona.amethyst.ui.screen.loggedIn.SpinnerSelectionDialog import com.vitorpamplona.amethyst.ui.theme.BottomTopHeight import com.vitorpamplona.amethyst.ui.theme.DoubleHorzSpacer +import com.vitorpamplona.amethyst.ui.theme.HalfVertSpacer import com.vitorpamplona.amethyst.ui.theme.HeaderPictureModifier import com.vitorpamplona.amethyst.ui.theme.Size22Modifier import com.vitorpamplona.amethyst.ui.theme.Size34dp @@ -231,7 +234,9 @@ private fun CommunityTopBar( ShortCommunityHeader(baseNote, fontWeight = FontWeight.Medium, accountViewModel, nav) }, extendableRow = { - LongCommunityHeader(baseNote = baseNote, accountViewModel = accountViewModel, nav = nav) + Column(Modifier.verticalScroll(rememberScrollState())) { + LongCommunityHeader(baseNote = baseNote, accountViewModel = accountViewModel, nav = nav) + } }, popBack = navPopBack ) @@ -698,6 +703,7 @@ fun FlexibleTopBarWithBackButton( }, actions = {} ) + Spacer(modifier = HalfVertSpacer) Divider(thickness = 0.25.dp) } } 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 6ef5f5c5d..aca280ee8 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 @@ -23,9 +23,11 @@ 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.material.Button import androidx.compose.material.ButtonDefaults import androidx.compose.material.Divider @@ -536,7 +538,14 @@ fun CommunityHeader( ) if (expanded.value) { - LongCommunityHeader(baseNote = baseNote, lineModifier = modifier, accountViewModel = accountViewModel, nav = nav) + Column(Modifier.verticalScroll(rememberScrollState())) { + LongCommunityHeader( + baseNote = baseNote, + lineModifier = modifier, + accountViewModel = accountViewModel, + nav = nav + ) + } } } @@ -561,8 +570,25 @@ fun LongCommunityHeader( Row( lineModifier ) { + val rulesLabel = stringResource(id = R.string.rules) val summary = remember(noteState) { - noteEvent.description()?.ifBlank { null } + 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( @@ -596,31 +622,6 @@ fun LongCommunityHeader( } } - val rules = remember(noteState) { - noteEvent.rules()?.ifBlank { null } - } - - rules?.let { - Row( - lineModifier - ) { - val defaultBackground = MaterialTheme.colors.background - val background = remember { - mutableStateOf(defaultBackground) - } - val tags = remember(noteEvent) { noteEvent.tags().toImmutableListOfLists() } - - TranslatableRichTextViewer( - content = it, - canPreview = false, - tags = tags, - backgroundColor = background, - accountViewModel = accountViewModel, - nav = nav - ) - } - } - Row( lineModifier, verticalAlignment = Alignment.CenterVertically diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 834c4a682..9d1c8d733 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -548,4 +548,5 @@ Copy Note ID to clipboard Created at + Rules From a9af2d8982e39fc4460c909dcc5c542b29f08d39 Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Mon, 21 Aug 2023 17:30:37 -0400 Subject: [PATCH 05/29] v0.74.4 --- app/build.gradle | 4 ++-- .../java/com/vitorpamplona/amethyst/ui/note/ReactionsRow.kt | 2 -- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/app/build.gradle b/app/build.gradle index e92f7687f..b8bd4b10b 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -13,8 +13,8 @@ android { applicationId "com.vitorpamplona.amethyst" minSdk 26 targetSdk 34 - versionCode 276 - versionName "0.74.3" + versionCode 277 + versionName "0.74.4" testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" vectorDrawables { 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 be0802cb5..30de27007 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 @@ -114,7 +114,6 @@ import java.math.BigDecimal import java.math.RoundingMode import java.text.DecimalFormat import kotlin.math.roundToInt -import kotlin.time.ExperimentalTime import kotlin.time.measureTimedValue @Composable @@ -141,7 +140,6 @@ fun ReactionsRow( Spacer(modifier = HalfDoubleVertSpacer) } -@OptIn(ExperimentalTime::class) @Composable private fun InnerReactionRow( baseNote: Note, From 2bedd79e6f05c1ffa4470f42b01787b381857302 Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Mon, 21 Aug 2023 18:17:21 -0400 Subject: [PATCH 06/29] Fixing search for root to go backwards in the etag-stack. --- .../java/com/vitorpamplona/amethyst/model/ThreadAssembler.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/main/java/com/vitorpamplona/amethyst/model/ThreadAssembler.kt b/app/src/main/java/com/vitorpamplona/amethyst/model/ThreadAssembler.kt index 48a1be853..ea5c04904 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/model/ThreadAssembler.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/model/ThreadAssembler.kt @@ -22,7 +22,7 @@ class ThreadAssembler { } } - val hasNoReplyTo = note.replyTo?.firstOrNull { it.replyTo?.isEmpty() == true } + val hasNoReplyTo = note.replyTo?.reversed()?.firstOrNull { it.replyTo?.isEmpty() == true } if (hasNoReplyTo != null) return hasNoReplyTo // recursive From ca9951f9b8858c97b67dd99ad9adeedf2685a2a9 Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Mon, 21 Aug 2023 18:19:10 -0400 Subject: [PATCH 07/29] Fixes the loading of GiftWraps as intents in the app. --- .../ui/screen/loggedIn/LoadRedirectScreen.kt | 16 +++++++++++++--- 1 file changed, 13 insertions(+), 3 deletions(-) diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/LoadRedirectScreen.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/LoadRedirectScreen.kt index 40ef07cef..68e92658b 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/LoadRedirectScreen.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/LoadRedirectScreen.kt @@ -25,6 +25,8 @@ import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.ui.navigation.Route import com.vitorpamplona.quartz.events.ChannelCreateEvent import com.vitorpamplona.quartz.events.ChatroomKeyable +import com.vitorpamplona.quartz.events.GiftWrapEvent +import com.vitorpamplona.quartz.events.SealedGossipEvent import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch import kotlinx.coroutines.withContext @@ -75,9 +77,17 @@ fun LoadRedirectScreen(baseNote: Note, accountViewModel: AccountViewModel, nav: LaunchedEffect(key1 = noteState) { scope.launch { - val note = noteState?.note - val event = note?.event - val channelHex = note?.channelHex() + val note = noteState?.note ?: return@launch + var event = note.event + val channelHex = note.channelHex() + + if (event is GiftWrapEvent) { + event = accountViewModel.unwrap(event) + } + + if (event is SealedGossipEvent) { + event = accountViewModel.unseal(event) + } if (event == null) { // stay here, loading From a1f116909fd0ffff2108b792b5900abce7db267d Mon Sep 17 00:00:00 2001 From: greenart7c3 Date: Mon, 21 Aug 2023 19:20:14 -0300 Subject: [PATCH 08/29] fix crash when not using play services --- .../service/notifications/PushNotificationUtils.kt | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/app/src/play/java/com/vitorpamplona/amethyst/service/notifications/PushNotificationUtils.kt b/app/src/play/java/com/vitorpamplona/amethyst/service/notifications/PushNotificationUtils.kt index cafb5c564..d965bc905 100644 --- a/app/src/play/java/com/vitorpamplona/amethyst/service/notifications/PushNotificationUtils.kt +++ b/app/src/play/java/com/vitorpamplona/amethyst/service/notifications/PushNotificationUtils.kt @@ -1,5 +1,6 @@ package com.vitorpamplona.amethyst.service.notifications +import android.util.Log import com.google.firebase.messaging.FirebaseMessaging import com.vitorpamplona.amethyst.AccountInfo import kotlinx.coroutines.Dispatchers @@ -8,6 +9,10 @@ import kotlinx.coroutines.tasks.await class PushNotificationUtils { suspend fun init(accounts: List) = with(Dispatchers.IO) { // get user notification token provided by firebase - RegisterAccounts(accounts).go(FirebaseMessaging.getInstance().token.await()) + try { + RegisterAccounts(accounts).go(FirebaseMessaging.getInstance().token.await()) + } catch (e: Exception) { + Log.e("Firebase token", "failed to get firebase token", e) + } } } From 87c19f4232ec8d65781798656498001fa2f6cc30 Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Mon, 21 Aug 2023 18:21:28 -0400 Subject: [PATCH 09/29] BugFix for ClassCastException when the event changes --- .../amethyst/ui/screen/loggedIn/LoadRedirectScreen.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/LoadRedirectScreen.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/LoadRedirectScreen.kt index 68e92658b..d48a598c9 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/LoadRedirectScreen.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/LoadRedirectScreen.kt @@ -95,7 +95,7 @@ fun LoadRedirectScreen(baseNote: Note, accountViewModel: AccountViewModel, nav: nav("Channel/${note.idHex}") } else if (event is ChatroomKeyable) { note.author?.let { - val withKey = (note.event as ChatroomKeyable) + val withKey = (event as ChatroomKeyable) .chatroomKey(accountViewModel.userProfile().pubkeyHex) withContext(Dispatchers.IO) { From d73dec8f0b5dbec153a2b0a8fa1e36d21c91b688 Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Mon, 21 Aug 2023 18:31:00 -0400 Subject: [PATCH 10/29] v0.74.5 --- app/build.gradle | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/build.gradle b/app/build.gradle index b8bd4b10b..bde0a2df4 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -13,8 +13,8 @@ android { applicationId "com.vitorpamplona.amethyst" minSdk 26 targetSdk 34 - versionCode 277 - versionName "0.74.4" + versionCode 278 + versionName "0.74.5" testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" vectorDrawables { From fa4257ad7d7afa76ccb368b4cf6cadefa39461ba Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Tue, 22 Aug 2023 09:41:40 -0400 Subject: [PATCH 11/29] Separating NIP24 into the encryption NIP 44 and the messaging NIP 24 --- .../amethyst/ImageUploadTesting.kt | 4 +- .../amethyst/RichTextParserTest.kt | 2 +- .../amethyst/benchmark/CryptoBenchmark.kt | 4 +- .../benchmark/GiftWrapReceivingBenchmark.kt | 8 ++-- .../vitorpamplona/quartz/CryptoUtilsTest.kt | 20 ++++----- .../quartz/crypto/CryptoUtils.kt | 42 +++++++++---------- .../quartz/events/GiftWrapEvent.kt | 6 +-- .../quartz/events/SealedGossipEvent.kt | 6 +-- 8 files changed, 46 insertions(+), 46 deletions(-) diff --git a/app/src/androidTest/java/com/vitorpamplona/amethyst/ImageUploadTesting.kt b/app/src/androidTest/java/com/vitorpamplona/amethyst/ImageUploadTesting.kt index 6b1b58f5e..fdc3cc836 100644 --- a/app/src/androidTest/java/com/vitorpamplona/amethyst/ImageUploadTesting.kt +++ b/app/src/androidTest/java/com/vitorpamplona/amethyst/ImageUploadTesting.kt @@ -21,7 +21,7 @@ class ImageUploadTesting { val image = "R0lGODlhPQBEAPeoAJosM//AwO/AwHVYZ/z595kzAP/s7P+goOXMv8+fhw/v739/f+8PD98fH/8mJl+fn/9ZWb8/PzWlwv///6wWGbImAPgTEMImIN9gUFCEm/gDALULDN8PAD6atYdCTX9gUNKlj8wZAKUsAOzZz+UMAOsJAP/Z2ccMDA8PD/95eX5NWvsJCOVNQPtfX/8zM8+QePLl38MGBr8JCP+zs9myn/8GBqwpAP/GxgwJCPny78lzYLgjAJ8vAP9fX/+MjMUcAN8zM/9wcM8ZGcATEL+QePdZWf/29uc/P9cmJu9MTDImIN+/r7+/vz8/P8VNQGNugV8AAF9fX8swMNgTAFlDOICAgPNSUnNWSMQ5MBAQEJE3QPIGAM9AQMqGcG9vb6MhJsEdGM8vLx8fH98AANIWAMuQeL8fABkTEPPQ0OM5OSYdGFl5jo+Pj/+pqcsTE78wMFNGQLYmID4dGPvd3UBAQJmTkP+8vH9QUK+vr8ZWSHpzcJMmILdwcLOGcHRQUHxwcK9PT9DQ0O/v70w5MLypoG8wKOuwsP/g4P/Q0IcwKEswKMl8aJ9fX2xjdOtGRs/Pz+Dg4GImIP8gIH0sKEAwKKmTiKZ8aB/f39Wsl+LFt8dgUE9PT5x5aHBwcP+AgP+WltdgYMyZfyywz78AAAAAAAD///8AAP9mZv///wAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACH5BAEAAKgALAAAAAA9AEQAAAj/AFEJHEiwoMGDCBMqXMiwocAbBww4nEhxoYkUpzJGrMixogkfGUNqlNixJEIDB0SqHGmyJSojM1bKZOmyop0gM3Oe2liTISKMOoPy7GnwY9CjIYcSRYm0aVKSLmE6nfq05QycVLPuhDrxBlCtYJUqNAq2bNWEBj6ZXRuyxZyDRtqwnXvkhACDV+euTeJm1Ki7A73qNWtFiF+/gA95Gly2CJLDhwEHMOUAAuOpLYDEgBxZ4GRTlC1fDnpkM+fOqD6DDj1aZpITp0dtGCDhr+fVuCu3zlg49ijaokTZTo27uG7Gjn2P+hI8+PDPERoUB318bWbfAJ5sUNFcuGRTYUqV/3ogfXp1rWlMc6awJjiAAd2fm4ogXjz56aypOoIde4OE5u/F9x199dlXnnGiHZWEYbGpsAEA3QXYnHwEFliKAgswgJ8LPeiUXGwedCAKABACCN+EA1pYIIYaFlcDhytd51sGAJbo3onOpajiihlO92KHGaUXGwWjUBChjSPiWJuOO/LYIm4v1tXfE6J4gCSJEZ7YgRYUNrkji9P55sF/ogxw5ZkSqIDaZBV6aSGYq/lGZplndkckZ98xoICbTcIJGQAZcNmdmUc210hs35nCyJ58fgmIKX5RQGOZowxaZwYA+JaoKQwswGijBV4C6SiTUmpphMspJx9unX4KaimjDv9aaXOEBteBqmuuxgEHoLX6Kqx+yXqqBANsgCtit4FWQAEkrNbpq7HSOmtwag5w57GrmlJBASEU18ADjUYb3ADTinIttsgSB1oJFfA63bduimuqKB1keqwUhoCSK374wbujvOSu4QG6UvxBRydcpKsav++Ca6G8A6Pr1x2kVMyHwsVxUALDq/krnrhPSOzXG1lUTIoffqGR7Goi2MAxbv6O2kEG56I7CSlRsEFKFVyovDJoIRTg7sugNRDGqCJzJgcKE0ywc0ELm6KBCCJo8DIPFeCWNGcyqNFE06ToAfV0HBRgxsvLThHn1oddQMrXj5DyAQgjEHSAJMWZwS3HPxT/QMbabI/iBCliMLEJKX2EEkomBAUCxRi42VDADxyTYDVogV+wSChqmKxEKCDAYFDFj4OmwbY7bDGdBhtrnTQYOigeChUmc1K3QTnAUfEgGFgAWt88hKA6aCRIXhxnQ1yg3BCayK44EWdkUQcBByEQChFXfCB776aQsG0BIlQgQgE8qO26X1h8cEUep8ngRBnOy74E9QgRgEAC8SvOfQkh7FDBDmS43PmGoIiKUUEGkMEC/PJHgxw0xH74yx/3XnaYRJgMB8obxQW6kL9QYEJ0FIFgByfIL7/IQAlvQwEpnAC7DtLNJCKUoO/w45c44GwCXiAFB/OXAATQryUxdN4LfFiwgjCNYg+kYMIEFkCKDs6PKAIJouyGWMS1FSKJOMRB/BoIxYJIUXFUxNwoIkEKPAgCBZSQHQ1A2EWDfDEUVLyADj5AChSIQW6gu10bE/JG2VnCZGfo4R4d0sdQoBAHhPjhIB94v/wRoRKQWGRHgrhGSQJxCS+0pCZbEhAAOw==" - fun testBase(server: FileServer) { + private fun testBase(server: FileServer) { val bytes = Base64.getDecoder().decode(image) val inputStream = bytes.inputStream() @@ -39,7 +39,7 @@ class ImageUploadTesting { "image/gif", server, onSuccess = { newUrl, contentType -> - println("Uploaded to $url") + println("Uploaded $contentType to $url") url = newUrl countDownLatch.countDown() }, diff --git a/app/src/androidTest/java/com/vitorpamplona/amethyst/RichTextParserTest.kt b/app/src/androidTest/java/com/vitorpamplona/amethyst/RichTextParserTest.kt index c21fc4ec1..b6a58ba2a 100644 --- a/app/src/androidTest/java/com/vitorpamplona/amethyst/RichTextParserTest.kt +++ b/app/src/androidTest/java/com/vitorpamplona/amethyst/RichTextParserTest.kt @@ -4128,7 +4128,7 @@ https://nostr.build/i/fd53fcf5ad950fbe45127e4bcee1b59e8301d41de6beee211f45e344db } private fun printStateForDebug(state: RichTextViewerState) { - state.paragraphs.forEachIndexed { index, paragraph -> + state.paragraphs.forEach { paragraph -> paragraph.words.forEach { seg -> println( "\"${ diff --git a/benchmark/src/androidTest/java/com/vitorpamplona/amethyst/benchmark/CryptoBenchmark.kt b/benchmark/src/androidTest/java/com/vitorpamplona/amethyst/benchmark/CryptoBenchmark.kt index 13e862dc0..371ed51cf 100644 --- a/benchmark/src/androidTest/java/com/vitorpamplona/amethyst/benchmark/CryptoBenchmark.kt +++ b/benchmark/src/androidTest/java/com/vitorpamplona/amethyst/benchmark/CryptoBenchmark.kt @@ -38,7 +38,7 @@ class CryptoBenchmark { val keyPair2 = KeyPair() benchmarkRule.measureRepeated { - assertNotNull(CryptoUtils.getSharedSecretNIP24(keyPair1.privKey!!, keyPair2.pubKey)) + assertNotNull(CryptoUtils.getSharedSecretNIP44(keyPair1.privKey!!, keyPair2.pubKey)) } } @@ -58,7 +58,7 @@ class CryptoBenchmark { val keyPair2 = KeyPair() benchmarkRule.measureRepeated { - assertNotNull(CryptoUtils.computeSharedSecretNIP24(keyPair1.privKey!!, keyPair2.pubKey)) + assertNotNull(CryptoUtils.computeSharedSecretNIP44(keyPair1.privKey!!, keyPair2.pubKey)) } } diff --git a/benchmark/src/androidTest/java/com/vitorpamplona/amethyst/benchmark/GiftWrapReceivingBenchmark.kt b/benchmark/src/androidTest/java/com/vitorpamplona/amethyst/benchmark/GiftWrapReceivingBenchmark.kt index f4f634346..c3faf87f4 100644 --- a/benchmark/src/androidTest/java/com/vitorpamplona/amethyst/benchmark/GiftWrapReceivingBenchmark.kt +++ b/benchmark/src/androidTest/java/com/vitorpamplona/amethyst/benchmark/GiftWrapReceivingBenchmark.kt @@ -166,7 +166,7 @@ class GiftWrapReceivingBenchmark { val toDecrypt = decodeNIP44(wrappedEvent.content) ?: return benchmarkRule.measureRepeated { - assertNotNull(CryptoUtils.decryptNIP24(toDecrypt, sender.privKey!!, wrappedEvent.pubKey.hexToByteArray())) + assertNotNull(CryptoUtils.decryptNIP44(toDecrypt, sender.privKey!!, wrappedEvent.pubKey.hexToByteArray())) } } @@ -191,7 +191,7 @@ class GiftWrapReceivingBenchmark { ) val toDecrypt = decodeNIP44(wrappedEvent.content) ?: return - val innerJson = CryptoUtils.decryptNIP24(toDecrypt, sender.privKey!!, wrappedEvent.pubKey.hexToByteArray()) + val innerJson = CryptoUtils.decryptNIP44(toDecrypt, sender.privKey!!, wrappedEvent.pubKey.hexToByteArray()) benchmarkRule.measureRepeated { assertNotNull(innerJson?.let { Event.fromJson(it) }) @@ -236,7 +236,7 @@ class GiftWrapReceivingBenchmark { val toDecrypt = decodeNIP44(seal.content) ?: return benchmarkRule.measureRepeated { - assertNotNull(CryptoUtils.decryptNIP24(toDecrypt, sender.privKey!!, seal.pubKey.hexToByteArray())) + assertNotNull(CryptoUtils.decryptNIP44(toDecrypt, sender.privKey!!, seal.pubKey.hexToByteArray())) } } @@ -256,7 +256,7 @@ class GiftWrapReceivingBenchmark { ) val toDecrypt = decodeNIP44(seal.content) ?: return - val innerJson = CryptoUtils.decryptNIP24(toDecrypt, sender.privKey!!, seal.pubKey.hexToByteArray()) + val innerJson = CryptoUtils.decryptNIP44(toDecrypt, sender.privKey!!, seal.pubKey.hexToByteArray()) benchmarkRule.measureRepeated { assertNotNull(innerJson?.let { Gossip.fromJson(it) }) diff --git a/quartz/src/androidTest/java/com/vitorpamplona/quartz/CryptoUtilsTest.kt b/quartz/src/androidTest/java/com/vitorpamplona/quartz/CryptoUtilsTest.kt index 15810d3fb..905f48e95 100644 --- a/quartz/src/androidTest/java/com/vitorpamplona/quartz/CryptoUtilsTest.kt +++ b/quartz/src/androidTest/java/com/vitorpamplona/quartz/CryptoUtilsTest.kt @@ -24,7 +24,7 @@ class CryptoUtilsTest { val privateKey = "f410f88bcec6cbfda04d6a273c7b1dd8bba144cd45b71e87109cfa11dd7ed561" val publicKey = "765cd7cf91d3ad07423d114d5a39c61d52b2cdbc18ba055ddbbeec71fbe2aa2f" - val key = CryptoUtils.getSharedSecretNIP24(privateKey = privateKey.hexToByteArray(), pubKey = publicKey.hexToByteArray()) + val key = CryptoUtils.getSharedSecretNIP44(privateKey = privateKey.hexToByteArray(), pubKey = publicKey.hexToByteArray()) assertEquals("577c966f499dddd8e8dcc34e8f352e283cc177e53ae372794947e0b8ede7cfd8", key.toHexKey()) } @@ -34,8 +34,8 @@ class CryptoUtilsTest { val sender = KeyPair() val receiver = KeyPair() - val sharedSecret1 = CryptoUtils.getSharedSecretNIP24(sender.privKey!!, receiver.pubKey) - val sharedSecret2 = CryptoUtils.getSharedSecretNIP24(receiver.privKey!!, sender.pubKey) + val sharedSecret1 = CryptoUtils.getSharedSecretNIP44(sender.privKey!!, receiver.pubKey) + val sharedSecret2 = CryptoUtils.getSharedSecretNIP44(receiver.privKey!!, sender.pubKey) assertEquals(sharedSecret1.toHexKey(), sharedSecret2.toHexKey()) @@ -73,14 +73,14 @@ class CryptoUtilsTest { } @Test - fun encryptDecryptNIP24Test() { + fun encryptDecryptNIP44Test() { val msg = "Hi" val privateKey = CryptoUtils.privkeyCreate() val publicKey = CryptoUtils.pubkeyCreate(privateKey) - val encrypted = CryptoUtils.encryptNIP24(msg, privateKey, publicKey) - val decrypted = CryptoUtils.decryptNIP24(encrypted, privateKey, publicKey) + val encrypted = CryptoUtils.encryptNIP44(msg, privateKey, publicKey) + val decrypted = CryptoUtils.decryptNIP44(encrypted, privateKey, publicKey) assertEquals(msg, decrypted) } @@ -99,15 +99,15 @@ class CryptoUtilsTest { } @Test - fun encryptSharedSecretDecryptNIP24Test() { + fun encryptSharedSecretDecryptNIP44Test() { val msg = "Hi" val privateKey = CryptoUtils.privkeyCreate() val publicKey = CryptoUtils.pubkeyCreate(privateKey) - val sharedSecret = CryptoUtils.getSharedSecretNIP24(privateKey, publicKey) + val sharedSecret = CryptoUtils.getSharedSecretNIP44(privateKey, publicKey) - val encrypted = CryptoUtils.encryptNIP24(msg, sharedSecret) - val decrypted = CryptoUtils.decryptNIP24(encrypted, sharedSecret) + val encrypted = CryptoUtils.encryptNIP44(msg, sharedSecret) + val decrypted = CryptoUtils.decryptNIP44(encrypted, sharedSecret) assertEquals(msg, decrypted) } diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/crypto/CryptoUtils.kt b/quartz/src/main/java/com/vitorpamplona/quartz/crypto/CryptoUtils.kt index b2b3a89ac..ccf3f0a34 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/crypto/CryptoUtils.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/crypto/CryptoUtils.kt @@ -17,7 +17,7 @@ import javax.crypto.spec.SecretKeySpec object CryptoUtils { private val sharedKeyCache04 = LruCache(200) - private val sharedKeyCache24 = LruCache(200) + private val sharedKeyCache44 = LruCache(200) private val secp256k1 = Secp256k1.get() private val libSodium = SodiumAndroid() @@ -26,7 +26,7 @@ object CryptoUtils { fun clearCache() { sharedKeyCache04.evictAll() - sharedKeyCache24.evictAll() + sharedKeyCache44.evictAll() } fun randomInt(bound: Int): Int { @@ -115,12 +115,12 @@ object CryptoUtils { return String(cipher.doFinal(encryptedMsg)) } - fun encryptNIP24(msg: String, privateKey: ByteArray, pubKey: ByteArray): EncryptedInfo { - val sharedSecret = getSharedSecretNIP24(privateKey, pubKey) - return encryptNIP24(msg, sharedSecret) + fun encryptNIP44(msg: String, privateKey: ByteArray, pubKey: ByteArray): EncryptedInfo { + val sharedSecret = getSharedSecretNIP44(privateKey, pubKey) + return encryptNIP44(msg, sharedSecret) } - fun encryptNIP24(msg: String, sharedSecret: ByteArray): EncryptedInfo { + fun encryptNIP44(msg: String, sharedSecret: ByteArray): EncryptedInfo { val nonce = ByteArray(24) random.nextBytes(nonce) @@ -134,16 +134,16 @@ object CryptoUtils { return EncryptedInfo( ciphertext = cipher ?: ByteArray(0), nonce = nonce, - v = Nip44Version.NIP24.versionCode + v = Nip44Version.NIP44.versionCode ) } - fun decryptNIP24(encryptedInfo: EncryptedInfo, privateKey: ByteArray, pubKey: ByteArray): String? { - val sharedSecret = getSharedSecretNIP24(privateKey, pubKey) - return decryptNIP24(encryptedInfo, sharedSecret) + fun decryptNIP44(encryptedInfo: EncryptedInfo, privateKey: ByteArray, pubKey: ByteArray): String? { + val sharedSecret = getSharedSecretNIP44(privateKey, pubKey) + return decryptNIP44(encryptedInfo, sharedSecret) } - fun decryptNIP24(encryptedInfo: EncryptedInfo, sharedSecret: ByteArray): String? { + fun decryptNIP44(encryptedInfo: EncryptedInfo, sharedSecret: ByteArray): String? { return cryptoStreamXChaCha20Xor( libSodium = libSodium, messageBytes = encryptedInfo.ciphertext, @@ -174,20 +174,20 @@ object CryptoUtils { /** * @return 32B shared secret */ - fun getSharedSecretNIP24(privateKey: ByteArray, pubKey: ByteArray): ByteArray { + fun getSharedSecretNIP44(privateKey: ByteArray, pubKey: ByteArray): ByteArray { val hash = combinedHashCode(privateKey, pubKey) - val preComputed = sharedKeyCache24[hash] + val preComputed = sharedKeyCache44[hash] if (preComputed != null) return preComputed - val computed = computeSharedSecretNIP24(privateKey, pubKey) - sharedKeyCache24.put(hash, computed) + val computed = computeSharedSecretNIP44(privateKey, pubKey) + sharedKeyCache44.put(hash, computed) return computed } /** * @return 32B shared secret */ - fun computeSharedSecretNIP24(privateKey: ByteArray, pubKey: ByteArray): ByteArray = + fun computeSharedSecretNIP44(privateKey: ByteArray, pubKey: ByteArray): ByteArray = sha256(secp256k1.pubKeyTweakMul(h02 + pubKey, privateKey).copyOfRange(1, 33)) } @@ -196,7 +196,7 @@ data class EncryptedInfoString(val ciphertext: String, val nonce: String, val v: enum class Nip44Version(val versionCode: Int) { NIP04(0), - NIP24(1) + NIP44(1) } @@ -234,10 +234,10 @@ fun decodeByteArray(base64: String): EncryptedInfo? { fun encodeJackson(info: EncryptedInfo): String { return Event.mapper.writeValueAsString( EncryptedInfoString( - v = info.v, - nonce = Base64.getEncoder().encodeToString(info.nonce), - ciphertext = Base64.getEncoder().encodeToString(info.ciphertext) - ) + v = info.v, + nonce = Base64.getEncoder().encodeToString(info.nonce), + ciphertext = Base64.getEncoder().encodeToString(info.ciphertext) + ) ) } diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/events/GiftWrapEvent.kt b/quartz/src/main/java/com/vitorpamplona/quartz/events/GiftWrapEvent.kt index 9c58f89ff..7fd91f1d5 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/events/GiftWrapEvent.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/events/GiftWrapEvent.kt @@ -51,7 +51,7 @@ class GiftWrapEvent( return when (toDecrypt.v) { Nip44Version.NIP04.versionCode -> CryptoUtils.decryptNIP04(toDecrypt, privKey, pubKey.hexToByteArray()) - Nip44Version.NIP24.versionCode -> CryptoUtils.decryptNIP24(toDecrypt, privKey, pubKey.hexToByteArray()) + Nip44Version.NIP44.versionCode -> CryptoUtils.decryptNIP44(toDecrypt, privKey, pubKey.hexToByteArray()) else -> null } } catch (e: Exception) { @@ -71,10 +71,10 @@ class GiftWrapEvent( createdAt: Long = TimeUtils.randomWithinAWeek() ): GiftWrapEvent { val privateKey = CryptoUtils.privkeyCreate() // GiftWrap is always a random key - val sharedSecret = CryptoUtils.getSharedSecretNIP24(privateKey, recipientPubKey.hexToByteArray()) + val sharedSecret = CryptoUtils.getSharedSecretNIP44(privateKey, recipientPubKey.hexToByteArray()) val content = encodeNIP44( - CryptoUtils.encryptNIP24( + CryptoUtils.encryptNIP44( toJson(event), sharedSecret ) diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/events/SealedGossipEvent.kt b/quartz/src/main/java/com/vitorpamplona/quartz/events/SealedGossipEvent.kt index c3eadf865..2eaa23460 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/events/SealedGossipEvent.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/events/SealedGossipEvent.kt @@ -53,7 +53,7 @@ class SealedGossipEvent( return when (toDecrypt.v) { Nip44Version.NIP04.versionCode -> CryptoUtils.decryptNIP04(toDecrypt, privKey, pubKey.hexToByteArray()) - Nip44Version.NIP24.versionCode -> CryptoUtils.decryptNIP24(toDecrypt, privKey, pubKey.hexToByteArray()) + Nip44Version.NIP44.versionCode -> CryptoUtils.decryptNIP44(toDecrypt, privKey, pubKey.hexToByteArray()) else -> null } } catch (e: Exception) { @@ -81,10 +81,10 @@ class SealedGossipEvent( privateKey: ByteArray, createdAt: Long = TimeUtils.randomWithinAWeek() ): SealedGossipEvent { - val sharedSecret = CryptoUtils.getSharedSecretNIP24(privateKey, encryptTo.hexToByteArray()) + val sharedSecret = CryptoUtils.getSharedSecretNIP44(privateKey, encryptTo.hexToByteArray()) val content = encodeNIP44( - CryptoUtils.encryptNIP24( + CryptoUtils.encryptNIP44( Gossip.toJson(gossip), sharedSecret ) From 521407f87926dcc93288bba02fa4a9e32e51939e Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Tue, 22 Aug 2023 10:07:40 -0400 Subject: [PATCH 12/29] Updates Gradle plugin --- build.gradle | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/build.gradle b/build.gradle index 73dba8106..ceb00b794 100644 --- a/build.gradle +++ b/build.gradle @@ -18,8 +18,8 @@ buildscript { } }// Top-level build file where you can add configuration options common to all sub-projects/modules. plugins { - id 'com.android.application' version '8.1.0' apply false - id 'com.android.library' version '8.1.0' apply false + id 'com.android.application' version '8.1.1' apply false + id 'com.android.library' version '8.1.1' apply false id 'org.jetbrains.kotlin.android' version '1.9.0' apply false id 'org.jetbrains.kotlin.jvm' version '1.9.0' apply false id 'androidx.benchmark' version '1.1.1' apply false From 98c55661e86672f1c8993c5c3c5e98832a931db9 Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Tue, 22 Aug 2023 11:05:17 -0400 Subject: [PATCH 13/29] Migrates to Compose 1.5.0 --- app/build.gradle | 2 +- .../amethyst/ui/actions/NewPostView.kt | 2 +- .../amethyst/ui/components/SlidingCarousel.kt | 8 ++-- .../ui/components/ZoomableContentView.kt | 12 ++--- .../amethyst/ui/note/ChannelCardCompose.kt | 2 +- .../amethyst/ui/note/ReactionsRow.kt | 8 ++-- .../ui/screen/RememberForeverStates.kt | 25 ++++++---- .../ui/screen/loggedIn/BookmarkListScreen.kt | 4 +- .../ui/screen/loggedIn/ChatroomListScreen.kt | 3 +- .../ui/screen/loggedIn/DiscoverScreen.kt | 24 +++++----- .../ui/screen/loggedIn/HiddenUsersScreen.kt | 4 +- .../amethyst/ui/screen/loggedIn/HomeScreen.kt | 4 +- .../ui/screen/loggedIn/ProfileScreen.kt | 10 ++-- .../ui/screen/loggedIn/VideoScreen.kt | 46 ++++++++++++------- build.gradle | 3 +- quartz/build.gradle | 2 +- 16 files changed, 85 insertions(+), 74 deletions(-) diff --git a/app/build.gradle b/app/build.gradle index bde0a2df4..229e270ba 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -88,7 +88,7 @@ android { dependencies { implementation project(path: ':quartz') - implementation 'androidx.core:core-ktx:1.10.1' + implementation "androidx.core:core-ktx:$core_ktx_version" implementation 'androidx.activity:activity-compose:1.7.2' implementation "androidx.compose.ui:ui:$compose_ui_version" implementation "androidx.compose.ui:ui-tooling-preview:$compose_ui_version" 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 b7e818af8..346e71377 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 @@ -886,7 +886,7 @@ fun DisplayLocationInTitle(geohash: String) { fun Notifying(baseMentions: ImmutableList?, onClick: (User) -> Unit) { val mentions = baseMentions?.toSet() - FlowRow(verticalAlignment = Alignment.CenterVertically, modifier = Modifier.padding(horizontal = 10.dp)) { + FlowRow(verticalArrangement = Arrangement.Center, modifier = Modifier.padding(horizontal = 10.dp)) { if (!mentions.isNullOrEmpty()) { Text( stringResource(R.string.reply_notify), diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/components/SlidingCarousel.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/components/SlidingCarousel.kt index bd88d47d2..38f1f4f88 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/components/SlidingCarousel.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/components/SlidingCarousel.kt @@ -18,7 +18,6 @@ import androidx.compose.material.MaterialTheme import androidx.compose.material.Surface import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue -import androidx.compose.runtime.remember import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip @@ -30,9 +29,8 @@ import com.vitorpamplona.amethyst.ui.theme.placeholderText @OptIn(ExperimentalFoundationApi::class) @Composable fun SlidingCarousel( + pagerState: PagerState, modifier: Modifier = Modifier, - pagerState: PagerState = remember { PagerState() }, - itemsCount: Int, itemContent: @Composable (index: Int) -> Unit ) { val isDragged by pagerState.interactionSource.collectIsDraggedAsState() @@ -40,7 +38,7 @@ fun SlidingCarousel( Box( modifier = modifier.fillMaxWidth() ) { - HorizontalPager(pageCount = itemsCount, state = pagerState) { page -> + HorizontalPager(state = pagerState) { page -> itemContent(page) } @@ -55,7 +53,7 @@ fun SlidingCarousel( ) { DotsIndicator( modifier = Modifier.padding(horizontal = 8.dp, vertical = 6.dp), - totalDots = itemsCount, + totalDots = pagerState.pageCount, selectedIndex = if (isDragged) pagerState.currentPage else pagerState.targetPage, dotSize = 8.dp ) 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 9e11efe18..373f24214 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 @@ -637,7 +637,7 @@ fun ZoomableImageDialog( ) { Surface(modifier = Modifier.fillMaxSize(), color = MaterialTheme.colors.background) { Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.TopCenter) { - val pagerState: PagerState = rememberPagerState() + val pagerState: PagerState = rememberPagerState() { allImages.size } LaunchedEffect(key1 = pagerState, key2 = imageUrl) { val page = allImages.indexOf(imageUrl) @@ -648,12 +648,10 @@ fun ZoomableImageDialog( if (allImages.size > 1) { SlidingCarousel( - pagerState = pagerState, - itemsCount = allImages.size, - itemContent = { index -> - RenderImageOrVideo(allImages[index], false, accountViewModel) - } - ) + pagerState = pagerState + ) { index -> + RenderImageOrVideo(allImages[index], false, accountViewModel) + } } else { RenderImageOrVideo(imageUrl, false, accountViewModel) } 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 6a7e79ad1..c6aef6fad 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 @@ -770,7 +770,7 @@ fun RenderChannelThumb(baseNote: Note, channel: Channel, accountViewModel: Accou @OptIn(ExperimentalLayoutApi::class) @Composable fun Gallery(users: List, accountViewModel: AccountViewModel) { - FlowRow(verticalAlignment = CenterVertically) { + FlowRow(verticalArrangement = Arrangement.Center) { users.take(6).forEach { ClickableUserPicture(it, Size35dp, accountViewModel) } 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 30de27007..971a4e26b 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 @@ -4,7 +4,7 @@ import android.content.Context import android.util.Log import android.widget.Toast import androidx.compose.animation.AnimatedContent -import androidx.compose.animation.AnimatedContentScope +import androidx.compose.animation.AnimatedContentTransitionScope import androidx.compose.animation.ContentTransform import androidx.compose.animation.Crossfade import androidx.compose.animation.ExperimentalAnimationApi @@ -610,14 +610,14 @@ private fun SlidingAnimationCount(baseCount: MutableState, textColor: Color private fun SlidingAnimationCount(baseCount: Int, textColor: Color) { AnimatedContent( targetState = baseCount, - transitionSpec = AnimatedContentScope::transitionSpec + transitionSpec = AnimatedContentTransitionScope::transitionSpec ) { count -> TextCount(count, textColor) } } @OptIn(ExperimentalAnimationApi::class) -private fun AnimatedContentScope.transitionSpec(): ContentTransform { +private fun AnimatedContentTransitionScope.transitionSpec(): ContentTransform { return slideAnimation } @@ -644,7 +644,7 @@ private fun TextCount(count: Int, textColor: Color) { private fun SlidingAnimationAmount(amount: MutableState, textColor: Color) { AnimatedContent( targetState = amount.value, - transitionSpec = AnimatedContentScope::transitionSpec + transitionSpec = AnimatedContentTransitionScope::transitionSpec ) { count -> Text( text = count, diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/RememberForeverStates.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/RememberForeverStates.kt index 575646ca0..6d5c4ebf7 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/RememberForeverStates.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/RememberForeverStates.kt @@ -3,8 +3,10 @@ package com.vitorpamplona.amethyst.ui.screen import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.foundation.lazy.LazyListState import androidx.compose.foundation.pager.PagerState +import androidx.compose.foundation.pager.rememberPagerState import androidx.compose.runtime.Composable import androidx.compose.runtime.DisposableEffect +import androidx.compose.runtime.saveable.Saver import androidx.compose.runtime.saveable.rememberSaveable import com.vitorpamplona.amethyst.ui.navigation.Route import kotlin.math.roundToInt @@ -61,17 +63,19 @@ fun rememberForeverLazyListState( fun rememberForeverPagerState( key: String, initialFirstVisibleItemIndex: Int = 0, - initialFirstVisibleItemScrollOffset: Float = 0.0f + initialFirstVisibleItemScrollOffset: Float = 0.0f, + pageCount: () -> Int ): PagerState { - val scrollState = rememberSaveable(saver = PagerState.Saver) { - val savedValue = savedScrollStates[key] - val savedIndex = savedValue?.index ?: initialFirstVisibleItemIndex - val savedOffset = savedValue?.scrollOffsetFraction ?: initialFirstVisibleItemScrollOffset - PagerState( - savedIndex, - savedOffset - ) - } + val savedValue = savedScrollStates[key] + val savedIndex = savedValue?.index ?: initialFirstVisibleItemIndex + val savedOffset = savedValue?.scrollOffsetFraction ?: initialFirstVisibleItemScrollOffset + + val scrollState = rememberPagerState( + savedIndex, + savedOffset, + pageCount + ) + DisposableEffect(scrollState) { onDispose { val lastIndex = scrollState.currentPage @@ -79,5 +83,6 @@ fun rememberForeverPagerState( savedScrollStates[key] = ScrollState(lastIndex, lastOffset) } } + return scrollState } diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/BookmarkListScreen.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/BookmarkListScreen.kt index 46f65e75c..a8040c539 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/BookmarkListScreen.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/BookmarkListScreen.kt @@ -50,7 +50,7 @@ fun BookmarkListScreen(accountViewModel: AccountViewModel, nav: (String) -> Unit Column(Modifier.fillMaxHeight()) { Column(modifier = Modifier.padding(start = 10.dp, end = 10.dp)) { - val pagerState = rememberPagerState() + val pagerState = rememberPagerState() { 2 } val coroutineScope = rememberCoroutineScope() TabRow( @@ -73,7 +73,7 @@ fun BookmarkListScreen(accountViewModel: AccountViewModel, nav: (String) -> Unit } ) } - HorizontalPager(pageCount = 2, state = pagerState) { page -> + HorizontalPager(state = pagerState) { page -> when (page) { 0 -> RefresheableFeedView( privateFeedViewModel, 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 81c383229..39b60a649 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 @@ -57,7 +57,7 @@ fun ChatroomListScreen( accountViewModel: AccountViewModel, nav: (String) -> Unit ) { - val pagerState = rememberPagerState() + val pagerState = rememberPagerState() { 2 } val coroutineScope = rememberCoroutineScope() var moreActionsExpanded by remember { mutableStateOf(false) } @@ -136,7 +136,6 @@ fun ChatroomListScreen( } HorizontalPager( - pageCount = 2, state = pagerState, modifier = Modifier.fillMaxSize() ) { page -> 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 ed06371c8..65df80ee9 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,17 @@ fun DiscoverScreen( ) { val lifeCycleOwner = LocalLifecycleOwner.current - val pagerState = rememberForeverPagerState(key = PagerStateKeys.DISCOVER_SCREEN) + val tabs by remember(discoveryLiveFeedViewModel, discoveryCommunityFeedViewModel, discoveryChatFeedViewModel) { + mutableStateOf( + listOf( + TabItem(R.string.discover_live, discoveryLiveFeedViewModel, Route.Discover.base + "Live", ScrollStateKeys.DISCOVER_LIVE, LiveActivitiesEvent.kind), + TabItem(R.string.discover_community, discoveryCommunityFeedViewModel, Route.Discover.base + "Community", ScrollStateKeys.DISCOVER_COMMUNITY, CommunityDefinitionEvent.kind), + TabItem(R.string.discover_chat, discoveryChatFeedViewModel, Route.Discover.base + "Chats", ScrollStateKeys.DISCOVER_CHATS, ChannelCreateEvent.kind) + ).toImmutableList() + ) + } + + val pagerState = rememberForeverPagerState(key = PagerStateKeys.DISCOVER_SCREEN) { tabs.size } WatchAccountForDiscoveryScreen( discoveryLiveFeedViewModel = discoveryLiveFeedViewModel, @@ -92,16 +102,6 @@ fun DiscoverScreen( } } - val tabs by remember(discoveryLiveFeedViewModel, discoveryCommunityFeedViewModel, discoveryChatFeedViewModel) { - mutableStateOf( - listOf( - TabItem(R.string.discover_live, discoveryLiveFeedViewModel, Route.Discover.base + "Live", ScrollStateKeys.DISCOVER_LIVE, LiveActivitiesEvent.kind), - TabItem(R.string.discover_community, discoveryCommunityFeedViewModel, Route.Discover.base + "Community", ScrollStateKeys.DISCOVER_COMMUNITY, CommunityDefinitionEvent.kind), - TabItem(R.string.discover_chat, discoveryChatFeedViewModel, Route.Discover.base + "Chats", ScrollStateKeys.DISCOVER_CHATS, ChannelCreateEvent.kind) - ).toImmutableList() - ) - } - Column(Modifier.fillMaxHeight()) { Column( modifier = Modifier.padding(vertical = 0.dp) @@ -139,7 +139,7 @@ private fun DiscoverPages( } } - HorizontalPager(pageCount = 3, state = pagerState) { page -> + HorizontalPager(state = pagerState) { page -> RefresheableView(tabs[page].viewModel, true) { SaveableFeedState(tabs[page].viewModel, scrollStateKey = tabs[page].scrollStateKey) { listState -> RenderDiscoverFeed( 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 5d68bf472..7f78f7bbe 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 @@ -78,7 +78,7 @@ fun HiddenUsersScreen( Column(Modifier.fillMaxHeight()) { Column(modifier = Modifier.padding(start = 10.dp, end = 10.dp)) { - val pagerState = rememberPagerState() + val pagerState = rememberPagerState() { 2 } val coroutineScope = rememberCoroutineScope() var warnAboutReports by remember { mutableStateOf(accountViewModel.account.warnAboutPostsWithReports) } var filterSpam by remember { mutableStateOf(accountViewModel.account.filterSpamFromStrangers) } @@ -129,7 +129,7 @@ fun HiddenUsersScreen( } ) } - HorizontalPager(pageCount = 2, state = pagerState) { page -> + HorizontalPager(state = pagerState) { page -> when (page) { 0 -> RefreshingUserFeedView(hiddenFeedViewModel, accountViewModel, nav) 1 -> RefreshingUserFeedView(spammerFeedViewModel, accountViewModel, nav) 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 73732e409..36de0257a 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 @@ -56,7 +56,7 @@ fun HomeScreen( ) { var wantsToAddNip47 by remember(nip47) { mutableStateOf(nip47) } - val pagerState = rememberForeverPagerState(key = PagerStateKeys.HOME_SCREEN) + val pagerState = rememberForeverPagerState(key = PagerStateKeys.HOME_SCREEN) { 2 } WatchAccountForHomeScreen(homeFeedViewModel, repliesFeedViewModel, accountViewModel) @@ -124,7 +124,7 @@ private fun HomePages( } } - HorizontalPager(pageCount = 2, state = pagerState) { page -> + HorizontalPager(state = pagerState) { page -> RefresheableFeedView( viewModel = tabs[page].viewModel, routeForLastRead = tabs[page].routeForLastRead, 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 2f3468937..1985b72b6 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 @@ -308,7 +308,6 @@ private fun RenderSurface( columnSize = it } ) { - val pagerState = rememberPagerState() val coroutineScope = rememberCoroutineScope() val scrollState = rememberScrollState() @@ -347,7 +346,6 @@ private fun RenderSurface( ) { RenderScreen( baseUser, - pagerState, tabRowModifier, pagerModifier, threadsViewModel, @@ -370,7 +368,6 @@ private fun RenderSurface( @OptIn(ExperimentalFoundationApi::class) private fun RenderScreen( baseUser: User, - pagerState: PagerState, tabRowModifier: Modifier, pagerModifier: Modifier, threadsViewModel: NostrUserProfileNewThreadsFeedViewModel, @@ -384,6 +381,8 @@ private fun RenderScreen( accountViewModel: AccountViewModel, nav: (String) -> Unit ) { + val pagerState = rememberPagerState { 9 } + Column() { ProfileHeader(baseUser, appRecommendations, nav, accountViewModel) ScrollableTabRow( @@ -395,7 +394,6 @@ private fun RenderScreen( CreateAndRenderTabs(baseUser, pagerState) } HorizontalPager( - pageCount = 9, state = pagerState, modifier = pagerModifier ) { page -> @@ -1089,7 +1087,7 @@ private fun DisplayAppRecommendations( Text(stringResource(id = R.string.recommended_apps)) FlowRow( - verticalAlignment = Alignment.CenterVertically, + verticalArrangement = Arrangement.Center, modifier = Modifier.padding(vertical = 5.dp) ) { state.feed.value.forEach { app -> @@ -1174,7 +1172,7 @@ private fun RenderBadgeList( nav: (String) -> Unit ) { FlowRow( - verticalAlignment = Alignment.CenterVertically, + verticalArrangement = Arrangement.Center, modifier = Modifier.padding(vertical = 5.dp) ) { list.forEach { badgeAwardEvent -> 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 2195ee070..81643f5bc 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 @@ -18,6 +18,7 @@ import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.width import androidx.compose.foundation.pager.PagerState import androidx.compose.foundation.pager.VerticalPager +import androidx.compose.foundation.pager.rememberPagerState import androidx.compose.material.Icon import androidx.compose.material.IconButton import androidx.compose.material.MaterialTheme @@ -131,15 +132,7 @@ private fun SaveableFeedState( nav: (String) -> Unit, scrollStateKey: String? = null ) { - val pagerState = if (scrollStateKey != null) { - rememberForeverPagerState(scrollStateKey) - } else { - remember { PagerState() } - } - - WatchScrollToTop(videoFeedView, pagerState) - - RenderPage(videoFeedView, accountViewModel, pagerState, nav) + RenderPage(videoFeedView, accountViewModel, scrollStateKey, nav) } @OptIn(ExperimentalFoundationApi::class) @@ -163,7 +156,7 @@ public fun WatchScrollToTop( fun RenderPage( videoFeedView: NostrVideoFeedViewModel, accountViewModel: AccountViewModel, - pagerState: PagerState, + pagerStateKey: String?, nav: (String) -> Unit ) { val feedState by videoFeedView.feedContent.collectAsState() @@ -184,12 +177,7 @@ fun RenderPage( } is FeedState.Loaded -> { - SlidingCarousel( - state.feed, - pagerState, - accountViewModel, - nav - ) + LoadedState(state, pagerStateKey, videoFeedView, accountViewModel, nav) } is FeedState.Loading -> { @@ -201,6 +189,31 @@ fun RenderPage( } } +@Composable +@OptIn(ExperimentalFoundationApi::class) +private fun LoadedState( + state: FeedState.Loaded, + pagerStateKey: String?, + videoFeedView: NostrVideoFeedViewModel, + accountViewModel: AccountViewModel, + nav: (String) -> Unit +) { + val pagerState = if (pagerStateKey != null) { + rememberForeverPagerState(pagerStateKey) { state.feed.value.size } + } else { + rememberPagerState { state.feed.value.size } + } + + WatchScrollToTop(videoFeedView, pagerState) + + SlidingCarousel( + state.feed, + pagerState, + accountViewModel, + nav + ) +} + @OptIn(ExperimentalFoundationApi::class) @Composable fun SlidingCarousel( @@ -210,7 +223,6 @@ fun SlidingCarousel( nav: (String) -> Unit ) { VerticalPager( - pageCount = feed.value.size, state = pagerState, beyondBoundsPageCount = 1, modifier = Modifier.fillMaxSize(1f), diff --git a/build.gradle b/build.gradle index ceb00b794..ede08fdeb 100644 --- a/build.gradle +++ b/build.gradle @@ -4,7 +4,7 @@ buildscript { ext { fragment_version = "1.6.1" lifecycle_version = '2.6.1' - compose_ui_version = '1.4.3' + compose_ui_version = '1.5.0' nav_version = "2.6.0" room_version = "2.4.3" accompanist_version = '0.30.1' @@ -12,6 +12,7 @@ buildscript { vico_version = '1.7.3' exoplayer_version = '1.1.0' media3_version = '1.1.0' + core_ktx_version = '1.10.1' } dependencies { classpath 'com.google.gms:google-services:4.3.15' diff --git a/quartz/build.gradle b/quartz/build.gradle index 916b6afdc..b3836de73 100644 --- a/quartz/build.gradle +++ b/quartz/build.gradle @@ -32,7 +32,7 @@ android { } dependencies { - implementation 'androidx.core:core-ktx:1.9.0' + implementation "androidx.core:core-ktx:$core_ktx_version" // @Immutable and @Stable implementation "androidx.compose.runtime:runtime:$compose_ui_version" From 9018175f54a5fb1e5493533e130780babb482725 Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Tue, 22 Aug 2023 11:06:10 -0400 Subject: [PATCH 14/29] Removes unecessary import. --- .../vitorpamplona/amethyst/ui/screen/RememberForeverStates.kt | 1 - 1 file changed, 1 deletion(-) diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/RememberForeverStates.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/RememberForeverStates.kt index 6d5c4ebf7..849551e7e 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/RememberForeverStates.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/RememberForeverStates.kt @@ -6,7 +6,6 @@ import androidx.compose.foundation.pager.PagerState import androidx.compose.foundation.pager.rememberPagerState import androidx.compose.runtime.Composable import androidx.compose.runtime.DisposableEffect -import androidx.compose.runtime.saveable.Saver import androidx.compose.runtime.saveable.rememberSaveable import com.vitorpamplona.amethyst.ui.navigation.Route import kotlin.math.roundToInt From 5d352f1b6cb7a9a847125a4e4f85bfe705e13908 Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Tue, 22 Aug 2023 11:16:31 -0400 Subject: [PATCH 15/29] Updates compose navigation, exoplayer and vico dependencies to the latest. --- app/build.gradle | 2 +- build.gradle | 9 +++++---- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/app/build.gradle b/app/build.gradle index 229e270ba..61dca94ee 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -112,7 +112,7 @@ dependencies { implementation "androidx.lifecycle:lifecycle-livedata-ktx:$lifecycle_version" // Zoomable images - implementation "net.engawapg.lib:zoomable:1.4.3" + implementation "net.engawapg.lib:zoomable:1.5.0" // Biometrics implementation "androidx.biometric:biometric-ktx:1.2.0-alpha05" diff --git a/build.gradle b/build.gradle index ede08fdeb..f00613616 100644 --- a/build.gradle +++ b/build.gradle @@ -5,19 +5,20 @@ buildscript { fragment_version = "1.6.1" lifecycle_version = '2.6.1' compose_ui_version = '1.5.0' - nav_version = "2.6.0" + nav_version = "2.7.0" room_version = "2.4.3" accompanist_version = '0.30.1' coil_version = '2.4.0' - vico_version = '1.7.3' - exoplayer_version = '1.1.0' + vico_version = '1.9.2' + exoplayer_version = '1.1.1' media3_version = '1.1.0' core_ktx_version = '1.10.1' } dependencies { classpath 'com.google.gms:google-services:4.3.15' } -}// Top-level build file where you can add configuration options common to all sub-projects/modules. +} + plugins { id 'com.android.application' version '8.1.1' apply false id 'com.android.library' version '8.1.1' apply false From 66b5fad6c2768e82ddf02d50a26585f695bd274d Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Tue, 22 Aug 2023 11:23:11 -0400 Subject: [PATCH 16/29] Saves some loading time by moving the check if notification was already asked forward. --- .../amethyst/ui/screen/loggedIn/NotificationScreen.kt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) 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 645a61cc7..cba29abb1 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 @@ -113,12 +113,12 @@ var hasAlreadyAskedNotificationPermissions = false @OptIn(ExperimentalPermissionsApi::class) @Composable fun CheckifItNeedsToRequestNotificationPermission() { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU && !hasAlreadyAskedNotificationPermissions) { val notificationPermissionState = rememberPermissionState( Manifest.permission.POST_NOTIFICATIONS ) - if (!notificationPermissionState.status.isGranted && !hasAlreadyAskedNotificationPermissions) { + if (!notificationPermissionState.status.isGranted) { hasAlreadyAskedNotificationPermissions = true // This will pause the APP, including the connection with relays. From 1121cfcaf93167c3d25a70e1cd67e8ae998517e4 Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Tue, 22 Aug 2023 12:23:26 -0400 Subject: [PATCH 17/29] BugFix on bundled update dispatchers. --- .../com/vitorpamplona/amethyst/ui/components/BundledUpdate.kt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) 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 d9b5c2feb..41963748f 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 @@ -35,7 +35,7 @@ class BundledUpdate( return } - scope.launch { + scope.launch(dispatcher) { try { onUpdate() delay(delay) @@ -77,7 +77,7 @@ class BundledInsert( return } - scope.launch(Dispatchers.IO) { + scope.launch(dispatcher) { try { val mySet = mutableSetOf() queue.drainTo(mySet) From 0a49298ec2482c07fea671d861defd21471a243c Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Tue, 22 Aug 2023 12:23:45 -0400 Subject: [PATCH 18/29] Faster ReactionRow compositions --- .../amethyst/ui/note/UserReactionsRow.kt | 109 +++++++++--------- 1 file changed, 56 insertions(+), 53 deletions(-) diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/note/UserReactionsRow.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/UserReactionsRow.kt index 68e7b267b..8ae51ed24 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/note/UserReactionsRow.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/UserReactionsRow.kt @@ -57,6 +57,7 @@ import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Job import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.map import kotlinx.coroutines.launch import java.math.BigDecimal import java.time.Instant @@ -109,26 +110,58 @@ fun UserReactionsRow( @Composable private fun UserZapModel(model: UserReactionsViewModel) { - val zaps by model.zaps.collectAsState() - UserZapReaction(showAmountAxis(zaps[model.today()])) + Icon( + imageVector = Icons.Default.Bolt, + contentDescription = stringResource(R.string.zaps), + modifier = Size24Modifier, + tint = BitcoinOrange + ) + + Spacer(modifier = Modifier.width(8.dp)) + + UserZapReaction(model) } @Composable private fun UserReactionModel(model: UserReactionsViewModel) { - val reactions by model.reactions.collectAsState() - UserLikeReaction(reactions[model.today()]) + Icon( + painter = painterResource(R.drawable.ic_liked), + null, + modifier = Size20Modifier, + tint = Color.Unspecified + ) + + Spacer(modifier = StdHorzSpacer) + + UserLikeReaction(model) } @Composable private fun UserBoostModel(model: UserReactionsViewModel) { - val boosts by model.boosts.collectAsState() - UserBoostReaction(boosts[model.today()]) + Icon( + painter = painterResource(R.drawable.ic_retweeted), + null, + modifier = Size24Modifier, + tint = Color.Unspecified + ) + + Spacer(modifier = StdHorzSpacer) + + UserBoostReaction(model) } @Composable private fun UserReplyModel(model: UserReactionsViewModel) { - val replies by model.replies.collectAsState() - UserReplyReaction(replies[model.today()]) + Icon( + painter = painterResource(R.drawable.ic_comment), + null, + modifier = Size20Modifier, + tint = RoyalBlue + ) + + Spacer(modifier = StdHorzSpacer) + + UserReplyReaction(model) } @Stable @@ -154,6 +187,11 @@ class UserReactionsViewModel(val account: Account) : ViewModel() { private var takenIntoAccount = setOf() private val sdf = DateTimeFormatter.ofPattern("yyyy-MM-dd") // SimpleDateFormat() + val todaysReplyCount = _replies.map { showCount(it[today()]) } + val todaysBoostCount = _boosts.map { showCount(it[today()]) } + val todaysReactionCount = _reactions.map { showCount(it[today()]) } + val todaysZapAmount = _zaps.map { showAmountAxis(it[today()]) } + fun formatDate(createAt: Long): String { return sdf.format( Instant.ofEpochSecond(createAt) @@ -339,18 +377,9 @@ class UserReactionsViewModel(val account: Account) : ViewModel() { @Composable fun UserReplyReaction( - replyCount: Int? + model: UserReactionsViewModel ) { - val showCounts = remember(replyCount) { showCount(replyCount) } - - Icon( - painter = painterResource(R.drawable.ic_comment), - null, - modifier = Size20Modifier, - tint = RoyalBlue - ) - - Spacer(modifier = StdHorzSpacer) + val showCounts by model.todaysReplyCount.collectAsState("") Text( showCounts, @@ -361,21 +390,12 @@ fun UserReplyReaction( @Composable fun UserBoostReaction( - boostCount: Int? + model: UserReactionsViewModel ) { - val showCounts = remember(boostCount) { showCount(boostCount) } - - Icon( - painter = painterResource(R.drawable.ic_retweeted), - null, - modifier = Size24Modifier, - tint = Color.Unspecified - ) - - Spacer(modifier = StdHorzSpacer) + val boosts by model.todaysBoostCount.collectAsState("") Text( - showCounts, + boosts, fontWeight = FontWeight.Bold, fontSize = 18.sp ) @@ -383,21 +403,12 @@ fun UserBoostReaction( @Composable fun UserLikeReaction( - likeCount: Int? + model: UserReactionsViewModel ) { - val showCounts = remember(likeCount) { showCount(likeCount) } - - Icon( - painter = painterResource(R.drawable.ic_liked), - null, - modifier = Size20Modifier, - tint = Color.Unspecified - ) - - Spacer(modifier = StdHorzSpacer) + val reactions by model.todaysReactionCount.collectAsState("") Text( - text = showCounts, + text = reactions, fontWeight = FontWeight.Bold, fontSize = 18.sp ) @@ -405,17 +416,9 @@ fun UserLikeReaction( @Composable fun UserZapReaction( - amount: String + model: UserReactionsViewModel ) { - Icon( - imageVector = Icons.Default.Bolt, - contentDescription = stringResource(R.string.zaps), - modifier = Size24Modifier, - tint = BitcoinOrange - ) - - Spacer(modifier = Modifier.width(8.dp)) - + val amount by model.todaysZapAmount.collectAsState("") Text( amount, fontWeight = FontWeight.Bold, From e04eb733e71babd3e1e8d760afa823228b96b8ff Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Tue, 22 Aug 2023 14:04:49 -0400 Subject: [PATCH 19/29] - Protecting contact lists of all accounts in the device. - Pruning events that are not from or cite an account. --- .../java/com/vitorpamplona/amethyst/LocalPreferences.kt | 4 ++++ .../main/java/com/vitorpamplona/amethyst/ServiceManager.kt | 7 +++++-- .../java/com/vitorpamplona/amethyst/model/LocalCache.kt | 4 ++-- app/src/main/java/com/vitorpamplona/amethyst/model/Note.kt | 3 +-- .../src/main/java/com/vitorpamplona/quartz/events/Event.kt | 3 ++- .../java/com/vitorpamplona/quartz/events/EventInterface.kt | 1 + 6 files changed, 15 insertions(+), 7 deletions(-) diff --git a/app/src/main/java/com/vitorpamplona/amethyst/LocalPreferences.kt b/app/src/main/java/com/vitorpamplona/amethyst/LocalPreferences.kt index c6f9017bc..a4bbd919e 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/LocalPreferences.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/LocalPreferences.kt @@ -205,6 +205,10 @@ object LocalPreferences { } } + fun allLocalAccountNPubs(): Set { + return savedAccounts().toSet() + } + fun saveToEncryptedStorage(account: Account) { val prefs = encryptedPreferences(account.userProfile().pubkeyNpub()) prefs.edit().apply { diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ServiceManager.kt b/app/src/main/java/com/vitorpamplona/amethyst/ServiceManager.kt index 0d4de5610..d4f2efd81 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ServiceManager.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ServiceManager.kt @@ -31,6 +31,7 @@ import com.vitorpamplona.amethyst.service.NostrUserProfileDataSource import com.vitorpamplona.amethyst.service.NostrVideoDataSource import com.vitorpamplona.amethyst.service.relays.Client import com.vitorpamplona.amethyst.ui.actions.ImageUploader +import com.vitorpamplona.quartz.encoders.decodePublicKeyAsHexOrNull import java.io.File object ServiceManager { @@ -117,11 +118,13 @@ object ServiceManager { fun cleanUp() { LocalCache.cleanObservers() + val accounts = LocalPreferences.allLocalAccountNPubs().mapNotNull { decodePublicKeyAsHexOrNull(it) }.toSet() + account?.let { LocalCache.pruneOldAndHiddenMessages(it) LocalCache.pruneHiddenMessages(it) - LocalCache.pruneContactLists(it) - LocalCache.pruneRepliesAndReactions(it) + LocalCache.pruneContactLists(accounts) + LocalCache.pruneRepliesAndReactions(accounts) } } } 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 867e7e153..32ec733fb 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/model/LocalCache.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/model/LocalCache.kt @@ -729,7 +729,7 @@ object LocalCache { val author = getOrCreateUser(event.pubKey) val repliesTo = event.boostedPost().mapNotNull { checkGetOrCreateNote(it) } + - event.taggedAddresses().mapNotNull { getOrCreateAddressableNote(it) } + event.taggedAddresses().map { getOrCreateAddressableNote(it) } note.loadEvent(event, author, repliesTo) @@ -751,7 +751,7 @@ object LocalCache { val author = getOrCreateUser(event.pubKey) val repliesTo = event.boostedPost().mapNotNull { checkGetOrCreateNote(it) } + - event.taggedAddresses().mapNotNull { getOrCreateAddressableNote(it) } + event.taggedAddresses().map { getOrCreateAddressableNote(it) } note.loadEvent(event, author, repliesTo) 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 7871305b4..e9237ab6e 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/model/Note.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/model/Note.kt @@ -253,8 +253,7 @@ open class Note(val idHex: String) { zaps = zaps.minus(note) liveSet?.zaps?.invalidateData() } else if (zaps.containsValue(note)) { - val toRemove = zaps.filterValues { it == note } - zaps = zaps.minus(toRemove.keys) + zaps = zaps.filterValues { it != note } liveSet?.zaps?.invalidateData() } } 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 2bf84d6dd..b891b4f93 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/events/Event.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/events/Event.kt @@ -98,6 +98,7 @@ open class Event( override fun matchTag1With(text: String) = tags.any { it.size > 1 && it[1].contains(text, true) } override fun isTaggedUser(idHex: String) = tags.any { it.size > 1 && it[0] == "p" && it[1] == idHex } + override fun isTaggedUsers(idHexes: Set) = tags.any { it.size > 1 && it[0] == "p" && it[1] in idHexes } override fun isTaggedEvent(idHex: String) = tags.any { it.size > 1 && it[0] == "e" && it[1] == idHex } @@ -199,7 +200,7 @@ open class Event( return try { hasCorrectIDHash() && hasVerifedSignature() } catch (e: Exception) { - Log.e("Event", "Fail checking if event $id has a valid signature", e) + Log.e("Event", "Event $id does not have a valid signature: ${toJson()}", e) false } } 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 777b1cffa..2cc03a5a3 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/events/EventInterface.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/events/EventInterface.kt @@ -30,6 +30,7 @@ interface EventInterface { fun hasValidSignature(): Boolean fun isTaggedUser(idHex: String): Boolean + fun isTaggedUsers(idHex: Set): Boolean fun isTaggedEvent(idHex: String): Boolean From a63b38342598b52dfb34f24813563720c2775fd9 Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Tue, 22 Aug 2023 14:06:02 -0400 Subject: [PATCH 20/29] BugFix Force Relay Reconnection when writing a new events. --- .../amethyst/service/relays/Relay.kt | 32 +++++++++++++++---- 1 file changed, 26 insertions(+), 6 deletions(-) diff --git a/app/src/main/java/com/vitorpamplona/amethyst/service/relays/Relay.kt b/app/src/main/java/com/vitorpamplona/amethyst/service/relays/Relay.kt index dc7909179..21ea17847 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/service/relays/Relay.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/service/relays/Relay.kt @@ -284,16 +284,36 @@ class Relay( checkNotInMainThread() if (signedEvent is RelayAuthEvent) { + // specific protocol for this event. val event = """["AUTH",${signedEvent.toJson()}]""" socket?.send(event) eventUploadCounterInBytes += event.bytesUsedInMemory() - } + } else { + if (write) { + if (isConnected()) { + if (isReady) { + val event = """["EVENT",${signedEvent.toJson()}]""" + socket?.send(event) + eventUploadCounterInBytes += event.bytesUsedInMemory() + } + } else { + // waits 60 seconds to reconnect after disconnected. + if (TimeUtils.now() > closingTimeInSeconds + RECONNECTING_IN_SECONDS) { + // sends all filters after connection is successful. + connectAndRun { + checkNotInMainThread() - if (write) { - if (signedEvent !is RelayAuthEvent) { - val event = """["EVENT",${signedEvent.toJson()}]""" - socket?.send(event) - eventUploadCounterInBytes += event.bytesUsedInMemory() + val event = """["EVENT",${signedEvent.toJson()}]""" + socket?.send(event) + eventUploadCounterInBytes += event.bytesUsedInMemory() + + // Sends everything. + Client.allSubscriptions().forEach { + sendFilter(requestId = it) + } + } + } + } } } } From 2d30f3e9d2590e9ae1f381fa29e7dad910ad5096 Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Tue, 22 Aug 2023 16:01:59 -0400 Subject: [PATCH 21/29] Finishing the prune clean up to make sure we don't delete post from other accounts logged in. --- .../amethyst/model/LocalCache.kt | 22 ++++++++++--------- 1 file changed, 12 insertions(+), 10 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 32ec733fb..ed7aae725 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/model/LocalCache.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/model/LocalCache.kt @@ -1365,8 +1365,8 @@ object LocalCache { // Doesn't need to clean up the replies and mentions.. Too small to matter. // Counts the replies - it.replyTo?.forEach { _ -> - it.removeReply(it) + it.replyTo?.forEach { parent -> + parent.removeReply(it) } childrenToBeRemoved.addAll(it.removeAllChildNotes()) @@ -1389,8 +1389,8 @@ object LocalCache { notes.remove(it.idHex) // Counts the replies - it.replyTo?.forEach { _ -> - it.removeReply(it) + it.replyTo?.forEach { parent -> + parent.removeReply(it) } childrenToBeRemoved.addAll(it.removeAllChildNotes()) @@ -1405,9 +1405,8 @@ object LocalCache { } } - fun pruneRepliesAndReactions(account: Account) { + fun pruneRepliesAndReactions(accounts: Set) { checkNotInMainThread() - val user = account.userProfile() val toBeRemoved = notes.filter { ( @@ -1416,8 +1415,8 @@ object LocalCache { it.value.event is ReportEvent || it.value.event is GenericRepostEvent ) && it.value.liveSet?.isInUse() != true && // don't delete if observing. - it.value.author != user && // don't delete if it is the logged in account - it.value.event?.isTaggedUser(user.pubkeyHex) != true // don't delete if it's a notification to the logged in user + it.value.author?.pubkeyHex !in accounts && // don't delete if it is the logged in account + it.value.event?.isTaggedUsers(accounts) != true // don't delete if it's a notification to the logged in user }.values val childrenToBeRemoved = mutableListOf() @@ -1491,12 +1490,15 @@ object LocalCache { } } - fun pruneContactLists(userAccount: Account) { + fun pruneContactLists(loggedIn: Set) { checkNotInMainThread() var removingContactList = 0 users.values.forEach { - if (it != userAccount.userProfile() && (it.liveSet == null || it.liveSet?.isInUse() == false) && it.latestContactList != null) { + if (it.pubkeyHex !in loggedIn && + (it.liveSet == null || it.liveSet?.isInUse() == false) && + it.latestContactList != null + ) { it.latestContactList = null removingContactList++ } From f027d23fcf6b505a6abf5d61e35f726dc70d62bc Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Tue, 22 Aug 2023 16:10:32 -0400 Subject: [PATCH 22/29] Small refactoring to create the PrivateZap Event --- .../quartz/events/EventFactory.kt | 4 +- .../quartz/events/LnZapPrivateEvent.kt | 43 +++++++++++++++++++ .../quartz/events/LnZapRequestEvent.kt | 29 +++++++------ 3 files changed, 60 insertions(+), 16 deletions(-) create mode 100644 quartz/src/main/java/com/vitorpamplona/quartz/events/LnZapPrivateEvent.kt 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 745d03ecd..70f66321a 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/events/EventFactory.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/events/EventFactory.kt @@ -41,9 +41,6 @@ class EventFactory { ContactListEvent.kind -> ContactListEvent(id, pubKey, createdAt, tags, content, sig) DeletionEvent.kind -> DeletionEvent(id, pubKey, createdAt, tags, content, sig) - // Will never happen. - // DirectMessageEvent.kind -> DirectMessageEvent(createdAt, tags, content) - EmojiPackEvent.kind -> EmojiPackEvent(id, pubKey, createdAt, tags, content, sig) EmojiPackSelectionEvent.kind -> EmojiPackSelectionEvent(id, pubKey, createdAt, tags, content, sig) SealedGossipEvent.kind -> SealedGossipEvent(id, pubKey, createdAt, tags, content, sig) @@ -59,6 +56,7 @@ class EventFactory { LnZapEvent.kind -> LnZapEvent(id, pubKey, createdAt, tags, content, sig) LnZapPaymentRequestEvent.kind -> LnZapPaymentRequestEvent(id, pubKey, createdAt, tags, content, sig) LnZapPaymentResponseEvent.kind -> LnZapPaymentResponseEvent(id, pubKey, createdAt, tags, content, sig) + LnZapPrivateEvent.kind -> LnZapPrivateEvent(id, pubKey, createdAt, tags, content, sig) LnZapRequestEvent.kind -> LnZapRequestEvent(id, pubKey, createdAt, tags, content, sig) LongTextNoteEvent.kind -> LongTextNoteEvent(id, pubKey, createdAt, tags, content, sig) MetadataEvent.kind -> MetadataEvent(id, pubKey, createdAt, tags, content, sig) diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/events/LnZapPrivateEvent.kt b/quartz/src/main/java/com/vitorpamplona/quartz/events/LnZapPrivateEvent.kt new file mode 100644 index 000000000..b3207d3ac --- /dev/null +++ b/quartz/src/main/java/com/vitorpamplona/quartz/events/LnZapPrivateEvent.kt @@ -0,0 +1,43 @@ +package com.vitorpamplona.quartz.events + +import android.util.Log +import androidx.compose.runtime.Immutable +import com.vitorpamplona.quartz.crypto.CryptoUtils +import com.vitorpamplona.quartz.encoders.Bech32 +import com.vitorpamplona.quartz.encoders.HexKey +import com.vitorpamplona.quartz.encoders.LnInvoiceUtil +import com.vitorpamplona.quartz.encoders.toHexKey +import com.vitorpamplona.quartz.utils.TimeUtils +import java.nio.charset.Charset +import java.security.SecureRandom +import javax.crypto.BadPaddingException +import javax.crypto.Cipher +import javax.crypto.spec.IvParameterSpec +import javax.crypto.spec.SecretKeySpec + +@Immutable +class LnZapPrivateEvent( + id: HexKey, + pubKey: HexKey, + createdAt: Long, + tags: List>, + content: String, + sig: HexKey +) : Event(id, pubKey, createdAt, kind, tags, content, sig) { + + companion object { + const val kind = 9733 + + fun create( + privateKey: ByteArray, + tags: List> = emptyList(), + content: String = "", + createdAt: Long = TimeUtils.now() + ): Event { + val pubKey = CryptoUtils.pubkeyCreate(privateKey).toHexKey() + val id = generateId(pubKey, createdAt, kind, tags, content) + val sig = CryptoUtils.sign(id, privateKey).toHexKey() + return Event(id.toHexKey(), pubKey, createdAt, kind, tags, content, sig) + } + } +} diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/events/LnZapRequestEvent.kt b/quartz/src/main/java/com/vitorpamplona/quartz/events/LnZapRequestEvent.kt index c318ca669..50cbe743d 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/events/LnZapRequestEvent.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/events/LnZapRequestEvent.kt @@ -86,9 +86,9 @@ class LnZapRequestEvent( privkey = CryptoUtils.privkeyCreate() pubKey = CryptoUtils.pubkeyCreate(privkey).toHexKey() } else if (zapType == LnZapEvent.ZapType.PRIVATE) { - var encryptionPrivateKey = createEncryptionPrivateKey(privateKey.toHexKey(), originalNote.id(), createdAt) - var noteJson = (create(privkey, 9733, listOf(tags[0], tags[1]), message)).toJson() - var encryptedContent = encryptPrivateZapMessage(noteJson, encryptionPrivateKey, originalNote.pubKey().hexToByteArray()) + val encryptionPrivateKey = createEncryptionPrivateKey(privateKey.toHexKey(), originalNote.id(), createdAt) + val noteJson = (LnZapPrivateEvent.create(privkey, listOf(tags[0], tags[1]), message)).toJson() + val encryptedContent = encryptPrivateZapMessage(noteJson, encryptionPrivateKey, originalNote.pubKey().hexToByteArray()) tags = tags + listOf(listOf("anon", encryptedContent)) content = "" // make sure public content is empty, as the content is encrypted privkey = encryptionPrivateKey // sign event with generated privkey @@ -119,9 +119,9 @@ class LnZapRequestEvent( pubKey = CryptoUtils.pubkeyCreate(privkey).toHexKey() tags = tags + listOf(listOf("anon", "")) } else if (zapType == LnZapEvent.ZapType.PRIVATE) { - var encryptionPrivateKey = createEncryptionPrivateKey(privateKey.toHexKey(), userHex, createdAt) - var noteJson = (create(privkey, 9733, listOf(tags[0], tags[1]), message)).toJson() - var encryptedContent = encryptPrivateZapMessage(noteJson, encryptionPrivateKey, userHex.hexToByteArray()) + val encryptionPrivateKey = createEncryptionPrivateKey(privateKey.toHexKey(), userHex, createdAt) + val noteJson = LnZapPrivateEvent.create(privkey, listOf(tags[0], tags[1]), message).toJson() + val encryptedContent = encryptPrivateZapMessage(noteJson, encryptionPrivateKey, userHex.hexToByteArray()) tags = tags + listOf(listOf("anon", encryptedContent)) content = "" privkey = encryptionPrivateKey @@ -132,21 +132,22 @@ class LnZapRequestEvent( return LnZapRequestEvent(id.toHexKey(), pubKey, createdAt, tags, content, sig.toHexKey()) } + fun createEncryptionPrivateKey(privkey: String, id: String, createdAt: Long): ByteArray { - var str = privkey + id + createdAt.toString() - var strbyte = str.toByteArray(Charset.forName("utf-8")) + val str = privkey + id + createdAt.toString() + val strbyte = str.toByteArray(Charset.forName("utf-8")) return CryptoUtils.sha256(strbyte) } private fun encryptPrivateZapMessage(msg: String, privkey: ByteArray, pubkey: ByteArray): String { - var sharedSecret = CryptoUtils.getSharedSecretNIP04(privkey, pubkey) + val sharedSecret = CryptoUtils.getSharedSecretNIP04(privkey, pubkey) val iv = ByteArray(16) SecureRandom().nextBytes(iv) val keySpec = SecretKeySpec(sharedSecret, "AES") val ivSpec = IvParameterSpec(iv) - var utf8message = msg.toByteArray(Charset.forName("utf-8")) + val utf8message = msg.toByteArray(Charset.forName("utf-8")) val cipher = Cipher.getInstance("AES/CBC/PKCS5Padding") cipher.init(Cipher.ENCRYPT_MODE, keySpec, ivSpec) val encryptedMsg = cipher.doFinal(utf8message) @@ -158,7 +159,7 @@ class LnZapRequestEvent( } private fun decryptPrivateZapMessage(msg: String, privkey: ByteArray, pubkey: ByteArray): String { - var sharedSecret = CryptoUtils.getSharedSecretNIP04(privkey, pubkey) + val sharedSecret = CryptoUtils.getSharedSecretNIP04(privkey, pubkey) if (sharedSecret.size != 16 && sharedSecret.size != 32) { throw IllegalArgumentException("Invalid shared secret size") } @@ -170,8 +171,10 @@ class LnZapRequestEvent( val encryptedMsg = parts.first().run { Bech32.decode(this).second } val encryptedBytes = Bech32.five2eight(encryptedMsg, 0) val cipher = Cipher.getInstance("AES/CBC/PKCS5Padding") - cipher.init(Cipher.DECRYPT_MODE, SecretKeySpec(sharedSecret, "AES"), IvParameterSpec( - Bech32.five2eight(iv, 0))) + cipher.init( + Cipher.DECRYPT_MODE, SecretKeySpec(sharedSecret, "AES"), IvParameterSpec( + Bech32.five2eight(iv, 0)) + ) try { val decryptedMsgBytes = cipher.doFinal(encryptedBytes) From 52761b3901f78f1e23e595a7233823a051d64265 Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Tue, 22 Aug 2023 16:19:37 -0400 Subject: [PATCH 23/29] Activates pull to refresh on the Stories feed. --- .../amethyst/ui/screen/loggedIn/VideoScreen.kt | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) 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 81643f5bc..4d0787bf0 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 @@ -64,6 +64,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.ScrollStateKeys import com.vitorpamplona.amethyst.ui.screen.rememberForeverPagerState import com.vitorpamplona.amethyst.ui.theme.Size35dp @@ -206,12 +207,14 @@ private fun LoadedState( WatchScrollToTop(videoFeedView, pagerState) - SlidingCarousel( - state.feed, - pagerState, - accountViewModel, - nav - ) + RefresheableView(viewModel = videoFeedView) { + SlidingCarousel( + state.feed, + pagerState, + accountViewModel, + nav + ) + } } @OptIn(ExperimentalFoundationApi::class) From af143f2ea1ca8b30fe747aae8b393881608022a4 Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Tue, 22 Aug 2023 16:19:59 -0400 Subject: [PATCH 24/29] Base support for Calendar events --- .../quartz/events/CalendarDateSlotEvent.kt | 44 +++++++++++++++++ .../quartz/events/CalendarEvent.kt | 36 ++++++++++++++ .../quartz/events/CalendarRSVPEvent.kt | 46 +++++++++++++++++ .../quartz/events/CalendarTimeSlotEvent.kt | 49 +++++++++++++++++++ .../quartz/events/EventFactory.kt | 4 ++ 5 files changed, 179 insertions(+) create mode 100644 quartz/src/main/java/com/vitorpamplona/quartz/events/CalendarDateSlotEvent.kt create mode 100644 quartz/src/main/java/com/vitorpamplona/quartz/events/CalendarEvent.kt create mode 100644 quartz/src/main/java/com/vitorpamplona/quartz/events/CalendarRSVPEvent.kt create mode 100644 quartz/src/main/java/com/vitorpamplona/quartz/events/CalendarTimeSlotEvent.kt diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/events/CalendarDateSlotEvent.kt b/quartz/src/main/java/com/vitorpamplona/quartz/events/CalendarDateSlotEvent.kt new file mode 100644 index 000000000..4b3e61b69 --- /dev/null +++ b/quartz/src/main/java/com/vitorpamplona/quartz/events/CalendarDateSlotEvent.kt @@ -0,0 +1,44 @@ +package com.vitorpamplona.quartz.events + +import androidx.compose.runtime.Immutable +import com.vitorpamplona.quartz.utils.TimeUtils +import com.vitorpamplona.quartz.encoders.toHexKey +import com.vitorpamplona.quartz.crypto.CryptoUtils +import com.vitorpamplona.quartz.encoders.ATag +import com.vitorpamplona.quartz.encoders.HexKey + +@Immutable +class CalendarDateSlotEvent( + 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 location() = tags.firstOrNull { it.size > 1 && it[0] == "location" }?.get(1) + fun start() = tags.firstOrNull { it.size > 1 && it[0] == "start" }?.get(1) + fun end() = tags.firstOrNull { it.size > 1 && it[0] == "end" }?.get(1) + + // ["start", ""], + // ["end", ""], + + companion object { + const val kind = 31922 + + fun create( + privateKey: ByteArray, + createdAt: Long = TimeUtils.now() + ): CalendarDateSlotEvent { + val tags = mutableListOf>() + val pubKey = CryptoUtils.pubkeyCreate(privateKey).toHexKey() + val id = generateId(pubKey, createdAt, kind, tags, "") + val sig = CryptoUtils.sign(id, privateKey) + return CalendarDateSlotEvent(id.toHexKey(), pubKey, createdAt, tags, "", sig.toHexKey()) + } + } +} diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/events/CalendarEvent.kt b/quartz/src/main/java/com/vitorpamplona/quartz/events/CalendarEvent.kt new file mode 100644 index 000000000..a21141367 --- /dev/null +++ b/quartz/src/main/java/com/vitorpamplona/quartz/events/CalendarEvent.kt @@ -0,0 +1,36 @@ +package com.vitorpamplona.quartz.events + +import androidx.compose.runtime.Immutable +import com.vitorpamplona.quartz.utils.TimeUtils +import com.vitorpamplona.quartz.encoders.toHexKey +import com.vitorpamplona.quartz.crypto.CryptoUtils +import com.vitorpamplona.quartz.encoders.ATag +import com.vitorpamplona.quartz.encoders.HexKey + +@Immutable +class CalendarEvent( + 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) + + companion object { + const val kind = 31924 + + fun create( + privateKey: ByteArray, + createdAt: Long = TimeUtils.now() + ): CalendarEvent { + val tags = mutableListOf>() + val pubKey = CryptoUtils.pubkeyCreate(privateKey).toHexKey() + val id = generateId(pubKey, createdAt, kind, tags, "") + val sig = CryptoUtils.sign(id, privateKey) + return CalendarEvent(id.toHexKey(), pubKey, createdAt, tags, "", sig.toHexKey()) + } + } +} diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/events/CalendarRSVPEvent.kt b/quartz/src/main/java/com/vitorpamplona/quartz/events/CalendarRSVPEvent.kt new file mode 100644 index 000000000..4a2b48166 --- /dev/null +++ b/quartz/src/main/java/com/vitorpamplona/quartz/events/CalendarRSVPEvent.kt @@ -0,0 +1,46 @@ +package com.vitorpamplona.quartz.events + +import androidx.compose.runtime.Immutable +import com.vitorpamplona.quartz.utils.TimeUtils +import com.vitorpamplona.quartz.encoders.toHexKey +import com.vitorpamplona.quartz.crypto.CryptoUtils +import com.vitorpamplona.quartz.encoders.ATag +import com.vitorpamplona.quartz.encoders.HexKey + +@Immutable +class CalendarRSVPEvent( + 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 status() = tags.firstOrNull { it.size > 1 && it[0] == "location" }?.get(1) + fun start() = tags.firstOrNull { it.size > 1 && it[0] == "start" }?.get(1) + fun end() = tags.firstOrNull { it.size > 1 && it[0] == "end" }?.get(1) + + // ["L", "status"], + // ["l", "", "status"], + // ["L", "freebusy"], + // ["l", "", "freebusy"] + + companion object { + const val kind = 31925 + + fun create( + privateKey: ByteArray, + createdAt: Long = TimeUtils.now() + ): CalendarRSVPEvent { + val tags = mutableListOf>() + val pubKey = CryptoUtils.pubkeyCreate(privateKey).toHexKey() + val id = generateId(pubKey, createdAt, kind, tags, "") + val sig = CryptoUtils.sign(id, privateKey) + return CalendarRSVPEvent(id.toHexKey(), pubKey, createdAt, tags, "", sig.toHexKey()) + } + } +} diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/events/CalendarTimeSlotEvent.kt b/quartz/src/main/java/com/vitorpamplona/quartz/events/CalendarTimeSlotEvent.kt new file mode 100644 index 000000000..7a18d1f78 --- /dev/null +++ b/quartz/src/main/java/com/vitorpamplona/quartz/events/CalendarTimeSlotEvent.kt @@ -0,0 +1,49 @@ +package com.vitorpamplona.quartz.events + +import androidx.compose.runtime.Immutable +import com.vitorpamplona.quartz.utils.TimeUtils +import com.vitorpamplona.quartz.encoders.toHexKey +import com.vitorpamplona.quartz.crypto.CryptoUtils +import com.vitorpamplona.quartz.encoders.ATag +import com.vitorpamplona.quartz.encoders.HexKey + +@Immutable +class CalendarTimeSlotEvent( + 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 location() = tags.firstOrNull { it.size > 1 && it[0] == "location" }?.get(1) + fun start() = tags.firstOrNull { it.size > 1 && it[0] == "start" }?.get(1)?.toLongOrNull() + fun end() = tags.firstOrNull { it.size > 1 && it[0] == "end" }?.get(1)?.toLongOrNull() + + fun startTmz() = tags.firstOrNull { it.size > 1 && it[0] == "start_tzid" }?.get(1)?.toLongOrNull() + fun endTmz() = tags.firstOrNull { it.size > 1 && it[0] == "end_tzid" }?.get(1)?.toLongOrNull() + + // ["start", ""], + // ["end", ""], + // ["start_tzid", ""], + // ["end_tzid", ""], + + companion object { + const val kind = 31923 + + fun create( + privateKey: ByteArray, + createdAt: Long = TimeUtils.now() + ): CalendarTimeSlotEvent { + val tags = mutableListOf>() + val pubKey = CryptoUtils.pubkeyCreate(privateKey).toHexKey() + val id = generateId(pubKey, createdAt, kind, tags, "") + val sig = CryptoUtils.sign(id, privateKey) + return CalendarTimeSlotEvent(id.toHexKey(), pubKey, createdAt, tags, "", sig.toHexKey()) + } + } +} 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 70f66321a..0cdab7daa 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/events/EventFactory.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/events/EventFactory.kt @@ -22,6 +22,10 @@ class EventFactory { BadgeDefinitionEvent.kind -> BadgeDefinitionEvent(id, pubKey, createdAt, tags, content, sig) BadgeProfilesEvent.kind -> BadgeProfilesEvent(id, pubKey, createdAt, tags, content, sig) BookmarkListEvent.kind -> BookmarkListEvent(id, pubKey, createdAt, tags, content, sig) + CalendarEvent.kind -> CalendarEvent(id, pubKey, createdAt, tags, content, sig) + CalendarDateSlotEvent.kind -> CalendarDateSlotEvent(id, pubKey, createdAt, tags, content, sig) + CalendarTimeSlotEvent.kind -> CalendarTimeSlotEvent(id, pubKey, createdAt, tags, content, sig) + CalendarRSVPEvent.kind -> CalendarRSVPEvent(id, pubKey, createdAt, tags, content, sig) ChannelCreateEvent.kind -> ChannelCreateEvent(id, pubKey, createdAt, tags, content, sig) ChannelHideMessageEvent.kind -> ChannelHideMessageEvent(id, pubKey, createdAt, tags, content, sig) ChannelMessageEvent.kind -> ChannelMessageEvent(id, pubKey, createdAt, tags, content, sig) From dfa9c77e3e295be408ee2de3be4b1b02f1734b24 Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Tue, 22 Aug 2023 16:39:23 -0400 Subject: [PATCH 25/29] Migrates Addressable Events to a Base class --- .../quartz/events/AdvertisedRelayListEvent.kt | 3 +-- .../quartz/events/AppDefinitionEvent.kt | 5 +---- .../quartz/events/AppRecommendationEvent.kt | 7 +------ .../vitorpamplona/quartz/events/AudioTrackEvent.kt | 5 +---- .../quartz/events/BadgeDefinitionEvent.kt | 5 +---- .../quartz/events/BadgeProfilesEvent.kt | 5 +---- .../quartz/events/CalendarDateSlotEvent.kt | 5 +---- .../vitorpamplona/quartz/events/CalendarEvent.kt | 5 +---- .../quartz/events/CalendarRSVPEvent.kt | 5 +---- .../quartz/events/CalendarTimeSlotEvent.kt | 6 +----- .../quartz/events/ChannelMessageEvent.kt | 2 +- .../quartz/events/ClassifiedsEvent.kt | 6 +----- .../quartz/events/CommunityDefinitionEvent.kt | 6 +----- .../quartz/events/EmojiPackSelectionEvent.kt | 6 +----- .../java/com/vitorpamplona/quartz/events/Event.kt | 14 ++++++++++++++ .../quartz/events/GeneralListEvent.kt | 5 +---- .../quartz/events/LiveActivitiesEvent.kt | 6 +----- .../vitorpamplona/quartz/events/MuteListEvent.kt | 5 +---- .../com/vitorpamplona/quartz/events/NNSEvent.kt | 6 +----- .../vitorpamplona/quartz/events/PinListEvent.kt | 5 +---- .../vitorpamplona/quartz/events/RelaySetEvent.kt | 6 +----- 21 files changed, 34 insertions(+), 84 deletions(-) diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/events/AdvertisedRelayListEvent.kt b/quartz/src/main/java/com/vitorpamplona/quartz/events/AdvertisedRelayListEvent.kt index d9c76162d..2f256ee16 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/events/AdvertisedRelayListEvent.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/events/AdvertisedRelayListEvent.kt @@ -15,9 +15,8 @@ class AdvertisedRelayListEvent( tags: List>, content: String, sig: HexKey -) : Event(id, pubKey, createdAt, kind, tags, content, sig), AddressableEvent { +) : BaseAddressableEvent(id, pubKey, createdAt, kind, tags, content, sig) { override fun dTag() = fixedDTag - override fun address() = ATag(kind, pubKey, dTag(), null) fun relays(): List { return tags.mapNotNull { diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/events/AppDefinitionEvent.kt b/quartz/src/main/java/com/vitorpamplona/quartz/events/AppDefinitionEvent.kt index 9c27d3bfc..c014bdc4f 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/events/AppDefinitionEvent.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/events/AppDefinitionEvent.kt @@ -14,10 +14,7 @@ class AppDefinitionEvent( 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) - +) : BaseAddressableEvent(id, pubKey, createdAt, kind, tags, content, sig) { fun appMetaData() = try { mapper.readValue( ByteArrayInputStream(content.toByteArray(Charsets.UTF_8)), diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/events/AppRecommendationEvent.kt b/quartz/src/main/java/com/vitorpamplona/quartz/events/AppRecommendationEvent.kt index eb6c895d6..d02a72640 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/events/AppRecommendationEvent.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/events/AppRecommendationEvent.kt @@ -12,16 +12,11 @@ class AppRecommendationEvent( tags: List>, content: String, sig: HexKey -) : Event(id, pubKey, createdAt, kind, tags, content, sig), AddressableEvent { +) : BaseAddressableEvent(id, pubKey, createdAt, kind, tags, content, sig) { fun recommendations() = tags.filter { it.size > 1 && it[0] == "a" }.mapNotNull { ATag.parse(it[1], it.getOrNull(2)) } - fun forKind() = runCatching { dTag().toInt() }.getOrNull() - - override fun dTag() = tags.firstOrNull { it.size > 1 && it[0] == "d" }?.get(1) ?: "" - override fun address() = ATag(kind, pubKey, dTag(), null) - companion object { const val kind = 31989 } diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/events/AudioTrackEvent.kt b/quartz/src/main/java/com/vitorpamplona/quartz/events/AudioTrackEvent.kt index 19346daf8..57b1d38d9 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/events/AudioTrackEvent.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/events/AudioTrackEvent.kt @@ -15,10 +15,7 @@ class AudioTrackEvent( 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) +) : BaseAddressableEvent(id, pubKey, createdAt, kind, tags, content, sig) { 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) diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/events/BadgeDefinitionEvent.kt b/quartz/src/main/java/com/vitorpamplona/quartz/events/BadgeDefinitionEvent.kt index 2ed6823d1..410a6a20c 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/events/BadgeDefinitionEvent.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/events/BadgeDefinitionEvent.kt @@ -12,10 +12,7 @@ class BadgeDefinitionEvent( 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) - +) : BaseAddressableEvent(id, pubKey, createdAt, kind, tags, content, sig) { fun name() = tags.firstOrNull { it.size > 1 && it[0] == "name" }?.get(1) fun thumb() = tags.firstOrNull { it.size > 1 && it[0] == "thumb" }?.get(1) fun image() = tags.firstOrNull { it.size > 1 && it[0] == "image" }?.get(1) diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/events/BadgeProfilesEvent.kt b/quartz/src/main/java/com/vitorpamplona/quartz/events/BadgeProfilesEvent.kt index 30b704652..a4801ecae 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/events/BadgeProfilesEvent.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/events/BadgeProfilesEvent.kt @@ -12,7 +12,7 @@ class BadgeProfilesEvent( tags: List>, content: String, sig: HexKey -) : Event(id, pubKey, createdAt, kind, tags, content, sig), AddressableEvent { +) : BaseAddressableEvent(id, pubKey, createdAt, kind, tags, content, sig) { fun badgeAwardEvents() = tags.filter { it.firstOrNull() == "e" }.mapNotNull { it.getOrNull(1) } fun badgeAwardDefinitions() = tags.filter { it.firstOrNull() == "a" }.mapNotNull { val aTagValue = it.getOrNull(1) @@ -21,9 +21,6 @@ class BadgeProfilesEvent( if (aTagValue != null) ATag.parse(aTagValue, relay) else 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 const val standardDTAg = "profile_badges" diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/events/CalendarDateSlotEvent.kt b/quartz/src/main/java/com/vitorpamplona/quartz/events/CalendarDateSlotEvent.kt index 4b3e61b69..a9b234436 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/events/CalendarDateSlotEvent.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/events/CalendarDateSlotEvent.kt @@ -15,10 +15,7 @@ class CalendarDateSlotEvent( 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) +) : BaseAddressableEvent(id, pubKey, createdAt, kind, tags, content, sig) { fun location() = tags.firstOrNull { it.size > 1 && it[0] == "location" }?.get(1) fun start() = tags.firstOrNull { it.size > 1 && it[0] == "start" }?.get(1) diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/events/CalendarEvent.kt b/quartz/src/main/java/com/vitorpamplona/quartz/events/CalendarEvent.kt index a21141367..9005e5eb5 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/events/CalendarEvent.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/events/CalendarEvent.kt @@ -15,10 +15,7 @@ class CalendarEvent( 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) - +) : BaseAddressableEvent(id, pubKey, createdAt, kind, tags, content, sig) { companion object { const val kind = 31924 diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/events/CalendarRSVPEvent.kt b/quartz/src/main/java/com/vitorpamplona/quartz/events/CalendarRSVPEvent.kt index 4a2b48166..282104501 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/events/CalendarRSVPEvent.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/events/CalendarRSVPEvent.kt @@ -15,10 +15,7 @@ class CalendarRSVPEvent( 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) +) : BaseAddressableEvent(id, pubKey, createdAt, kind, tags, content, sig) { fun status() = tags.firstOrNull { it.size > 1 && it[0] == "location" }?.get(1) fun start() = tags.firstOrNull { it.size > 1 && it[0] == "start" }?.get(1) diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/events/CalendarTimeSlotEvent.kt b/quartz/src/main/java/com/vitorpamplona/quartz/events/CalendarTimeSlotEvent.kt index 7a18d1f78..d0bc33693 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/events/CalendarTimeSlotEvent.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/events/CalendarTimeSlotEvent.kt @@ -15,11 +15,7 @@ class CalendarTimeSlotEvent( 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) - +) : BaseAddressableEvent(id, pubKey, createdAt, kind, tags, content, sig) { fun location() = tags.firstOrNull { it.size > 1 && it[0] == "location" }?.get(1) fun start() = tags.firstOrNull { it.size > 1 && it[0] == "start" }?.get(1)?.toLongOrNull() fun end() = tags.firstOrNull { it.size > 1 && it[0] == "end" }?.get(1)?.toLongOrNull() diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/events/ChannelMessageEvent.kt b/quartz/src/main/java/com/vitorpamplona/quartz/events/ChannelMessageEvent.kt index 5835283a3..adbd79f0e 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/events/ChannelMessageEvent.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/events/ChannelMessageEvent.kt @@ -71,5 +71,5 @@ class ChannelMessageEvent( } interface IsInPublicChatChannel { - open fun channel(): String? + fun channel(): String? } diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/events/ClassifiedsEvent.kt b/quartz/src/main/java/com/vitorpamplona/quartz/events/ClassifiedsEvent.kt index f215430cd..e22b0e0cc 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/events/ClassifiedsEvent.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/events/ClassifiedsEvent.kt @@ -15,11 +15,7 @@ class ClassifiedsEvent( 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) - +) : BaseAddressableEvent(id, pubKey, createdAt, kind, tags, content, sig) { fun title() = tags.firstOrNull { it.size > 1 && it[0] == "title" }?.get(1) fun image() = tags.firstOrNull { it.size > 1 && it[0] == "image" }?.get(1) fun summary() = tags.firstOrNull { it.size > 1 && it[0] == "summary" }?.get(1) diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/events/CommunityDefinitionEvent.kt b/quartz/src/main/java/com/vitorpamplona/quartz/events/CommunityDefinitionEvent.kt index 8e8bb4e1c..447261d01 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/events/CommunityDefinitionEvent.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/events/CommunityDefinitionEvent.kt @@ -15,11 +15,7 @@ class CommunityDefinitionEvent( 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) - +) : BaseAddressableEvent(id, pubKey, createdAt, kind, tags, content, sig) { fun description() = tags.firstOrNull { it.size > 1 && it[0] == "description" }?.get(1) fun image() = tags.firstOrNull { it.size > 1 && it[0] == "image" }?.get(1) fun rules() = tags.firstOrNull { it.size > 1 && it[0] == "rules" }?.get(1) diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/events/EmojiPackSelectionEvent.kt b/quartz/src/main/java/com/vitorpamplona/quartz/events/EmojiPackSelectionEvent.kt index 78ff9e030..46b403f15 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/events/EmojiPackSelectionEvent.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/events/EmojiPackSelectionEvent.kt @@ -15,11 +15,7 @@ class EmojiPackSelectionEvent( tags: List>, content: String, sig: HexKey -) : Event(id, pubKey, createdAt, kind, tags, content, sig), AddressableEvent { - - override fun dTag() = "" - override fun address() = ATag(kind, pubKey, dTag(), null) - +) : BaseAddressableEvent(id, pubKey, createdAt, kind, tags, content, sig) { companion object { const val kind = 10030 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 b891b4f93..123bd2f70 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/events/Event.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/events/Event.kt @@ -385,6 +385,20 @@ interface AddressableEvent { fun address(): ATag } +@Immutable +open class BaseAddressableEvent( + id: HexKey, + pubKey: HexKey, + createdAt: Long, + kind: Int, + 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 String.bytesUsedInMemory(): Int { return (8 * ((((this.length) * 2) + 45) / 8)) } diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/events/GeneralListEvent.kt b/quartz/src/main/java/com/vitorpamplona/quartz/events/GeneralListEvent.kt index ba0f39fd8..6a23374b9 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/events/GeneralListEvent.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/events/GeneralListEvent.kt @@ -17,10 +17,7 @@ abstract class GeneralListEvent( tags: List>, content: String, sig: HexKey -) : 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) - +) : BaseAddressableEvent(id, pubKey, createdAt, kind, tags, content, sig) { fun category() = dTag() fun bookmarkedPosts() = taggedEvents() fun bookmarkedPeople() = taggedUsers() diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/events/LiveActivitiesEvent.kt b/quartz/src/main/java/com/vitorpamplona/quartz/events/LiveActivitiesEvent.kt index 83f89d044..9dde93029 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/events/LiveActivitiesEvent.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/events/LiveActivitiesEvent.kt @@ -15,11 +15,7 @@ class LiveActivitiesEvent( 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) - +) : BaseAddressableEvent(id, pubKey, createdAt, kind, tags, content, sig) { fun title() = tags.firstOrNull { it.size > 1 && it[0] == "title" }?.get(1) fun summary() = tags.firstOrNull { it.size > 1 && it[0] == "summary" }?.get(1) fun image() = tags.firstOrNull { it.size > 1 && it[0] == "image" }?.get(1) diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/events/MuteListEvent.kt b/quartz/src/main/java/com/vitorpamplona/quartz/events/MuteListEvent.kt index cc8eea424..69cca0280 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/events/MuteListEvent.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/events/MuteListEvent.kt @@ -18,10 +18,7 @@ class MuteListEvent( tags: List>, content: String, sig: HexKey -) : 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) - +) : BaseAddressableEvent(id, pubKey, createdAt, kind, tags, content, sig) { fun plainContent(privKey: ByteArray): String? { return try { val sharedSecret = CryptoUtils.getSharedSecretNIP04(privKey, pubKey.hexToByteArray()) diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/events/NNSEvent.kt b/quartz/src/main/java/com/vitorpamplona/quartz/events/NNSEvent.kt index 25a22e706..5940bb458 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/events/NNSEvent.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/events/NNSEvent.kt @@ -15,11 +15,7 @@ class NNSEvent( 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) - +) : BaseAddressableEvent(id, pubKey, createdAt, kind, tags, content, sig) { fun ip4() = tags.firstOrNull { it.size > 1 && it[0] == "ip4" }?.get(1) fun ip6() = tags.firstOrNull { it.size > 1 && it[0] == "ip6" }?.get(1) fun version() = tags.firstOrNull { it.size > 1 && it[0] == "version" }?.get(1) diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/events/PinListEvent.kt b/quartz/src/main/java/com/vitorpamplona/quartz/events/PinListEvent.kt index fb33e998d..b1a719655 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/events/PinListEvent.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/events/PinListEvent.kt @@ -15,10 +15,7 @@ class PinListEvent( 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) +) : BaseAddressableEvent(id, pubKey, createdAt, kind, tags, content, sig) { fun pins() = tags.filter { it.size > 1 && it[0] == "pin" }.map { it[1] } diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/events/RelaySetEvent.kt b/quartz/src/main/java/com/vitorpamplona/quartz/events/RelaySetEvent.kt index 555355b0e..cebff1e06 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/events/RelaySetEvent.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/events/RelaySetEvent.kt @@ -15,11 +15,7 @@ class RelaySetEvent( 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) - +) : BaseAddressableEvent(id, pubKey, createdAt, kind, tags, content, sig) { fun relays() = tags.filter { it.size > 1 && it[0] == "r" }.map { it[1] } fun description() = tags.firstOrNull() { it.size > 1 && it[0] == "description" }?.get(1) From 6bcd6950ef5ebebf7bba68d375d155120d0db0d0 Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Tue, 22 Aug 2023 16:59:58 -0400 Subject: [PATCH 26/29] Refactors replaceable consumers into a single function Activates calendar consumers --- .../amethyst/model/LocalCache.kt | 257 ++---------------- 1 file changed, 24 insertions(+), 233 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 ed7aae725..d6d7311a5 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/model/LocalCache.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/model/LocalCache.kt @@ -204,46 +204,6 @@ object LocalCache { } } - fun consume(event: PeopleListEvent) { - val version = getOrCreateNote(event.id) - val note = getOrCreateAddressableNote(event.address()) - val author = getOrCreateUser(event.pubKey) - - if (version.event == null) { - version.loadEvent(event, author, emptyList()) - version.moveAllReferencesTo(note) - } - - // Already processed this event. - if (note.event?.id() == event.id()) return - - if (event.createdAt > (note.createdAt() ?: 0)) { - note.loadEvent(event, author, emptyList()) - - refreshObservers(note) - } - } - - private fun consume(event: AdvertisedRelayListEvent) { - val version = getOrCreateNote(event.id) - val note = getOrCreateAddressableNote(event.address()) - val author = getOrCreateUser(event.pubKey) - - if (version.event == null) { - version.loadEvent(event, author, emptyList()) - version.moveAllReferencesTo(note) - } - - // Already processed this event. - if (note.event?.id() == event.id()) return - - if (event.createdAt > (note.createdAt() ?: 0)) { - note.loadEvent(event, author, emptyList()) - - refreshObservers(note) - } - } - fun formattedDateTime(timestamp: Long): String { return Instant.ofEpochSecond(timestamp).atZone(ZoneId.systemDefault()) .format(DateTimeFormatter.ofPattern("uuuu MMM d hh:mm a")) @@ -349,25 +309,6 @@ object LocalCache { refreshObservers(note) } - private fun consume(event: CommunityDefinitionEvent, relay: Relay?) { - val version = getOrCreateNote(event.id) - val note = getOrCreateAddressableNote(event.address()) - val author = getOrCreateUser(event.pubKey) - - if (version.event == null) { - version.loadEvent(event, author, emptyList()) - version.moveAllReferencesTo(note) - } - - if (note.event?.id() == event.id()) return - - if (event.createdAt > (note.createdAt() ?: 0)) { - note.loadEvent(event, author, emptyList()) - - refreshObservers(note) - } - } - private fun consume(event: LiveActivitiesEvent, relay: Relay?) { val version = getOrCreateNote(event.id) val note = getOrCreateAddressableNote(event.address()) @@ -392,140 +333,17 @@ object LocalCache { } } - fun consume(event: EmojiPackSelectionEvent) { - val version = getOrCreateNote(event.id) - val note = getOrCreateAddressableNote(event.address()) - val author = getOrCreateUser(event.pubKey) + fun consume(event: PeopleListEvent) { consumeBaseReplaceable(event) } + private fun consume(event: AdvertisedRelayListEvent) { consumeBaseReplaceable(event) } + private fun consume(event: CommunityDefinitionEvent, relay: Relay?) { consumeBaseReplaceable(event) } + fun consume(event: EmojiPackSelectionEvent) { consumeBaseReplaceable(event) } + private fun consume(event: EmojiPackEvent) { consumeBaseReplaceable(event) } + private fun consume(event: ClassifiedsEvent) { consumeBaseReplaceable(event) } + private fun consume(event: PinListEvent) { consumeBaseReplaceable(event) } + private fun consume(event: RelaySetEvent) { consumeBaseReplaceable(event) } + private fun consume(event: AudioTrackEvent) { consumeBaseReplaceable(event) } - if (version.event == null) { - version.loadEvent(event, author, emptyList()) - version.moveAllReferencesTo(note) - } - - if (note.event?.id() == event.id()) return - - if (event.createdAt > (note.createdAt() ?: 0)) { - note.loadEvent(event, author, emptyList()) - - refreshObservers(note) - } - } - - private fun consume(event: EmojiPackEvent) { - val version = getOrCreateNote(event.id) - val note = getOrCreateAddressableNote(event.address()) - val author = getOrCreateUser(event.pubKey) - - if (version.event == null) { - version.loadEvent(event, author, emptyList()) - version.moveAllReferencesTo(note) - } - - if (note.event?.id() == event.id()) return - - if (event.createdAt > (note.createdAt() ?: 0)) { - note.loadEvent(event, author, emptyList()) - - refreshObservers(note) - } - } - - private fun consume(event: ClassifiedsEvent) { - val version = getOrCreateNote(event.id) - val note = getOrCreateAddressableNote(event.address()) - val author = getOrCreateUser(event.pubKey) - - if (version.event == null) { - version.loadEvent(event, author, emptyList()) - version.moveAllReferencesTo(note) - } - - if (note.event?.id() == event.id()) return - - if (event.createdAt > (note.createdAt() ?: 0)) { - note.loadEvent(event, author, emptyList()) - - refreshObservers(note) - } - } - - private fun consume(event: PinListEvent) { - val version = getOrCreateNote(event.id) - val note = getOrCreateAddressableNote(event.address()) - val author = getOrCreateUser(event.pubKey) - - if (version.event == null) { - version.loadEvent(event, author, emptyList()) - version.moveAllReferencesTo(note) - } - - if (note.event?.id() == event.id()) return - - if (event.createdAt > (note.createdAt() ?: 0)) { - note.loadEvent(event, author, emptyList()) - - refreshObservers(note) - } - } - - private fun consume(event: RelaySetEvent) { - val version = getOrCreateNote(event.id) - val note = getOrCreateAddressableNote(event.address()) - val author = getOrCreateUser(event.pubKey) - - if (version.event == null) { - version.loadEvent(event, author, emptyList()) - version.moveAllReferencesTo(note) - } - - if (note.event?.id() == event.id()) return - - if (event.createdAt > (note.createdAt() ?: 0)) { - note.loadEvent(event, author, emptyList()) - - refreshObservers(note) - } - } - - private fun consume(event: AudioTrackEvent) { - val version = getOrCreateNote(event.id) - val note = getOrCreateAddressableNote(event.address()) - val author = getOrCreateUser(event.pubKey) - - if (version.event == null) { - version.loadEvent(event, author, emptyList()) - version.moveAllReferencesTo(note) - } - - // Already processed this event. - if (note.event?.id() == event.id()) return - - if (event.createdAt > (note.createdAt() ?: 0)) { - note.loadEvent(event, author, emptyList()) - - refreshObservers(note) - } - } - - fun consume(event: BadgeDefinitionEvent) { - val version = getOrCreateNote(event.id) - val note = getOrCreateAddressableNote(event.address()) - val author = getOrCreateUser(event.pubKey) - - if (version.event == null) { - version.loadEvent(event, author, emptyList()) - version.moveAllReferencesTo(note) - } - - // Already processed this event. - if (note.event?.id() == event.id()) return - - if (event.createdAt > (note.createdAt() ?: 0)) { - note.loadEvent(event, author, emptyList()) - - refreshObservers(note) - } - } + fun consume(event: BadgeDefinitionEvent) { consumeBaseReplaceable(event) } fun consume(event: BadgeProfilesEvent) { val version = getOrCreateNote(event.id) @@ -541,7 +359,7 @@ object LocalCache { if (note.event?.id() == event.id()) return val replyTo = event.badgeAwardEvents().mapNotNull { checkGetOrCreateNote(it) } + - event.badgeAwardDefinitions().mapNotNull { getOrCreateAddressableNote(it) } + event.badgeAwardDefinitions().map { getOrCreateAddressableNote(it) } if (event.createdAt > (note.createdAt() ?: 0)) { note.loadEvent(event, author, replyTo) @@ -571,7 +389,14 @@ object LocalCache { refreshObservers(note) } - private fun comsume(event: NNSEvent) { + private fun comsume(event: NNSEvent) { consumeBaseReplaceable(event) } + fun consume(event: AppDefinitionEvent) { consumeBaseReplaceable(event) } + private fun consume(event: CalendarEvent) { consumeBaseReplaceable(event) } + private fun consume(event: CalendarDateSlotEvent) { consumeBaseReplaceable(event) } + private fun consume(event: CalendarTimeSlotEvent) { consumeBaseReplaceable(event) } + private fun consume(event: CalendarRSVPEvent) { consumeBaseReplaceable(event) } + + private fun consumeBaseReplaceable(event: BaseAddressableEvent) { val version = getOrCreateNote(event.id) val note = getOrCreateAddressableNote(event.address()) val author = getOrCreateUser(event.pubKey) @@ -591,45 +416,7 @@ object LocalCache { } } - fun consume(event: AppDefinitionEvent) { - val version = getOrCreateNote(event.id) - val note = getOrCreateAddressableNote(event.address()) - val author = getOrCreateUser(event.pubKey) - - if (version.event == null) { - version.loadEvent(event, author, emptyList()) - version.moveAllReferencesTo(note) - } - - // Already processed this event. - if (note.event?.id() == event.id()) return - - if (event.createdAt > (note.createdAt() ?: 0)) { - note.loadEvent(event, author, emptyList()) - - refreshObservers(note) - } - } - - fun consume(event: AppRecommendationEvent) { - val version = getOrCreateNote(event.id) - val note = getOrCreateAddressableNote(event.address()) - val author = getOrCreateUser(event.pubKey) - - if (version.event == null) { - version.loadEvent(event, author, emptyList()) - version.moveAllReferencesTo(note) - } - - // Already processed this event. - if (note.event?.id() == event.id()) return - - if (event.createdAt > (note.createdAt() ?: 0)) { - note.loadEvent(event, author, emptyList()) - - refreshObservers(note) - } - } + fun consume(event: AppRecommendationEvent) { consumeBaseReplaceable(event) } @Suppress("UNUSED_PARAMETER") fun consume(event: RecommendRelayEvent) { @@ -1549,6 +1336,10 @@ object LocalCache { is BadgeDefinitionEvent -> consume(event) is BadgeProfilesEvent -> consume(event) is BookmarkListEvent -> consume(event) + is CalendarEvent -> consume(event) + is CalendarDateSlotEvent -> consume(event) + is CalendarTimeSlotEvent -> consume(event) + is CalendarRSVPEvent -> consume(event) is ChannelCreateEvent -> consume(event) is ChannelHideMessageEvent -> consume(event) is ChannelMessageEvent -> consume(event, relay) From ca89b701500118e82cc8257619e64927211c6c41 Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Tue, 22 Aug 2023 17:07:59 -0400 Subject: [PATCH 27/29] Activates pruning of old replaceable versions --- .../vitorpamplona/amethyst/ServiceManager.kt | 1 + .../amethyst/model/LocalCache.kt | 41 +++++++++++++++++++ 2 files changed, 42 insertions(+) diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ServiceManager.kt b/app/src/main/java/com/vitorpamplona/amethyst/ServiceManager.kt index d4f2efd81..48b7fc4f4 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ServiceManager.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ServiceManager.kt @@ -125,6 +125,7 @@ object ServiceManager { LocalCache.pruneHiddenMessages(it) LocalCache.pruneContactLists(accounts) LocalCache.pruneRepliesAndReactions(accounts) + LocalCache.prunePastVersionsOfReplaceables() } } } 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 d6d7311a5..f1be3b36c 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/model/LocalCache.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/model/LocalCache.kt @@ -1192,6 +1192,44 @@ object LocalCache { } } + fun prunePastVersionsOfReplaceables() { + val toBeRemoved = notes.filter { + val noteEvent = it.value.event + if (noteEvent is AddressableEvent) { + noteEvent.createdAt() < (addressables[noteEvent.address().toTag()]?.event?.createdAt() ?: 0) + } else { + false + } + }.values + + val childrenToBeRemoved = mutableListOf() + + toBeRemoved.forEach { + notes.remove(it.idHex) + + val newerVersion = addressables[(it.event as? AddressableEvent)?.address()?.toTag()] + if (newerVersion != null) { + it.moveAllReferencesTo(newerVersion) + } + + it.replyTo?.forEach { masterNote -> + masterNote.removeReply(it) + masterNote.removeBoost(it) + masterNote.removeReaction(it) + masterNote.removeZap(it) + masterNote.clearEOSE() // allows reloading of these events + } + + childrenToBeRemoved.addAll(it.removeAllChildNotes()) + } + + removeChildrenOf(childrenToBeRemoved) + + if (toBeRemoved.size > 1) { + println("PRUNE: ${toBeRemoved.size} old version of addressables removed.") + } + } + fun pruneRepliesAndReactions(accounts: Set) { checkNotInMainThread() @@ -1216,12 +1254,15 @@ object LocalCache { masterNote.removeBoost(it) masterNote.removeReaction(it) masterNote.removeZap(it) + masterNote.removeReport(it) masterNote.clearEOSE() // allows reloading of these events } childrenToBeRemoved.addAll(it.removeAllChildNotes()) } + removeChildrenOf(childrenToBeRemoved) + if (toBeRemoved.size > 1) { println("PRUNE: ${toBeRemoved.size} thread replies removed.") } From eb191406e62582c02fafcad0109a792e5ad64216 Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Tue, 22 Aug 2023 17:32:59 -0400 Subject: [PATCH 28/29] Vertical Arrangement not working as intended. --- .../com/vitorpamplona/amethyst/ui/actions/NewPostView.kt | 8 ++++---- 1 file changed, 4 insertions(+), 4 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 346e71377..ed4535fc2 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 @@ -42,6 +42,7 @@ import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment +import androidx.compose.ui.Alignment.Companion.CenterVertically import androidx.compose.ui.ExperimentalComposeUiApi import androidx.compose.ui.Modifier import androidx.compose.ui.draw.alpha @@ -886,19 +887,18 @@ fun DisplayLocationInTitle(geohash: String) { fun Notifying(baseMentions: ImmutableList?, onClick: (User) -> Unit) { val mentions = baseMentions?.toSet() - FlowRow(verticalArrangement = Arrangement.Center, modifier = Modifier.padding(horizontal = 10.dp)) { + FlowRow(horizontalArrangement = Arrangement.spacedBy(5.dp)) { if (!mentions.isNullOrEmpty()) { Text( stringResource(R.string.reply_notify), fontWeight = FontWeight.Bold, - color = MaterialTheme.colors.placeholderText + color = MaterialTheme.colors.placeholderText, + modifier = Modifier.align(CenterVertically) ) mentions.forEachIndexed { idx, user -> val innerUserState by user.live().metadata.observeAsState() innerUserState?.user?.let { myUser -> - Spacer(modifier = Modifier.width(5.dp)) - val tags = remember(innerUserState) { myUser.info?.latestMetadata?.tags?.toImmutableListOfLists() } From 8e913ef9740380601b8b27e84d72ed853d1fc520 Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Tue, 22 Aug 2023 17:49:44 -0400 Subject: [PATCH 29/29] Adds replies and boosts to Stories --- .../amethyst/ui/note/ReactionsRow.kt | 44 ++++++++++++------- .../ui/screen/loggedIn/VideoScreen.kt | 9 ++-- 2 files changed, 33 insertions(+), 20 deletions(-) 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 971a4e26b..6d1cad664 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 @@ -57,6 +57,7 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.ColorFilter import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource import androidx.compose.ui.semantics.Role @@ -695,20 +696,21 @@ fun BoostReaction( } ) { BoostIcon(baseNote, iconSize, grayTint, accountViewModel) - } - if (wantsToBoost) { - BoostTypeChoicePopup( - baseNote, - accountViewModel, - onDismiss = { - wantsToBoost = false - }, - onQuote = { - wantsToBoost = false - onQuotePress() - } - ) + if (wantsToBoost) { + BoostTypeChoicePopup( + baseNote, + iconSize, + accountViewModel, + onDismiss = { + wantsToBoost = false + }, + onQuote = { + wantsToBoost = false + onQuotePress() + } + ) + } } BoostText(baseNote, grayTint) @@ -795,6 +797,7 @@ fun LikeReaction( if (wantsToReact) { ReactionChoicePopup( baseNote, + iconSize, accountViewModel, onDismiss = { wantsToReact = false @@ -1225,10 +1228,14 @@ private fun DrawViewCount( @OptIn(ExperimentalLayoutApi::class) @Composable -private fun BoostTypeChoicePopup(baseNote: Note, accountViewModel: AccountViewModel, onDismiss: () -> Unit, onQuote: () -> Unit) { +private fun BoostTypeChoicePopup(baseNote: Note, iconSize: Dp, accountViewModel: AccountViewModel, onDismiss: () -> Unit, onQuote: () -> Unit) { + val iconSizePx = with(LocalDensity.current) { + -iconSize.toPx().toInt() + } + Popup( alignment = Alignment.BottomCenter, - offset = IntOffset(0, -50), + offset = IntOffset(0, iconSizePx), onDismissRequest = { onDismiss() } ) { FlowRow { @@ -1269,6 +1276,7 @@ private fun BoostTypeChoicePopup(baseNote: Note, accountViewModel: AccountViewMo @Composable fun ReactionChoicePopup( baseNote: Note, + iconSize: Dp, accountViewModel: AccountViewModel, onDismiss: () -> Unit, onChangeAmount: () -> Unit @@ -1280,9 +1288,13 @@ fun ReactionChoicePopup( baseNote.reactedBy(account.userProfile()).toSet() } + val iconSizePx = with(LocalDensity.current) { + -iconSize.toPx().toInt() + } + Popup( alignment = Alignment.BottomCenter, - offset = IntOffset(0, -50), + offset = IntOffset(0, iconSizePx), onDismissRequest = { onDismiss() } ) { FlowRow(horizontalArrangement = Arrangement.Center) { 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 4d0787bf0..9ab694bfe 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 @@ -46,6 +46,7 @@ import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.service.NostrVideoDataSource import com.vitorpamplona.amethyst.ui.actions.NewPostView import com.vitorpamplona.amethyst.ui.components.ObserveDisplayNip05Status +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 @@ -55,6 +56,7 @@ import com.vitorpamplona.amethyst.ui.note.NoteComposeReportState 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 @@ -449,13 +451,12 @@ fun ReactionsColumn(baseNote: Note, accountViewModel: AccountViewModel, nav: (St Spacer(modifier = Modifier.height(8.dp)) Column(horizontalAlignment = Alignment.CenterHorizontally, modifier = Modifier.padding(bottom = 75.dp, end = 20.dp)) { - /* - ReplyReaction(baseNote, accountViewModel, iconSize = 40.dp) { + ReplyReaction(baseNote, grayTint = MaterialTheme.colors.onBackground, accountViewModel, iconSize = 40.dp) { wantsToReplyTo = baseNote } - BoostReaction(baseNote, accountViewModel, iconSize = 40.dp) { + BoostReaction(baseNote, grayTint = MaterialTheme.colors.onBackground, accountViewModel, iconSize = 40.dp) { wantsToQuote = baseNote - }*/ + } LikeReaction(baseNote, grayTint = MaterialTheme.colors.onBackground, accountViewModel, nav, iconSize = 40.dp, heartSize = Size35dp, 28.sp) ZapReaction(baseNote, grayTint = MaterialTheme.colors.onBackground, accountViewModel, iconSize = 40.dp, animationSize = Size35dp) ViewCountReaction(baseNote, grayTint = MaterialTheme.colors.onBackground, barChartSize = 39.dp, viewCountColorFilter = MaterialTheme.colors.onBackgroundColorFilter)