From 3950517743aa396e9393cf98611708d1abe87c57 Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Tue, 16 May 2023 12:20:27 -0400 Subject: [PATCH 01/24] Improves rendering performance for chats. --- .../ui/note/ChatroomMessageCompose.kt | 660 +++++++++++------- 1 file changed, 398 insertions(+), 262 deletions(-) diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/note/ChatroomMessageCompose.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/ChatroomMessageCompose.kt index ffb7dbea5..a20a887be 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/note/ChatroomMessageCompose.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/ChatroomMessageCompose.kt @@ -27,6 +27,7 @@ import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.ChevronRight import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.derivedStateOf import androidx.compose.runtime.getValue import androidx.compose.runtime.livedata.observeAsState import androidx.compose.runtime.mutableStateOf @@ -45,6 +46,7 @@ import androidx.compose.ui.layout.onSizeChanged import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.platform.LocalUriHandler +import androidx.compose.ui.platform.UriHandler import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.unit.IntSize @@ -55,9 +57,12 @@ import com.google.accompanist.flowlayout.FlowRow import com.vitorpamplona.amethyst.NotificationCache import com.vitorpamplona.amethyst.R import com.vitorpamplona.amethyst.model.Note +import com.vitorpamplona.amethyst.model.User +import com.vitorpamplona.amethyst.service.model.AudioTrackEvent import com.vitorpamplona.amethyst.service.model.ChannelCreateEvent import com.vitorpamplona.amethyst.service.model.ChannelMessageEvent import com.vitorpamplona.amethyst.service.model.ChannelMetadataEvent +import com.vitorpamplona.amethyst.service.model.EventInterface import com.vitorpamplona.amethyst.ui.components.CreateClickableTextWithEmoji import com.vitorpamplona.amethyst.ui.components.CreateTextWithEmoji import com.vitorpamplona.amethyst.ui.components.ResizeImage @@ -67,6 +72,7 @@ import com.vitorpamplona.amethyst.ui.components.TranslatableRichTextViewer import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext val ChatBubbleShapeMe = RoundedCornerShape(15.dp, 15.dp, 3.dp, 15.dp) val ChatBubbleShapeThem = RoundedCornerShape(3.dp, 15.dp, 15.dp, 15.dp) @@ -82,320 +88,416 @@ fun ChatroomMessageCompose( navController: NavController, onWantsToReply: (Note) -> Unit ) { - val noteState by baseNote.live().metadata.observeAsState() - val note = noteState?.note - val accountState by accountViewModel.accountLiveData.observeAsState() - val account = accountState?.account ?: return + val account = remember(accountState) { accountState?.account } ?: return + val loggedIn = remember(accountState) { accountState?.account?.userProfile() } ?: return + + val noteState by baseNote.live().metadata.observeAsState() + val note = remember(noteState) { noteState?.note } val noteReportsState by baseNote.live().reports.observeAsState() - val noteForReports = noteReportsState?.note ?: return + val noteForReports = remember(noteReportsState) { noteReportsState?.note } ?: return - val accountUser = account.userProfile() + val noteEvent = note?.event var popupExpanded by remember { mutableStateOf(false) } - var showHiddenNote by remember { mutableStateOf(false) } - val context = LocalContext.current.applicationContext - val scope = rememberCoroutineScope() + if (noteEvent == null) { + BlankNote(Modifier.combinedClickable( + onClick = { }, + onLongClick = { popupExpanded = true } + )) - if (note?.event == null) { - BlankNote(Modifier) - } else if (!account.isAcceptable(noteForReports) && !showHiddenNote) { - if (!account.isHidden(noteForReports.author!!)) { - HiddenNote( - account.getRelevantReports(noteForReports), - account.userProfile(), - Modifier, - innerQuote, - navController, - onClick = { showHiddenNote = true } - ) + note?.let { + NoteQuickActionMenu(it, popupExpanded, { popupExpanded = false }, accountViewModel) } } else { - var backgroundBubbleColor: Color - var alignment: Arrangement.Horizontal - var shape: Shape + var showHiddenNote by remember { mutableStateOf(false) } + var isAcceptableAndCanPreview by remember { mutableStateOf(Pair(true, true)) } - val grayTint = MaterialTheme.colors.onSurface.copy(alpha = 0.32f) + LaunchedEffect(key1 = noteReportsState, key2 = accountState) { + withContext(Dispatchers.IO) { + account.userProfile().let { loggedIn -> + val newCanPreview = note.author === loggedIn || + (note.author?.let { loggedIn.isFollowingCached(it) } ?: true) || + !(noteForReports.hasAnyReports()) - if (note.author == accountUser) { - backgroundBubbleColor = MaterialTheme.colors.primary.copy(alpha = 0.32f) - alignment = Arrangement.End - shape = ChatBubbleShapeMe - } else { - backgroundBubbleColor = MaterialTheme.colors.onSurface.copy(alpha = 0.12f) - alignment = Arrangement.Start - shape = ChatBubbleShapeThem - } + val newIsAcceptable = account.isAcceptable(noteForReports) - if (parentBackgroundColor != null) { - backgroundBubbleColor = backgroundBubbleColor.compositeOver(parentBackgroundColor) - } else { - backgroundBubbleColor = backgroundBubbleColor.compositeOver(MaterialTheme.colors.background) - } - - var isNew by remember { mutableStateOf(false) } - - LaunchedEffect(key1 = routeForLastRead) { - routeForLastRead?.let { - scope.launch(Dispatchers.IO) { - val lastTime = NotificationCache.load(it) - - val createdAt = note.createdAt() - if (createdAt != null) { - NotificationCache.markAsRead(it, createdAt) - isNew = createdAt > lastTime + if (newIsAcceptable != isAcceptableAndCanPreview.first && newCanPreview != isAcceptableAndCanPreview.second) { + isAcceptableAndCanPreview = Pair(newIsAcceptable, newCanPreview) } } } } - Column() { - val modif = if (innerQuote) { - Modifier.padding(top = 10.dp, end = 5.dp) - } else { - Modifier - .fillMaxWidth(1f) - .padding( - start = 12.dp, - end = 12.dp, - top = 5.dp, - bottom = 5.dp - ) + if (!isAcceptableAndCanPreview.first && !showHiddenNote) { + if (!account.isHidden(noteForReports.author!!)) { + HiddenNote( + account.getRelevantReports(noteForReports), + account.userProfile(), + Modifier, + innerQuote, + navController, + onClick = { showHiddenNote = true } + ) } - Row( - modifier = modif, - horizontalArrangement = alignment - ) { - var availableBubbleSize by remember { mutableStateOf(IntSize.Zero) } - val modif2 = if (innerQuote) Modifier else Modifier.fillMaxWidth(0.85f) + } else { + val backgroundBubbleColor: Color + val alignment: Arrangement.Horizontal + val shape: Shape + + if (note.author == loggedIn) { + backgroundBubbleColor = MaterialTheme.colors.primary.copy(alpha = 0.32f) + .compositeOver(parentBackgroundColor ?: MaterialTheme.colors.background) + + alignment = Arrangement.End + shape = ChatBubbleShapeMe + } else { + backgroundBubbleColor = MaterialTheme.colors.onSurface.copy(alpha = 0.12f) + .compositeOver(parentBackgroundColor ?: MaterialTheme.colors.background) + + alignment = Arrangement.Start + shape = ChatBubbleShapeThem + } + + val scope = rememberCoroutineScope() + + LaunchedEffect(key1 = routeForLastRead) { + routeForLastRead?.let { + scope.launch(Dispatchers.IO) { + val lastTime = NotificationCache.load(it) + + val createdAt = note.createdAt() + if (createdAt != null) { + NotificationCache.markAsRead(it, createdAt) + } + } + } + } + + Column() { + val modif = remember { + if (innerQuote) { + Modifier.padding(top = 10.dp, end = 5.dp) + } else { + Modifier + .fillMaxWidth(1f) + .padding( + start = 12.dp, + end = 12.dp, + top = 5.dp, + bottom = 5.dp + ) + } + } Row( - horizontalArrangement = alignment, - modifier = modif2.onSizeChanged { - availableBubbleSize = it - } + modifier = modif, + horizontalArrangement = alignment ) { - Surface( - color = backgroundBubbleColor, - shape = shape, - modifier = Modifier - .combinedClickable( - onClick = { }, - onLongClick = { popupExpanded = true } - ) + var availableBubbleSize by remember { mutableStateOf(IntSize.Zero) } + val modif2 = if (innerQuote) Modifier else Modifier.fillMaxWidth(0.85f) + + Row( + horizontalArrangement = alignment, + modifier = modif2.onSizeChanged { + availableBubbleSize = it + } ) { - var bubbleSize by remember { mutableStateOf(IntSize.Zero) } - - Column( + Surface( + color = backgroundBubbleColor, + shape = shape, modifier = Modifier - .padding(start = 10.dp, end = 5.dp, bottom = 5.dp) - .onSizeChanged { - bubbleSize = it - } + .combinedClickable( + onClick = { }, + onLongClick = { popupExpanded = true } + ) ) { - val authorState by note.author!!.live().metadata.observeAsState() - val author = authorState?.user!! + var bubbleSize by remember { mutableStateOf(IntSize.Zero) } - if (innerQuote || author != accountUser && note.event is ChannelMessageEvent) { - Row( - verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = alignment, - modifier = Modifier.padding(top = 5.dp) - ) { - RobohashAsyncImageProxy( - robot = author.pubkeyHex, - model = ResizeImage(author.profilePicture(), 25.dp), - contentDescription = stringResource(id = R.string.profile_image), - modifier = Modifier - .width(25.dp) - .height(25.dp) - .clip(shape = CircleShape) - .clickable(onClick = { - author.let { - navController.navigate("User/${it.pubkeyHex}") - } - }) - ) - - CreateClickableTextWithEmoji( - clickablePart = " ${author.toBestDisplayName()}", - suffix = "", - tags = author.info?.latestMetadata?.tags, - fontWeight = FontWeight.Bold, - overrideColor = MaterialTheme.colors.onBackground, - route = "User/${author.pubkeyHex}", - navController = navController - ) - } - } - - val replyTo = note.replyTo - if (!innerQuote && !replyTo.isNullOrEmpty()) { - Row(verticalAlignment = Alignment.CenterVertically) { - replyTo.toSet().mapIndexed { _, note -> - ChatroomMessageCompose( - note, - null, - innerQuote = true, - parentBackgroundColor = backgroundBubbleColor, - accountViewModel = accountViewModel, - navController = navController, - onWantsToReply = onWantsToReply - ) - } - } - } - - Row(verticalAlignment = Alignment.CenterVertically) { - val event = note.event - if (event is ChannelCreateEvent) { - val channelInfo = event.channelInfo() - val text = note.author?.toBestDisplayName() - .toString() + " ${stringResource(R.string.created)} " + ( - channelInfo.name - ?: "" - ) + " ${stringResource(R.string.with_description_of)} '" + ( - channelInfo.about - ?: "" - ) + "', ${stringResource(R.string.and_picture)} '" + ( - channelInfo.picture - ?: "" - ) + "'" - - CreateTextWithEmoji( - text = text, - tags = note.author?.info?.latestMetadata?.tags - ) - } else if (event is ChannelMetadataEvent) { - val channelInfo = event.channelInfo() - val text = note.author?.toBestDisplayName() - .toString() + " ${stringResource(R.string.changed_chat_name_to)} '" + ( - channelInfo.name - ?: "" - ) + "', ${stringResource(R.string.description_to)} '" + ( - channelInfo.about - ?: "" - ) + "', ${stringResource(R.string.and_picture_to)} '" + ( - channelInfo.picture - ?: "" - ) + "'" - - CreateTextWithEmoji( - text = text, - tags = note.author?.info?.latestMetadata?.tags - ) - } else { - val eventContent = accountViewModel.decrypt(note) - - val canPreview = note.author == accountUser || - (note.author?.let { accountUser.isFollowingCached(it) } ?: true) || - !noteForReports.hasAnyReports() - - if (eventContent != null) { - TranslatableRichTextViewer( - eventContent, - canPreview, - Modifier.padding(top = 5.dp), - note.event?.tags(), - backgroundBubbleColor, - accountViewModel, - navController - ) - } else { - TranslatableRichTextViewer( - stringResource(R.string.could_not_decrypt_the_message), - true, - Modifier.padding(top = 5.dp), - note.event?.tags(), - backgroundBubbleColor, - accountViewModel, - navController - ) - } - } - } - - Row( - verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = Arrangement.SpaceBetween, + Column( modifier = Modifier - .padding(top = 5.dp) - .then( - with(LocalDensity.current) { - Modifier.widthIn( - bubbleSize.width.toDp(), - availableBubbleSize.width.toDp() + .padding(start = 10.dp, end = 5.dp, bottom = 5.dp) + .onSizeChanged { + bubbleSize = it + } + ) { + if ((innerQuote || note.author != loggedIn) && noteEvent is ChannelMessageEvent) { + DrawAuthorInfo( + baseNote, + alignment, + navController + ) + } + + val replyTo = note.replyTo + if (!innerQuote && !replyTo.isNullOrEmpty()) { + Row(verticalAlignment = Alignment.CenterVertically) { + replyTo.toSet().mapIndexed { _, note -> + ChatroomMessageCompose( + note, + null, + innerQuote = true, + parentBackgroundColor = backgroundBubbleColor, + accountViewModel = accountViewModel, + navController = navController, + onWantsToReply = onWantsToReply ) } - ) - ) { - Row() { - Text( - timeAgoShort(note.createdAt(), context), - color = MaterialTheme.colors.onSurface.copy(alpha = 0.32f), - fontSize = 12.sp - ) - - RelayBadges(note) - - Spacer(modifier = Modifier.width(10.dp)) + } } - Row() { - LikeReaction(baseNote, grayTint, accountViewModel) - Spacer(modifier = Modifier.width(5.dp)) - ZapReaction(baseNote, grayTint, accountViewModel) - Spacer(modifier = Modifier.width(5.dp)) - ReplyReaction(baseNote, grayTint, accountViewModel, showCounter = false) { - onWantsToReply(baseNote) + Row(verticalAlignment = Alignment.CenterVertically) { + when (noteEvent) { + is ChannelCreateEvent -> { + RenderCreateChannelNote(note) + } + + is ChannelMetadataEvent -> { + RenderChangeChannelMetadataNote(note) + } + + else -> { + RenderRegularTextNote( + note, + loggedIn, + isAcceptableAndCanPreview.second, + backgroundBubbleColor, + accountViewModel, + navController + ) + } } } + + Row( + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.SpaceBetween, + modifier = Modifier + .padding(top = 5.dp) + .then( + with(LocalDensity.current) { + Modifier.widthIn( + bubbleSize.width.toDp(), + availableBubbleSize.width.toDp() + ) + } + ) + ) { + StatusRow( + baseNote, + accountViewModel, + onWantsToReply + ) + } } } } - } - NoteQuickActionMenu(note, popupExpanded, { popupExpanded = false }, accountViewModel) + NoteQuickActionMenu( + note, + popupExpanded, + { popupExpanded = false }, + accountViewModel + ) + } } } } } +@Composable +private fun StatusRow( + baseNote: Note, + accountViewModel: AccountViewModel, + onWantsToReply: (Note) -> Unit +) { + val grayTint = MaterialTheme.colors.onSurface.copy(alpha = 0.32f) + val time = remember { baseNote.createdAt() ?: 0 } + + Row() { + ChatTimeAgo(time) + RelayBadges(baseNote) + Spacer(modifier = Modifier.width(10.dp)) + } + + Row() { + LikeReaction(baseNote, grayTint, accountViewModel) + Spacer(modifier = Modifier.width(5.dp)) + ZapReaction(baseNote, grayTint, accountViewModel) + Spacer(modifier = Modifier.width(5.dp)) + ReplyReaction(baseNote, grayTint, accountViewModel, showCounter = false) { + onWantsToReply(baseNote) + } + } +} + +@Composable +fun ChatTimeAgo(time: Long) { + val context = LocalContext.current + + var timeStr by remember { mutableStateOf("") } + + LaunchedEffect(key1 = time) { + withContext(Dispatchers.IO) { + timeStr = timeAgoShort(time, context = context) + } + } + + Text( + timeStr, + color = MaterialTheme.colors.onSurface.copy(alpha = 0.32f), + fontSize = 12.sp + ) +} + +@Composable +private fun RenderRegularTextNote( + note: Note, + loggedIn: User, + canPreview: Boolean, + backgroundBubbleColor: Color, + accountViewModel: AccountViewModel, + navController: NavController +) { + val tags = remember { note.event?.tags() } + val eventContent = remember { accountViewModel.decrypt(note) } + val modifier = remember { Modifier.padding(top = 5.dp) } + + if (eventContent != null) { + TranslatableRichTextViewer( + eventContent, + canPreview, + modifier, + tags, + backgroundBubbleColor, + accountViewModel, + navController + ) + } else { + TranslatableRichTextViewer( + stringResource(R.string.could_not_decrypt_the_message), + true, + modifier, + tags, + backgroundBubbleColor, + accountViewModel, + navController + ) + } +} + +@Composable +private fun RenderChangeChannelMetadataNote( + note: Note +) { + val noteEvent = note.event as? ChannelMetadataEvent ?: return + + val channelInfo = noteEvent.channelInfo() + val text = note.author?.toBestDisplayName() + .toString() + " ${stringResource(R.string.changed_chat_name_to)} '" + ( + channelInfo.name + ?: "" + ) + "', ${stringResource(R.string.description_to)} '" + ( + channelInfo.about + ?: "" + ) + "', ${stringResource(R.string.and_picture_to)} '" + ( + channelInfo.picture + ?: "" + ) + "'" + + CreateTextWithEmoji( + text = text, + tags = note.author?.info?.latestMetadata?.tags + ) +} + +@Composable +private fun RenderCreateChannelNote(note: Note) { + val noteEvent = note.event as? ChannelCreateEvent ?: return + val channelInfo = remember { noteEvent.channelInfo() } + + val text = note.author?.toBestDisplayName() + .toString() + " ${stringResource(R.string.created)} " + ( + channelInfo.name + ?: "" + ) + " ${stringResource(R.string.with_description_of)} '" + ( + channelInfo.about + ?: "" + ) + "', ${stringResource(R.string.and_picture)} '" + ( + channelInfo.picture + ?: "" + ) + "'" + + CreateTextWithEmoji( + text = text, + tags = note.author?.info?.latestMetadata?.tags + ) +} + +@Composable +private fun DrawAuthorInfo( + baseNote: Note, + alignment: Arrangement.Horizontal, + navController: NavController +) { + val userState by baseNote.author!!.live().metadata.observeAsState() + + val pubkeyHex = remember { baseNote.author?.pubkeyHex } ?: return + val route = remember { "User/$pubkeyHex" } + val userDisplayName = remember(userState) { userState?.user?.toBestDisplayName() } + val userProfilePicture = remember(userState) { ResizeImage(userState?.user?.profilePicture(), 25.dp) } + val userTags = remember(userState) { userState?.user?.info?.latestMetadata?.tags } + + Row( + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = alignment, + modifier = Modifier.padding(top = 5.dp) + ) { + RobohashAsyncImageProxy( + robot = pubkeyHex, + model = userProfilePicture, + contentDescription = stringResource(id = R.string.profile_image), + modifier = Modifier + .width(25.dp) + .height(25.dp) + .clip(shape = CircleShape) + .clickable(onClick = { + navController.navigate(route) + }) + ) + + CreateClickableTextWithEmoji( + clickablePart = " $userDisplayName", + suffix = "", + tags = userTags, + fontWeight = FontWeight.Bold, + overrideColor = MaterialTheme.colors.onBackground, + route = route, + navController = navController + ) + } +} + @Composable private fun RelayBadges(baseNote: Note) { val noteRelaysState by baseNote.live().relays.observeAsState() - val noteRelays = noteRelaysState?.note?.relays ?: emptySet() + val noteRelays = remember(noteRelaysState) { noteRelaysState?.note?.relays ?: emptySet() } + val noteRelaysSimple = remember(noteRelaysState) { noteRelaysState?.note?.relays?.take(3) ?: emptySet() } var expanded by remember { mutableStateOf(false) } - val relaysToDisplay = if (expanded) noteRelays else noteRelays.take(3) - - val uri = LocalUriHandler.current + val relaysToDisplay by remember { + derivedStateOf { + if (expanded) noteRelays else noteRelaysSimple + } + } FlowRow(Modifier.padding(start = 10.dp)) { relaysToDisplay.forEach { - val url = it.removePrefix("wss://").removePrefix("ws://") - Box( - Modifier - .size(15.dp) - .padding(1.dp) - ) { - RobohashFallbackAsyncImage( - robot = "https://$url/favicon.ico", - robotSize = 15.dp, - model = "https://$url/favicon.ico", - contentDescription = stringResource(id = R.string.relay_icon), - colorFilter = ColorFilter.colorMatrix(ColorMatrix().apply { setToSaturation(0f) }), - modifier = Modifier - .fillMaxSize(1f) - .clip(shape = CircleShape) - .background(MaterialTheme.colors.background) - .clickable(onClick = { uri.openUri("https://$url") }) - ) - } + RenderRelay(it) } if (noteRelays.size > 3 && !expanded) { @@ -413,3 +515,37 @@ private fun RelayBadges(baseNote: Note) { } } } + +@Composable +private fun RenderRelay(dirtyUrl: String) { + val uri = LocalUriHandler.current + val website = remember { + val cleanUrl = dirtyUrl.removePrefix("wss://").removePrefix("ws://") + "https://$cleanUrl" + } + val iconUrl = remember { + val cleanUrl = dirtyUrl.removePrefix("wss://").removePrefix("ws://") + "https://$cleanUrl/favicon.ico" + } + + Box( + remember { + Modifier + .size(15.dp) + .padding(1.dp) + } + ) { + RobohashFallbackAsyncImage( + robot = iconUrl, + robotSize = 15.dp, + model = iconUrl, + contentDescription = stringResource(id = R.string.relay_icon), + colorFilter = ColorFilter.colorMatrix(ColorMatrix().apply { setToSaturation(0f) }), + modifier = Modifier + .fillMaxSize(1f) + .clip(shape = CircleShape) + .background(MaterialTheme.colors.background) + .clickable(onClick = { uri.openUri(website) }) + ) + } +} From cf5f345d545290d6cfe4a4baddf3486b240da0c9 Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Tue, 16 May 2023 12:20:35 -0400 Subject: [PATCH 02/24] Formatting --- .../ui/note/ChatroomMessageCompose.kt | 54 +++++++++---------- 1 file changed, 26 insertions(+), 28 deletions(-) diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/note/ChatroomMessageCompose.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/ChatroomMessageCompose.kt index a20a887be..167ebc19b 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/note/ChatroomMessageCompose.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/ChatroomMessageCompose.kt @@ -46,7 +46,6 @@ import androidx.compose.ui.layout.onSizeChanged import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.platform.LocalUriHandler -import androidx.compose.ui.platform.UriHandler import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.unit.IntSize @@ -58,11 +57,9 @@ import com.vitorpamplona.amethyst.NotificationCache import com.vitorpamplona.amethyst.R import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.model.User -import com.vitorpamplona.amethyst.service.model.AudioTrackEvent import com.vitorpamplona.amethyst.service.model.ChannelCreateEvent import com.vitorpamplona.amethyst.service.model.ChannelMessageEvent import com.vitorpamplona.amethyst.service.model.ChannelMetadataEvent -import com.vitorpamplona.amethyst.service.model.EventInterface import com.vitorpamplona.amethyst.ui.components.CreateClickableTextWithEmoji import com.vitorpamplona.amethyst.ui.components.CreateTextWithEmoji import com.vitorpamplona.amethyst.ui.components.ResizeImage @@ -103,10 +100,12 @@ fun ChatroomMessageCompose( var popupExpanded by remember { mutableStateOf(false) } if (noteEvent == null) { - BlankNote(Modifier.combinedClickable( - onClick = { }, - onLongClick = { popupExpanded = true } - )) + BlankNote( + Modifier.combinedClickable( + onClick = { }, + onLongClick = { popupExpanded = true } + ) + ) note?.let { NoteQuickActionMenu(it, popupExpanded, { popupExpanded = false }, accountViewModel) @@ -119,8 +118,8 @@ fun ChatroomMessageCompose( withContext(Dispatchers.IO) { account.userProfile().let { loggedIn -> val newCanPreview = note.author === loggedIn || - (note.author?.let { loggedIn.isFollowingCached(it) } ?: true) || - !(noteForReports.hasAnyReports()) + (note.author?.let { loggedIn.isFollowingCached(it) } ?: true) || + !(noteForReports.hasAnyReports()) val newIsAcceptable = account.isAcceptable(noteForReports) @@ -142,7 +141,6 @@ fun ChatroomMessageCompose( onClick = { showHiddenNote = true } ) } - } else { val backgroundBubbleColor: Color val alignment: Arrangement.Horizontal @@ -398,15 +396,15 @@ private fun RenderChangeChannelMetadataNote( val channelInfo = noteEvent.channelInfo() val text = note.author?.toBestDisplayName() .toString() + " ${stringResource(R.string.changed_chat_name_to)} '" + ( - channelInfo.name - ?: "" - ) + "', ${stringResource(R.string.description_to)} '" + ( - channelInfo.about - ?: "" - ) + "', ${stringResource(R.string.and_picture_to)} '" + ( - channelInfo.picture - ?: "" - ) + "'" + channelInfo.name + ?: "" + ) + "', ${stringResource(R.string.description_to)} '" + ( + channelInfo.about + ?: "" + ) + "', ${stringResource(R.string.and_picture_to)} '" + ( + channelInfo.picture + ?: "" + ) + "'" CreateTextWithEmoji( text = text, @@ -421,15 +419,15 @@ private fun RenderCreateChannelNote(note: Note) { val text = note.author?.toBestDisplayName() .toString() + " ${stringResource(R.string.created)} " + ( - channelInfo.name - ?: "" - ) + " ${stringResource(R.string.with_description_of)} '" + ( - channelInfo.about - ?: "" - ) + "', ${stringResource(R.string.and_picture)} '" + ( - channelInfo.picture - ?: "" - ) + "'" + channelInfo.name + ?: "" + ) + " ${stringResource(R.string.with_description_of)} '" + ( + channelInfo.about + ?: "" + ) + "', ${stringResource(R.string.and_picture)} '" + ( + channelInfo.picture + ?: "" + ) + "'" CreateTextWithEmoji( text = text, From 27182a77a437c6a504580a53d8175af45616be15 Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Tue, 16 May 2023 12:25:16 -0400 Subject: [PATCH 03/24] v0.49.3 --- app/build.gradle | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/build.gradle b/app/build.gradle index 2e3b151c7..36cd3b7cb 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -13,8 +13,8 @@ android { applicationId "com.vitorpamplona.amethyst" minSdk 26 targetSdk 33 - versionCode 165 - versionName "0.49.2" + versionCode 166 + versionName "0.49.3" testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" vectorDrawables { From dd8b208a0d6227592f51f123ecb754030784dae5 Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Tue, 16 May 2023 17:52:39 -0400 Subject: [PATCH 04/24] Avoiding creating a lnurl link with just those words --- .../vitorpamplona/amethyst/service/lnurl/LnWithdrawalUtil.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/main/java/com/vitorpamplona/amethyst/service/lnurl/LnWithdrawalUtil.kt b/app/src/main/java/com/vitorpamplona/amethyst/service/lnurl/LnWithdrawalUtil.kt index bc5d60083..47183238e 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/service/lnurl/LnWithdrawalUtil.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/service/lnurl/LnWithdrawalUtil.kt @@ -4,7 +4,7 @@ import java.util.regex.Pattern object LnWithdrawalUtil { private val withdrawalPattern = Pattern.compile( - "lnurl.*", + "lnurl.+", Pattern.CASE_INSENSITIVE ) From d6c29667925428e1816dc2e83ead705a458d55b8 Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Tue, 16 May 2023 17:53:02 -0400 Subject: [PATCH 05/24] Adds color to relay icons --- .../main/java/com/vitorpamplona/amethyst/ui/note/NoteCompose.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/note/NoteCompose.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/NoteCompose.kt index cc7723a6f..bfdcfb7dc 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 @@ -1941,7 +1941,7 @@ private fun RelayIconCompose(url: String) { .clickable(onClick = { uri.openUri("https://$url") }) } val colorFilter = remember { - ColorFilter.colorMatrix(ColorMatrix().apply { setToSaturation(0f) }) + ColorFilter.colorMatrix(ColorMatrix().apply { setToSaturation(0.5f) }) } Box(boxModifier) { From 389afcb6003e31e3c8c39d400d1f459ae8e5322a Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Tue, 16 May 2023 17:53:15 -0400 Subject: [PATCH 06/24] Fixes Robohash dependency in size --- .../vitorpamplona/amethyst/ui/components/RobohashAsyncImage.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/components/RobohashAsyncImage.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/components/RobohashAsyncImage.kt index b5d08d167..c387bfa5d 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/components/RobohashAsyncImage.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/components/RobohashAsyncImage.kt @@ -37,7 +37,7 @@ fun RobohashAsyncImage( robotSize.roundToPx() } - val imageRequest = remember(size, robot) { + val imageRequest = remember(robotSize, robot) { Robohash.imageRequest( context, robot, From 135596591a48766d266869dda0a3e4718aade8eb Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Tue, 16 May 2023 17:53:36 -0400 Subject: [PATCH 07/24] Optimizes the code for chat bubble rendering --- .../ui/note/ChatroomMessageCompose.kt | 79 ++++++++++++------- 1 file changed, 50 insertions(+), 29 deletions(-) diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/note/ChatroomMessageCompose.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/ChatroomMessageCompose.kt index 167ebc19b..bba847864 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/note/ChatroomMessageCompose.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/ChatroomMessageCompose.kt @@ -315,18 +315,18 @@ private fun StatusRow( val grayTint = MaterialTheme.colors.onSurface.copy(alpha = 0.32f) val time = remember { baseNote.createdAt() ?: 0 } - Row() { + Row(verticalAlignment = Alignment.CenterVertically) { ChatTimeAgo(time) RelayBadges(baseNote) Spacer(modifier = Modifier.width(10.dp)) } - Row() { + Row(verticalAlignment = Alignment.CenterVertically) { LikeReaction(baseNote, grayTint, accountViewModel) Spacer(modifier = Modifier.width(5.dp)) ZapReaction(baseNote, grayTint, accountViewModel) Spacer(modifier = Modifier.width(5.dp)) - ReplyReaction(baseNote, grayTint, accountViewModel, showCounter = false) { + ReplyReaction(baseNote, grayTint, accountViewModel, showCounter = false, iconSize = 16.dp) { onWantsToReply(baseNote) } } @@ -479,17 +479,29 @@ private fun DrawAuthorInfo( } } +data class RelayBadgesState( + val shouldDisplayExpandButton: Boolean, + val noteRelays: List, + val noteRelaysSimple: List +) + @Composable private fun RelayBadges(baseNote: Note) { val noteRelaysState by baseNote.live().relays.observeAsState() - val noteRelays = remember(noteRelaysState) { noteRelaysState?.note?.relays ?: emptySet() } - val noteRelaysSimple = remember(noteRelaysState) { noteRelaysState?.note?.relays?.take(3) ?: emptySet() } + + val state: RelayBadgesState by remember(noteRelaysState) { + val newShouldDisplayExpandButton = (noteRelaysState?.note?.relays?.size ?: 0) > 3 + val noteRelays = noteRelaysState?.note?.relays?.toList() ?: emptyList() + val noteRelaysSimple = noteRelaysState?.note?.relays?.take(3)?.toList() ?: emptyList() + + mutableStateOf(RelayBadgesState(newShouldDisplayExpandButton, noteRelays, noteRelaysSimple)) + } var expanded by remember { mutableStateOf(false) } val relaysToDisplay by remember { derivedStateOf { - if (expanded) noteRelays else noteRelaysSimple + if (expanded) state.noteRelays else state.noteRelaysSimple } } @@ -497,19 +509,19 @@ private fun RelayBadges(baseNote: Note) { relaysToDisplay.forEach { RenderRelay(it) } + } - if (noteRelays.size > 3 && !expanded) { - IconButton( - modifier = Modifier.then(Modifier.size(15.dp)), - onClick = { expanded = true } - ) { - Icon( - imageVector = Icons.Default.ChevronRight, - null, - modifier = Modifier.size(15.dp), - tint = MaterialTheme.colors.onSurface.copy(alpha = 0.32f) - ) - } + if (state.shouldDisplayExpandButton && !expanded) { + IconButton( + modifier = Modifier.then(Modifier.size(15.dp)), + onClick = { expanded = true } + ) { + Icon( + imageVector = Icons.Default.ChevronRight, + null, + modifier = Modifier.size(15.dp), + tint = MaterialTheme.colors.onSurface.copy(alpha = 0.32f) + ) } } } @@ -526,24 +538,33 @@ private fun RenderRelay(dirtyUrl: String) { "https://$cleanUrl/favicon.ico" } + val clickableModifier = remember { + Modifier + .size(15.dp) + .padding(1.dp) + .clickable(onClick = { uri.openUri(website) }) + } + + val colorFilter = remember { + ColorFilter.colorMatrix(ColorMatrix().apply { setToSaturation(0.5f) }) + } + + val iconModifier = remember { + Modifier + .fillMaxSize(1f) + .clip(shape = CircleShape) + } + Box( - remember { - Modifier - .size(15.dp) - .padding(1.dp) - } + modifier = clickableModifier ) { RobohashFallbackAsyncImage( robot = iconUrl, robotSize = 15.dp, model = iconUrl, contentDescription = stringResource(id = R.string.relay_icon), - colorFilter = ColorFilter.colorMatrix(ColorMatrix().apply { setToSaturation(0f) }), - modifier = Modifier - .fillMaxSize(1f) - .clip(shape = CircleShape) - .background(MaterialTheme.colors.background) - .clickable(onClick = { uri.openUri(website) }) + colorFilter = colorFilter, + modifier = iconModifier.background(MaterialTheme.colors.background) ) } } From 1519e70b24691d127b85f2521dc02ce2016b61d6 Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Tue, 16 May 2023 18:03:24 -0400 Subject: [PATCH 08/24] Increasing size of the "More Options" button in the Video Feed. --- .../vitorpamplona/amethyst/ui/screen/loggedIn/VideoScreen.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 4ae9fef50..7dc3d86dc 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 @@ -275,7 +275,7 @@ private fun RenderVideoOrPictureNote( Icon( imageVector = Icons.Default.MoreVert, null, - modifier = Modifier.size(15.dp), + modifier = Modifier.size(20.dp), tint = MaterialTheme.colors.onSurface.copy(alpha = 0.32f) ) From 5a3cf4040210aefd7217e301ad93c7953635f9cc Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Tue, 16 May 2023 18:43:58 -0400 Subject: [PATCH 09/24] Adds online search when typing the username in the new Post field. --- .../amethyst/ui/actions/JoinUserOrChannelView.kt | 1 + .../vitorpamplona/amethyst/ui/actions/NewPostView.kt | 8 ++++++++ .../amethyst/ui/actions/NewPostViewModel.kt | 11 +++++++++-- .../amethyst/ui/screen/loggedIn/SearchScreen.kt | 5 ++++- 4 files changed, 22 insertions(+), 3 deletions(-) diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/JoinUserOrChannelView.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/JoinUserOrChannelView.kt index 310eed8d1..eda028bf0 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/JoinUserOrChannelView.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/JoinUserOrChannelView.kt @@ -75,6 +75,7 @@ import kotlinx.coroutines.withContext @Composable fun JoinUserOrChannelView(onClose: () -> Unit, account: Account, navController: NavController) { val searchBarViewModel: SearchBarViewModel = viewModel() + searchBarViewModel.account = account Dialog( onDismissRequest = { 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 a663d5d1e..dc8a01489 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 @@ -25,6 +25,7 @@ import androidx.compose.material.icons.filled.CurrencyBitcoin import androidx.compose.material.icons.outlined.ArrowForwardIos import androidx.compose.material.icons.outlined.Bolt import androidx.compose.runtime.Composable +import androidx.compose.runtime.DisposableEffect import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue import androidx.compose.runtime.livedata.observeAsState @@ -62,6 +63,7 @@ import com.vitorpamplona.amethyst.R import com.vitorpamplona.amethyst.model.Account import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.model.User +import com.vitorpamplona.amethyst.service.NostrSearchEventOrUserDataSource import com.vitorpamplona.amethyst.ui.components.* import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel import com.vitorpamplona.amethyst.ui.screen.loggedIn.TextSpinner @@ -95,6 +97,12 @@ fun NewPostView(onClose: () -> Unit, baseReplyTo: Note? = null, quote: Note? = n } } + DisposableEffect(Unit) { + onDispose { + NostrSearchEventOrUserDataSource.clear() + } + } + Dialog( onDismissRequest = { onClose() }, properties = DialogProperties( diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewPostViewModel.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewPostViewModel.kt index a74a15d7a..69f207596 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewPostViewModel.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewPostViewModel.kt @@ -14,6 +14,7 @@ import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import com.vitorpamplona.amethyst.model.* import com.vitorpamplona.amethyst.service.FileHeader +import com.vitorpamplona.amethyst.service.NostrSearchEventOrUserDataSource import com.vitorpamplona.amethyst.service.model.PrivateDmEvent import com.vitorpamplona.amethyst.service.model.TextNoteEvent import com.vitorpamplona.amethyst.ui.components.isValidURL @@ -188,6 +189,8 @@ open class NewPostViewModel : ViewModel() { userSuggestions = emptyList() userSuggestionAnchor = null userSuggestionsMainMessage = null + + NostrSearchEventOrUserDataSource.clear() } open fun findUrlInMessage(): String? { @@ -211,8 +214,10 @@ open class NewPostViewModel : ViewModel() { userSuggestionAnchor = it.selection userSuggestionsMainMessage = true if (lastWord.startsWith("@") && lastWord.length > 2) { - userSuggestions = LocalCache.findUsersStartingWith(lastWord.removePrefix("@")) + userSuggestions = LocalCache.findUsersStartingWith(lastWord.removePrefix("@")).sortedWith(compareBy({ account?.isFollowing(it) }, { it.toBestDisplayName() })).reversed() + println("AAAA" + lastWord.removePrefix("@") + userSuggestions.size) } else { + NostrSearchEventOrUserDataSource.clear() userSuggestions = emptyList() } } @@ -225,8 +230,10 @@ open class NewPostViewModel : ViewModel() { userSuggestionAnchor = it.selection userSuggestionsMainMessage = false if (lastWord.startsWith("@") && lastWord.length > 2) { - userSuggestions = LocalCache.findUsersStartingWith(lastWord.removePrefix("@")) + NostrSearchEventOrUserDataSource.search(lastWord.removePrefix("@")) + userSuggestions = LocalCache.findUsersStartingWith(lastWord.removePrefix("@")).sortedWith(compareBy({ account?.isFollowing(it) }, { it.toBestDisplayName() })).reversed() } else { + NostrSearchEventOrUserDataSource.clear() userSuggestions = emptyList() } } diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/SearchScreen.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/SearchScreen.kt index d37de7787..320e913c0 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/SearchScreen.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/SearchScreen.kt @@ -131,6 +131,8 @@ fun SearchScreen( } class SearchBarViewModel : ViewModel() { + var account: Account? = null + var searchValue by mutableStateOf("") val searchResults = mutableStateOf>(emptyList()) val searchResultsNotes = mutableStateOf>(emptyList()) @@ -156,7 +158,7 @@ class SearchBarViewModel : ViewModel() { } hashtagResults.value = findHashtags(searchValue) - searchResults.value = LocalCache.findUsersStartingWith(searchValue) + searchResults.value = LocalCache.findUsersStartingWith(searchValue).sortedWith(compareBy({ account?.isFollowing(it) }, { it.toBestDisplayName() })).reversed() searchResultsNotes.value = LocalCache.findNotesStartingWith(searchValue).sortedWith(compareBy({ it.createdAt() }, { it.idHex })).reversed() searchResultsChannels.value = LocalCache.findChannelsStartingWith(searchValue) } @@ -185,6 +187,7 @@ class SearchBarViewModel : ViewModel() { @Composable private fun SearchBar(accountViewModel: AccountViewModel, navController: NavController) { val searchBarViewModel: SearchBarViewModel = viewModel() + searchBarViewModel.account = accountViewModel.accountLiveData.value?.account val scope = rememberCoroutineScope() val listState = rememberLazyListState() From bb9fa8e14437cfddbae5c04614c987903ffa4b85 Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Tue, 16 May 2023 18:51:08 -0400 Subject: [PATCH 10/24] Adding the missing search element --- .../com/vitorpamplona/amethyst/ui/actions/NewPostViewModel.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewPostViewModel.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewPostViewModel.kt index 69f207596..c22115261 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewPostViewModel.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewPostViewModel.kt @@ -214,8 +214,8 @@ open class NewPostViewModel : ViewModel() { userSuggestionAnchor = it.selection userSuggestionsMainMessage = true if (lastWord.startsWith("@") && lastWord.length > 2) { + NostrSearchEventOrUserDataSource.search(lastWord.removePrefix("@")) userSuggestions = LocalCache.findUsersStartingWith(lastWord.removePrefix("@")).sortedWith(compareBy({ account?.isFollowing(it) }, { it.toBestDisplayName() })).reversed() - println("AAAA" + lastWord.removePrefix("@") + userSuggestions.size) } else { NostrSearchEventOrUserDataSource.clear() userSuggestions = emptyList() From 6aadf8a883f0e2d0a4470fb4041dbaedde04d133 Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Tue, 16 May 2023 19:00:44 -0400 Subject: [PATCH 11/24] 0.49.4 --- app/build.gradle | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/build.gradle b/app/build.gradle index 36cd3b7cb..fb00fb23e 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -13,8 +13,8 @@ android { applicationId "com.vitorpamplona.amethyst" minSdk 26 targetSdk 33 - versionCode 166 - versionName "0.49.3" + versionCode 167 + versionName "0.49.4" testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" vectorDrawables { From cb421968899aee4844681ea2e2e5299aa9d5b621 Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Tue, 16 May 2023 22:41:47 -0400 Subject: [PATCH 12/24] Adds reaction watch. --- app/build.gradle | 10 +- .../service/NostrAccountDataSource.kt | 2 +- .../amethyst/ui/note/UserReactionsRow.kt | 245 ++++++++++++++++++ .../ui/screen/loggedIn/NotificationScreen.kt | 180 +++++++++++++ build.gradle | 1 + 5 files changed, 435 insertions(+), 3 deletions(-) create mode 100644 app/src/main/java/com/vitorpamplona/amethyst/ui/note/UserReactionsRow.kt diff --git a/app/build.gradle b/app/build.gradle index fb00fb23e..0d4408282 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -13,8 +13,8 @@ android { applicationId "com.vitorpamplona.amethyst" minSdk 26 targetSdk 33 - versionCode 167 - versionName "0.49.4" + versionCode 168 + versionName "0.50.0" testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" vectorDrawables { @@ -177,6 +177,12 @@ dependencies { playImplementation platform('com.google.firebase:firebase-bom:32.0.0') playImplementation 'com.google.firebase:firebase-messaging-ktx' + // Charts + implementation "com.patrykandpatrick.vico:core:${vico_version}" + implementation "com.patrykandpatrick.vico:compose:${vico_version}" + implementation "com.patrykandpatrick.vico:views:${vico_version}" + implementation "com.patrykandpatrick.vico:compose-m2:${vico_version}" + // Automatic memory leak detection debugImplementation 'com.squareup.leakcanary:leakcanary-android:2.10' diff --git a/app/src/main/java/com/vitorpamplona/amethyst/service/NostrAccountDataSource.kt b/app/src/main/java/com/vitorpamplona/amethyst/service/NostrAccountDataSource.kt index aba906e2a..8a2ceecee 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/service/NostrAccountDataSource.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/service/NostrAccountDataSource.kt @@ -95,7 +95,7 @@ object NostrAccountDataSource : NostrDataSource("AccountData") { BadgeAwardEvent.kind ), tags = mapOf("p" to listOf(account.userProfile().pubkeyHex)), - limit = 400, + limit = 4000, since = latestEOSEs.users[account.userProfile()]?.followList?.get(account.defaultNotificationFollowList)?.relayList ) ) 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 new file mode 100644 index 000000000..5203edd1a --- /dev/null +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/UserReactionsRow.kt @@ -0,0 +1,245 @@ +package com.vitorpamplona.amethyst.ui.note + +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.width +import androidx.compose.material.Icon +import androidx.compose.material.Text +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Bolt +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment.Companion.CenterVertically +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import androidx.navigation.NavController +import com.vitorpamplona.amethyst.R +import com.vitorpamplona.amethyst.model.LocalCache +import com.vitorpamplona.amethyst.model.User +import com.vitorpamplona.amethyst.service.model.LnZapEvent +import com.vitorpamplona.amethyst.service.model.ReactionEvent +import com.vitorpamplona.amethyst.service.model.RepostEvent +import com.vitorpamplona.amethyst.service.model.TextNoteEvent +import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel +import com.vitorpamplona.amethyst.ui.theme.BitcoinOrange +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.Job +import kotlinx.coroutines.launch +import java.math.BigDecimal +import java.time.Instant +import java.time.LocalDateTime +import java.time.ZoneId +import java.time.format.DateTimeFormatter + +@Composable +fun UserReactionsRow(model: UserReactionsViewModel, accountViewModel: AccountViewModel, navController: NavController, onClick: () -> Unit) { + Row(verticalAlignment = CenterVertically, modifier = Modifier.clickable(onClick = onClick).padding(10.dp)) { + Text( + text = "Today", + fontWeight = FontWeight.Bold, + fontSize = 18.sp, + modifier = Modifier.width(65.dp) + ) + + Row(verticalAlignment = CenterVertically, modifier = Modifier.weight(1f)) { + UserReplyReaction(model.replies[model.today]) + } + + Row(verticalAlignment = CenterVertically, modifier = Modifier.weight(1f)) { + UserBoostReaction(model.boosts[model.today]) + } + + Row(verticalAlignment = CenterVertically, modifier = Modifier.weight(1f)) { + UserLikeReaction(model.replies[model.today]) + } + + Row(verticalAlignment = CenterVertically, modifier = Modifier.weight(1f)) { + UserZapReaction(model.zaps[model.today]) + } + } +} + +class UserReactionsViewModel : ViewModel() { + var user: User? = null + + var reactions by mutableStateOf>(emptyMap()) + var boosts by mutableStateOf>(emptyMap()) + var zaps by mutableStateOf>(emptyMap()) + var replies by mutableStateOf>(emptyMap()) + + val sdf = DateTimeFormatter.ofPattern("yyyy-MM-dd") // SimpleDateFormat() + val today = sdf.format(LocalDateTime.now()) + + fun load(baseUser: User) { + user = baseUser + reactions = emptyMap() + boosts = emptyMap() + zaps = emptyMap() + replies = emptyMap() + } + + fun refresh() { + val scope = CoroutineScope(Job() + Dispatchers.Default) + scope.launch { + refreshSuspended() + } + } + + fun formatDate(createAt: Long): String { + return sdf.format( + Instant.ofEpochSecond(createAt) + .atZone(ZoneId.systemDefault()) + .toLocalDateTime() + ) + } + + fun refreshSuspended() { + val currentUser = user?.pubkeyHex ?: return + + val reactions = mutableMapOf() + val boosts = mutableMapOf() + val zaps = mutableMapOf() + val replies = mutableMapOf() + + LocalCache.notes.values.forEach { + val noteEvent = it.event + if (noteEvent is ReactionEvent) { + if (noteEvent.isTaggedUser(currentUser)) { + val netDate = formatDate(noteEvent.createdAt) + reactions[netDate] = (reactions[netDate] ?: 0) + 1 + } + } + if (noteEvent is RepostEvent) { + if (noteEvent.isTaggedUser(currentUser)) { + val netDate = formatDate(noteEvent.createdAt) + boosts[netDate] = (boosts[netDate] ?: 0) + 1 + } + } + if (noteEvent is LnZapEvent) { + if (noteEvent.isTaggedUser(currentUser)) { + val netDate = formatDate(noteEvent.createdAt) + zaps[netDate] = (zaps[netDate] ?: BigDecimal.ZERO) + (noteEvent.amount ?: BigDecimal.ZERO) + } + } + if (noteEvent is TextNoteEvent) { + if (noteEvent.isTaggedUser(currentUser)) { + val netDate = formatDate(noteEvent.createdAt) + replies[netDate] = (replies[netDate] ?: 0) + 1 + } + } + } + + this.reactions = reactions + this.replies = replies + this.zaps = zaps + this.boosts = boosts + } + + var collectorJob: Job? = null + + init { + collectorJob = viewModelScope.launch(Dispatchers.IO) { + LocalCache.live.newEventBundles.collect { newNotes -> + refresh() + } + } + } + + override fun onCleared() { + collectorJob?.cancel() + super.onCleared() + } +} + +@Composable +fun UserReplyReaction( + replyCount: Int? +) { + Icon( + painter = painterResource(R.drawable.ic_comment), + null, + modifier = Modifier.size(20.dp), + tint = Color.Cyan + ) + + Spacer(modifier = Modifier.width(10.dp)) + + Text( + showCount(replyCount), + fontWeight = FontWeight.Bold, + fontSize = 18.sp + ) +} + +@Composable +fun UserBoostReaction( + boostCount: Int? +) { + Icon( + painter = painterResource(R.drawable.ic_retweeted), + null, + modifier = Modifier.size(20.dp), + tint = Color.Unspecified + ) + + Spacer(modifier = Modifier.width(10.dp)) + + Text( + showCount(boostCount), + fontWeight = FontWeight.Bold, + fontSize = 18.sp + ) +} + +@Composable +fun UserLikeReaction( + likeCount: Int? +) { + Icon( + painter = painterResource(R.drawable.ic_liked), + null, + modifier = Modifier.size(20.dp), + tint = Color.Unspecified + ) + + Spacer(modifier = Modifier.width(10.dp)) + + Text( + showCount(likeCount), + fontWeight = FontWeight.Bold, + fontSize = 18.sp + ) +} + +@Composable +fun UserZapReaction( + amount: BigDecimal? +) { + Icon( + imageVector = Icons.Default.Bolt, + contentDescription = stringResource(R.string.zaps), + modifier = Modifier.size(20.dp), + tint = BitcoinOrange + ) + + Spacer(modifier = Modifier.width(8.dp)) + + Text( + showAmount(amount), + fontWeight = FontWeight.Bold, + fontSize = 18.sp + ) +} 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 9046a1569..aab31e69e 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 @@ -1,25 +1,64 @@ package com.vitorpamplona.amethyst.ui.screen.loggedIn +import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.fillMaxHeight import androidx.compose.foundation.layout.padding +import androidx.compose.material.Divider import androidx.compose.runtime.Composable import androidx.compose.runtime.DisposableEffect import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue import androidx.compose.runtime.livedata.observeAsState +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Brush +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.toArgb import androidx.compose.ui.platform.LocalLifecycleOwner import androidx.compose.ui.unit.dp import androidx.lifecycle.Lifecycle import androidx.lifecycle.LifecycleEventObserver +import androidx.lifecycle.viewmodel.compose.viewModel import androidx.navigation.NavController +import com.patrykandpatrick.vico.compose.axis.horizontal.bottomAxis +import com.patrykandpatrick.vico.compose.axis.vertical.endAxis +import com.patrykandpatrick.vico.compose.axis.vertical.startAxis +import com.patrykandpatrick.vico.compose.chart.Chart +import com.patrykandpatrick.vico.compose.chart.line.lineChart +import com.patrykandpatrick.vico.compose.component.shape.shader.fromBrush +import com.patrykandpatrick.vico.compose.style.ProvideChartStyle +import com.patrykandpatrick.vico.core.DefaultAlpha +import com.patrykandpatrick.vico.core.axis.AxisPosition +import com.patrykandpatrick.vico.core.axis.formatter.AxisValueFormatter +import com.patrykandpatrick.vico.core.chart.composed.ComposedChartEntryModel +import com.patrykandpatrick.vico.core.chart.composed.plus +import com.patrykandpatrick.vico.core.chart.line.LineChart +import com.patrykandpatrick.vico.core.chart.values.ChartValues +import com.patrykandpatrick.vico.core.component.shape.shader.DynamicShaders +import com.patrykandpatrick.vico.core.entry.ChartEntryModel +import com.patrykandpatrick.vico.core.entry.ChartEntryModelProducer +import com.patrykandpatrick.vico.core.entry.composed.plus +import com.patrykandpatrick.vico.core.entry.entryOf import com.vitorpamplona.amethyst.service.NostrAccountDataSource import com.vitorpamplona.amethyst.ui.dal.NotificationFeedFilter import com.vitorpamplona.amethyst.ui.navigation.Route +import com.vitorpamplona.amethyst.ui.note.UserReactionsRow +import com.vitorpamplona.amethyst.ui.note.UserReactionsViewModel +import com.vitorpamplona.amethyst.ui.note.showAmount +import com.vitorpamplona.amethyst.ui.note.showCount import com.vitorpamplona.amethyst.ui.screen.CardFeedView import com.vitorpamplona.amethyst.ui.screen.NotificationViewModel import com.vitorpamplona.amethyst.ui.screen.ScrollStateKeys +import com.vitorpamplona.amethyst.ui.theme.BitcoinOrange +import kotlinx.coroutines.launch +import java.time.LocalDateTime +import java.time.format.DateTimeFormatter +import kotlin.math.roundToInt @Composable fun NotificationScreen( @@ -61,6 +100,7 @@ fun NotificationScreen( Column( modifier = Modifier.padding(vertical = 0.dp) ) { + SummaryBar(accountViewModel, navController) CardFeedView( viewModel = notifFeedViewModel, accountViewModel = accountViewModel, @@ -72,3 +112,143 @@ fun NotificationScreen( } } } + +@Composable +fun SummaryBar(accountViewModel: AccountViewModel, navController: NavController) { + val accountState by accountViewModel.accountLiveData.observeAsState() + val accountUser = remember(accountState) { accountState?.account?.userProfile() } ?: return + + val model: UserReactionsViewModel = viewModel() + + var chartModel by remember(accountState) { mutableStateOf?>(null) } + var axisLabels by remember(accountState) { mutableStateOf>(emptyList()) } + + val scope = rememberCoroutineScope() + + var showChart by remember { + mutableStateOf(false) + } + + LaunchedEffect(accountUser.pubkeyHex) { + scope.launch { + model.load(accountUser) + model.refreshSuspended() + val day = 24 * 60 * 60L + val now = LocalDateTime.now() + val displayAxisFormatter = DateTimeFormatter.ofPattern("EEE") + + val dataAxisLabels = listOf(6, 5, 4, 3, 2, 1, 0).map { model.sdf.format(now.minusSeconds(day * it)) } + axisLabels = listOf(6, 5, 4, 3, 2, 1, 0).map { displayAxisFormatter.format(now.minusSeconds(day * it)) } + + val listOfCountCurves = listOf( + dataAxisLabels.mapIndexed { index, dateStr -> entryOf(index, model.replies[dateStr]?.toFloat() ?: 0f) }, + dataAxisLabels.mapIndexed { index, dateStr -> entryOf(index, model.boosts[dateStr]?.toFloat() ?: 0f) }, + dataAxisLabels.mapIndexed { index, dateStr -> entryOf(index, model.reactions[dateStr]?.toFloat() ?: 0f) } + ) + + val listOfValueCurves = listOf( + dataAxisLabels.mapIndexed { index, dateStr -> entryOf(index, model.zaps[dateStr]?.toFloat() ?: 0f) } + ) + + val chartEntryModelProducer1 = ChartEntryModelProducer(listOfCountCurves).getModel() + val chartEntryModelProducer2 = ChartEntryModelProducer(listOfValueCurves).getModel() + + chartModel = chartEntryModelProducer1.plus(chartEntryModelProducer2) + } + } + + UserReactionsRow(model, accountViewModel, navController) { + showChart = !showChart + } + + val lineChartCount = + lineChart( + lines = listOf(Color.Cyan, Color.Green, Color.Red).map { lineChartColor -> + LineChart.LineSpec( + lineColor = lineChartColor.toArgb(), + lineBackgroundShader = DynamicShaders.fromBrush( + Brush.verticalGradient( + listOf( + lineChartColor.copy(DefaultAlpha.LINE_BACKGROUND_SHADER_START), + lineChartColor.copy(DefaultAlpha.LINE_BACKGROUND_SHADER_END) + ) + ) + ) + ) + }, + targetVerticalAxisPosition = AxisPosition.Vertical.Start + ) + + val lineChartZaps = + lineChart( + lines = listOf(BitcoinOrange).map { lineChartColor -> + LineChart.LineSpec( + lineColor = lineChartColor.toArgb(), + lineBackgroundShader = DynamicShaders.fromBrush( + Brush.verticalGradient( + listOf( + lineChartColor.copy(DefaultAlpha.LINE_BACKGROUND_SHADER_START), + lineChartColor.copy(DefaultAlpha.LINE_BACKGROUND_SHADER_END) + ) + ) + ) + ) + }, + targetVerticalAxisPosition = AxisPosition.Vertical.End + ) + + chartModel?.let { + if (showChart) { + Row(modifier = Modifier.padding(vertical = 10.dp, horizontal = 20.dp).clickable(onClick = { showChart = !showChart })) { + ProvideChartStyle() { + Chart( + chart = remember(lineChartCount, lineChartZaps) { + lineChartCount.plus(lineChartZaps) + }, + model = it, + startAxis = startAxis( + valueFormatter = CountAxisValueFormatter() + ), + endAxis = endAxis( + valueFormatter = AmountAxisValueFormatter() + ), + bottomAxis = bottomAxis( + valueFormatter = LabelValueFormatter(axisLabels) + ) + ) + } + } + } + } + + Divider( + thickness = 0.25.dp + ) +} + +class LabelValueFormatter(val axisLabels: List) : AxisValueFormatter { + override fun formatValue( + value: Float, + chartValues: ChartValues + ): String { + return axisLabels[value.roundToInt()] + } +} + +class CountAxisValueFormatter() : AxisValueFormatter { + override fun formatValue( + value: Float, + chartValues: ChartValues + ): String { + return showCount(value.roundToInt()) + } +} + +class AmountAxisValueFormatter() : AxisValueFormatter { + override fun formatValue( + value: Float, + chartValues: ChartValues + ): String { + return showAmount(value.toBigDecimal()) + } +} diff --git a/build.gradle b/build.gradle index 09111b578..4d3a662e0 100644 --- a/build.gradle +++ b/build.gradle @@ -7,6 +7,7 @@ buildscript { room_version = "2.4.3" accompanist_version = '0.30.0' coil_version = '2.3.0' + vico_version = '1.6.5' } dependencies { classpath 'com.google.gms:google-services:4.3.15' From 0823f2e7b5548dd83531a3431f7e692553971910 Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Wed, 17 May 2023 09:34:21 -0400 Subject: [PATCH 13/24] Additive rendering of statistics --- .../amethyst/ui/navigation/AppNavigation.kt | 18 +- .../amethyst/ui/note/UserReactionsRow.kt | 162 ++++++++++++++---- .../ui/screen/loggedIn/NotificationScreen.kt | 81 +++------ 3 files changed, 172 insertions(+), 89 deletions(-) diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/navigation/AppNavigation.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/navigation/AppNavigation.kt index 512e6c8e2..d1005265b 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/navigation/AppNavigation.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/navigation/AppNavigation.kt @@ -8,6 +8,7 @@ import androidx.compose.runtime.getValue import androidx.compose.runtime.livedata.observeAsState import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.setValue import androidx.lifecycle.viewmodel.compose.viewModel import androidx.navigation.NavHostController @@ -18,6 +19,7 @@ import com.vitorpamplona.amethyst.ui.dal.HomeConversationsFeedFilter import com.vitorpamplona.amethyst.ui.dal.HomeNewThreadFeedFilter import com.vitorpamplona.amethyst.ui.dal.NotificationFeedFilter import com.vitorpamplona.amethyst.ui.dal.VideoFeedFilter +import com.vitorpamplona.amethyst.ui.note.UserReactionsViewModel import com.vitorpamplona.amethyst.ui.screen.NostrGlobalFeedViewModel import com.vitorpamplona.amethyst.ui.screen.NostrHomeFeedViewModel import com.vitorpamplona.amethyst.ui.screen.NostrHomeRepliesFeedViewModel @@ -37,6 +39,8 @@ import com.vitorpamplona.amethyst.ui.screen.loggedIn.ProfileScreen import com.vitorpamplona.amethyst.ui.screen.loggedIn.SearchScreen import com.vitorpamplona.amethyst.ui.screen.loggedIn.ThreadScreen import com.vitorpamplona.amethyst.ui.screen.loggedIn.VideoScreen +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch @OptIn(ExperimentalFoundationApi::class) @Composable @@ -50,7 +54,8 @@ fun AppNavigation( // Avoids creating ViewModels for performance reasons (up to 1 second delays) val accountState by accountViewModel.accountLiveData.observeAsState() - val account = accountState?.account ?: return + val account = remember(accountState) { accountState?.account } ?: return + val accountHex = remember(accountState) { accountState?.account?.userProfile()?.pubkeyHex } HomeNewThreadFeedFilter.account = account HomeConversationsFeedFilter.account = account @@ -67,6 +72,16 @@ fun AppNavigation( NotificationFeedFilter.account = account val notifFeedViewModel: NotificationViewModel = viewModel() + val userReactionsStatsModel: UserReactionsViewModel = viewModel() + val scope = rememberCoroutineScope() + + LaunchedEffect(accountHex) { + scope.launch(Dispatchers.IO) { + userReactionsStatsModel.load(account.userProfile()) + userReactionsStatsModel.initializeSuspend() + } + } + NavHost(navController, startDestination = Route.Home.route) { Route.Video.let { route -> composable(route.route, route.arguments, content = { @@ -135,6 +150,7 @@ fun AppNavigation( NotificationScreen( notifFeedViewModel = notifFeedViewModel, + userReactionsStatsModel = userReactionsStatsModel, accountViewModel = accountViewModel, navController = navController, scrollToTop = scrollToTop 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 5203edd1a..8eb5f573f 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 @@ -25,8 +25,15 @@ import androidx.compose.ui.unit.sp import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import androidx.navigation.NavController +import com.patrykandpatrick.vico.core.chart.composed.ComposedChartEntryModel +import com.patrykandpatrick.vico.core.entry.ChartEntryModel +import com.patrykandpatrick.vico.core.entry.ChartEntryModelProducer +import com.patrykandpatrick.vico.core.entry.composed.plus +import com.patrykandpatrick.vico.core.entry.entryOf import com.vitorpamplona.amethyst.R +import com.vitorpamplona.amethyst.model.HexKey import com.vitorpamplona.amethyst.model.LocalCache +import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.model.User import com.vitorpamplona.amethyst.service.model.LnZapEvent import com.vitorpamplona.amethyst.service.model.ReactionEvent @@ -34,7 +41,6 @@ import com.vitorpamplona.amethyst.service.model.RepostEvent import com.vitorpamplona.amethyst.service.model.TextNoteEvent import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel import com.vitorpamplona.amethyst.ui.theme.BitcoinOrange -import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Job import kotlinx.coroutines.launch @@ -63,7 +69,7 @@ fun UserReactionsRow(model: UserReactionsViewModel, accountViewModel: AccountVie } Row(verticalAlignment = CenterVertically, modifier = Modifier.weight(1f)) { - UserLikeReaction(model.replies[model.today]) + UserLikeReaction(model.reactions[model.today]) } Row(verticalAlignment = CenterVertically, modifier = Modifier.weight(1f)) { @@ -80,22 +86,21 @@ class UserReactionsViewModel : ViewModel() { var zaps by mutableStateOf>(emptyMap()) var replies by mutableStateOf>(emptyMap()) + var takenIntoAccount = setOf() + val sdf = DateTimeFormatter.ofPattern("yyyy-MM-dd") // SimpleDateFormat() val today = sdf.format(LocalDateTime.now()) + var chartModel by mutableStateOf?>(null) + var axisLabels by mutableStateOf>(emptyList()) + fun load(baseUser: User) { user = baseUser reactions = emptyMap() boosts = emptyMap() zaps = emptyMap() replies = emptyMap() - } - - fun refresh() { - val scope = CoroutineScope(Job() + Dispatchers.Default) - scope.launch { - refreshSuspended() - } + takenIntoAccount = emptySet() } fun formatDate(createAt: Long): String { @@ -106,46 +111,133 @@ class UserReactionsViewModel : ViewModel() { ) } - fun refreshSuspended() { + fun initializeSuspend() { val currentUser = user?.pubkeyHex ?: return val reactions = mutableMapOf() val boosts = mutableMapOf() val zaps = mutableMapOf() val replies = mutableMapOf() + val takenIntoAccount = mutableSetOf() LocalCache.notes.values.forEach { val noteEvent = it.event - if (noteEvent is ReactionEvent) { - if (noteEvent.isTaggedUser(currentUser)) { - val netDate = formatDate(noteEvent.createdAt) - reactions[netDate] = (reactions[netDate] ?: 0) + 1 - } - } - if (noteEvent is RepostEvent) { - if (noteEvent.isTaggedUser(currentUser)) { - val netDate = formatDate(noteEvent.createdAt) - boosts[netDate] = (boosts[netDate] ?: 0) + 1 - } - } - if (noteEvent is LnZapEvent) { - if (noteEvent.isTaggedUser(currentUser)) { - val netDate = formatDate(noteEvent.createdAt) - zaps[netDate] = (zaps[netDate] ?: BigDecimal.ZERO) + (noteEvent.amount ?: BigDecimal.ZERO) - } - } - if (noteEvent is TextNoteEvent) { - if (noteEvent.isTaggedUser(currentUser)) { - val netDate = formatDate(noteEvent.createdAt) - replies[netDate] = (replies[netDate] ?: 0) + 1 + if (noteEvent != null && !takenIntoAccount.contains(noteEvent.id())) { + if (noteEvent is ReactionEvent) { + if (noteEvent.isTaggedUser(currentUser) && noteEvent.pubKey != currentUser) { + val netDate = formatDate(noteEvent.createdAt) + reactions[netDate] = (reactions[netDate] ?: 0) + 1 + takenIntoAccount.add(noteEvent.id()) + } + } else if (noteEvent is RepostEvent) { + if (noteEvent.isTaggedUser(currentUser) && noteEvent.pubKey != currentUser) { + val netDate = formatDate(noteEvent.createdAt) + boosts[netDate] = (boosts[netDate] ?: 0) + 1 + takenIntoAccount.add(noteEvent.id()) + } + } else if (noteEvent is LnZapEvent) { + if (noteEvent.isTaggedUser(currentUser) && noteEvent.pubKey != currentUser) { + val netDate = formatDate(noteEvent.createdAt) + zaps[netDate] = (zaps[netDate] ?: BigDecimal.ZERO) + (noteEvent.amount ?: BigDecimal.ZERO) + takenIntoAccount.add(noteEvent.id()) + } + } else if (noteEvent is TextNoteEvent) { + if (noteEvent.isTaggedUser(currentUser) && noteEvent.pubKey != currentUser) { + val netDate = formatDate(noteEvent.createdAt) + replies[netDate] = (replies[netDate] ?: 0) + 1 + takenIntoAccount.add(noteEvent.id()) + } } } } + this.takenIntoAccount = takenIntoAccount this.reactions = reactions this.replies = replies this.zaps = zaps this.boosts = boosts + + refreshChartModel() + } + + fun addToStatsSuspend(newNotes: Set) { + val currentUser = user?.pubkeyHex ?: return + + val reactions = this.reactions.toMutableMap() + val boosts = this.boosts.toMutableMap() + val zaps = this.zaps.toMutableMap() + val replies = this.replies.toMutableMap() + val takenIntoAccount = this.takenIntoAccount.toMutableSet() + var hasNewElements = false + + newNotes.forEach { + val noteEvent = it.event + if (noteEvent != null && !takenIntoAccount.contains(noteEvent.id())) { + if (noteEvent is ReactionEvent) { + if (noteEvent.isTaggedUser(currentUser) && noteEvent.pubKey != currentUser) { + val netDate = formatDate(noteEvent.createdAt) + reactions[netDate] = (reactions[netDate] ?: 0) + 1 + takenIntoAccount.add(noteEvent.id()) + hasNewElements = true + } + } else if (noteEvent is RepostEvent) { + if (noteEvent.isTaggedUser(currentUser) && noteEvent.pubKey != currentUser) { + val netDate = formatDate(noteEvent.createdAt) + boosts[netDate] = (boosts[netDate] ?: 0) + 1 + takenIntoAccount.add(noteEvent.id()) + hasNewElements = true + } + } else if (noteEvent is LnZapEvent) { + if (noteEvent.isTaggedUser(currentUser) && noteEvent.pubKey != currentUser) { + val netDate = formatDate(noteEvent.createdAt) + zaps[netDate] = (zaps[netDate] ?: BigDecimal.ZERO) + (noteEvent.amount ?: BigDecimal.ZERO) + takenIntoAccount.add(noteEvent.id()) + hasNewElements = true + } + } else if (noteEvent is TextNoteEvent) { + if (noteEvent.isTaggedUser(currentUser) && noteEvent.pubKey != currentUser) { + val netDate = formatDate(noteEvent.createdAt) + replies[netDate] = (replies[netDate] ?: 0) + 1 + takenIntoAccount.add(noteEvent.id()) + hasNewElements = true + } + } + } + } + + if (hasNewElements) { + this.takenIntoAccount = takenIntoAccount + this.reactions = reactions + this.replies = replies + this.zaps = zaps + this.boosts = boosts + + refreshChartModel() + } + } + + private fun refreshChartModel() { + val day = 24 * 60 * 60L + val now = LocalDateTime.now() + val displayAxisFormatter = DateTimeFormatter.ofPattern("EEE") + + val dataAxisLabels = listOf(6, 5, 4, 3, 2, 1, 0).map { sdf.format(now.minusSeconds(day * it)) } + + val listOfCountCurves = listOf( + dataAxisLabels.mapIndexed { index, dateStr -> entryOf(index, replies[dateStr]?.toFloat() ?: 0f) }, + dataAxisLabels.mapIndexed { index, dateStr -> entryOf(index, boosts[dateStr]?.toFloat() ?: 0f) }, + dataAxisLabels.mapIndexed { index, dateStr -> entryOf(index, reactions[dateStr]?.toFloat() ?: 0f) } + ) + + val listOfValueCurves = listOf( + dataAxisLabels.mapIndexed { index, dateStr -> entryOf(index, zaps[dateStr]?.toFloat() ?: 0f) } + ) + + val chartEntryModelProducer1 = ChartEntryModelProducer(listOfCountCurves).getModel() + val chartEntryModelProducer2 = ChartEntryModelProducer(listOfValueCurves).getModel() + + this.axisLabels = listOf(6, 5, 4, 3, 2, 1, 0).map { displayAxisFormatter.format(now.minusSeconds(day * it)) } + this.chartModel = chartEntryModelProducer1.plus(chartEntryModelProducer2) } var collectorJob: Job? = null @@ -153,7 +245,7 @@ class UserReactionsViewModel : ViewModel() { init { collectorJob = viewModelScope.launch(Dispatchers.IO) { LocalCache.live.newEventBundles.collect { newNotes -> - refresh() + addToStatsSuspend(newNotes) } } } @@ -191,7 +283,7 @@ fun UserBoostReaction( Icon( painter = painterResource(R.drawable.ic_retweeted), null, - modifier = Modifier.size(20.dp), + modifier = Modifier.size(24.dp), tint = Color.Unspecified ) @@ -231,7 +323,7 @@ fun UserZapReaction( Icon( imageVector = Icons.Default.Bolt, contentDescription = stringResource(R.string.zaps), - modifier = Modifier.size(20.dp), + modifier = Modifier.size(24.dp), tint = BitcoinOrange ) 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 aab31e69e..298955136 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 @@ -13,7 +13,6 @@ import androidx.compose.runtime.getValue import androidx.compose.runtime.livedata.observeAsState import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember -import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Brush @@ -35,34 +34,32 @@ import com.patrykandpatrick.vico.compose.style.ProvideChartStyle import com.patrykandpatrick.vico.core.DefaultAlpha import com.patrykandpatrick.vico.core.axis.AxisPosition import com.patrykandpatrick.vico.core.axis.formatter.AxisValueFormatter -import com.patrykandpatrick.vico.core.chart.composed.ComposedChartEntryModel import com.patrykandpatrick.vico.core.chart.composed.plus import com.patrykandpatrick.vico.core.chart.line.LineChart import com.patrykandpatrick.vico.core.chart.values.ChartValues import com.patrykandpatrick.vico.core.component.shape.shader.DynamicShaders -import com.patrykandpatrick.vico.core.entry.ChartEntryModel -import com.patrykandpatrick.vico.core.entry.ChartEntryModelProducer import com.patrykandpatrick.vico.core.entry.composed.plus -import com.patrykandpatrick.vico.core.entry.entryOf import com.vitorpamplona.amethyst.service.NostrAccountDataSource import com.vitorpamplona.amethyst.ui.dal.NotificationFeedFilter import com.vitorpamplona.amethyst.ui.navigation.Route +import com.vitorpamplona.amethyst.ui.note.OneGiga +import com.vitorpamplona.amethyst.ui.note.OneKilo +import com.vitorpamplona.amethyst.ui.note.OneMega import com.vitorpamplona.amethyst.ui.note.UserReactionsRow import com.vitorpamplona.amethyst.ui.note.UserReactionsViewModel -import com.vitorpamplona.amethyst.ui.note.showAmount import com.vitorpamplona.amethyst.ui.note.showCount import com.vitorpamplona.amethyst.ui.screen.CardFeedView import com.vitorpamplona.amethyst.ui.screen.NotificationViewModel import com.vitorpamplona.amethyst.ui.screen.ScrollStateKeys import com.vitorpamplona.amethyst.ui.theme.BitcoinOrange -import kotlinx.coroutines.launch -import java.time.LocalDateTime -import java.time.format.DateTimeFormatter +import java.math.BigDecimal +import java.math.RoundingMode import kotlin.math.roundToInt @Composable fun NotificationScreen( notifFeedViewModel: NotificationViewModel, + userReactionsStatsModel: UserReactionsViewModel, accountViewModel: AccountViewModel, navController: NavController, scrollToTop: Boolean = false @@ -100,7 +97,7 @@ fun NotificationScreen( Column( modifier = Modifier.padding(vertical = 0.dp) ) { - SummaryBar(accountViewModel, navController) + SummaryBar(userReactionsStatsModel, accountViewModel, navController) CardFeedView( viewModel = notifFeedViewModel, accountViewModel = accountViewModel, @@ -114,49 +111,11 @@ fun NotificationScreen( } @Composable -fun SummaryBar(accountViewModel: AccountViewModel, navController: NavController) { - val accountState by accountViewModel.accountLiveData.observeAsState() - val accountUser = remember(accountState) { accountState?.account?.userProfile() } ?: return - - val model: UserReactionsViewModel = viewModel() - - var chartModel by remember(accountState) { mutableStateOf?>(null) } - var axisLabels by remember(accountState) { mutableStateOf>(emptyList()) } - - val scope = rememberCoroutineScope() - +fun SummaryBar(model: UserReactionsViewModel, accountViewModel: AccountViewModel, navController: NavController) { var showChart by remember { mutableStateOf(false) } - LaunchedEffect(accountUser.pubkeyHex) { - scope.launch { - model.load(accountUser) - model.refreshSuspended() - val day = 24 * 60 * 60L - val now = LocalDateTime.now() - val displayAxisFormatter = DateTimeFormatter.ofPattern("EEE") - - val dataAxisLabels = listOf(6, 5, 4, 3, 2, 1, 0).map { model.sdf.format(now.minusSeconds(day * it)) } - axisLabels = listOf(6, 5, 4, 3, 2, 1, 0).map { displayAxisFormatter.format(now.minusSeconds(day * it)) } - - val listOfCountCurves = listOf( - dataAxisLabels.mapIndexed { index, dateStr -> entryOf(index, model.replies[dateStr]?.toFloat() ?: 0f) }, - dataAxisLabels.mapIndexed { index, dateStr -> entryOf(index, model.boosts[dateStr]?.toFloat() ?: 0f) }, - dataAxisLabels.mapIndexed { index, dateStr -> entryOf(index, model.reactions[dateStr]?.toFloat() ?: 0f) } - ) - - val listOfValueCurves = listOf( - dataAxisLabels.mapIndexed { index, dateStr -> entryOf(index, model.zaps[dateStr]?.toFloat() ?: 0f) } - ) - - val chartEntryModelProducer1 = ChartEntryModelProducer(listOfCountCurves).getModel() - val chartEntryModelProducer2 = ChartEntryModelProducer(listOfValueCurves).getModel() - - chartModel = chartEntryModelProducer1.plus(chartEntryModelProducer2) - } - } - UserReactionsRow(model, accountViewModel, navController) { showChart = !showChart } @@ -197,9 +156,13 @@ fun SummaryBar(accountViewModel: AccountViewModel, navController: NavController) targetVerticalAxisPosition = AxisPosition.Vertical.End ) - chartModel?.let { + model.chartModel?.let { if (showChart) { - Row(modifier = Modifier.padding(vertical = 10.dp, horizontal = 20.dp).clickable(onClick = { showChart = !showChart })) { + Row( + modifier = Modifier + .padding(vertical = 10.dp, horizontal = 20.dp) + .clickable(onClick = { showChart = !showChart }) + ) { ProvideChartStyle() { Chart( chart = remember(lineChartCount, lineChartZaps) { @@ -213,7 +176,7 @@ fun SummaryBar(accountViewModel: AccountViewModel, navController: NavController) valueFormatter = AmountAxisValueFormatter() ), bottomAxis = bottomAxis( - valueFormatter = LabelValueFormatter(axisLabels) + valueFormatter = LabelValueFormatter(model.axisLabels) ) ) } @@ -249,6 +212,18 @@ class AmountAxisValueFormatter() : AxisValueFormatter value: Float, chartValues: ChartValues ): String { - return showAmount(value.toBigDecimal()) + return showAmountAxis(value.toBigDecimal()) + } +} + +fun showAmountAxis(amount: BigDecimal?): String { + if (amount == null) return "" + if (amount.abs() < BigDecimal(0.01)) return "" + + return when { + amount >= OneGiga -> "%.0fG".format(amount.div(OneGiga).setScale(0, RoundingMode.HALF_UP)) + amount >= OneMega -> "%.0fM".format(amount.div(OneMega).setScale(0, RoundingMode.HALF_UP)) + amount >= OneKilo -> "%.0fk".format(amount.div(OneKilo).setScale(0, RoundingMode.HALF_UP)) + else -> "%.0f".format(amount) } } From 95a21cc08c161b138915c652d324402f404526af Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Wed, 17 May 2023 09:49:31 -0400 Subject: [PATCH 14/24] Adds Navigation markers to the top panel and reaction watch --- .../amethyst/ui/navigation/AppTopBar.kt | 13 ++++++++- .../amethyst/ui/note/UserReactionsRow.kt | 28 ++++++++++++++----- app/src/main/res/values/strings.xml | 2 ++ 3 files changed, 35 insertions(+), 8 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 1ebe2c432..cecad69ec 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 @@ -7,6 +7,7 @@ import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxHeight import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height @@ -23,6 +24,7 @@ import androidx.compose.material.Text import androidx.compose.material.TopAppBar import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.ArrowBack +import androidx.compose.material.icons.filled.ExpandMore import androidx.compose.runtime.Composable import androidx.compose.runtime.derivedStateOf import androidx.compose.runtime.getValue @@ -337,7 +339,16 @@ fun SimpleTextSpinner( modifier = modifier, contentAlignment = Alignment.Center ) { - Text(placeholder) + Row(verticalAlignment = Alignment.CenterVertically) { + Spacer(modifier = Modifier.size(20.dp)) + Text(placeholder) + Icon( + imageVector = Icons.Default.ExpandMore, + null, + modifier = Modifier.size(20.dp), + tint = MaterialTheme.colors.onSurface.copy(alpha = 0.32f) + ) + } Box( modifier = Modifier .matchParentSize() 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 8eb5f573f..484fbdde2 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 @@ -7,9 +7,11 @@ import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.width import androidx.compose.material.Icon +import androidx.compose.material.MaterialTheme import androidx.compose.material.Text import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.Bolt +import androidx.compose.material.icons.filled.ExpandMore import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf @@ -52,13 +54,25 @@ import java.time.format.DateTimeFormatter @Composable fun UserReactionsRow(model: UserReactionsViewModel, accountViewModel: AccountViewModel, navController: NavController, onClick: () -> Unit) { - Row(verticalAlignment = CenterVertically, modifier = Modifier.clickable(onClick = onClick).padding(10.dp)) { - Text( - text = "Today", - fontWeight = FontWeight.Bold, - fontSize = 18.sp, - modifier = Modifier.width(65.dp) - ) + Row( + verticalAlignment = CenterVertically, + modifier = Modifier + .clickable(onClick = onClick) + .padding(10.dp) + ) { + Row(verticalAlignment = CenterVertically, modifier = Modifier.width(68.dp)) { + Text( + text = stringResource(id = R.string.today), + fontWeight = FontWeight.Bold + ) + + Icon( + imageVector = Icons.Default.ExpandMore, + null, + modifier = Modifier.size(20.dp), + tint = MaterialTheme.colors.onSurface.copy(alpha = 0.32f) + ) + } Row(verticalAlignment = CenterVertically, modifier = Modifier.weight(1f)) { UserReplyReaction(model.replies[model.today]) diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 9de6c008b..b10e09631 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -402,4 +402,6 @@ npub, nevent or hex Create Join + + Today From a8e650a38dcc9fe8ea2d7f6b8807e37af373a719 Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Wed, 17 May 2023 09:50:43 -0400 Subject: [PATCH 15/24] v0.50.1 --- app/build.gradle | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/build.gradle b/app/build.gradle index 0d4408282..ed1e20fde 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -13,8 +13,8 @@ android { applicationId "com.vitorpamplona.amethyst" minSdk 26 targetSdk 33 - versionCode 168 - versionName "0.50.0" + versionCode 169 + versionName "0.50.1" testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" vectorDrawables { From 117446688b0f9d4045a216fad1feaa51c8142b7a Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Wed, 17 May 2023 11:20:03 -0400 Subject: [PATCH 16/24] Fixes jumping chat screens on hidden posts. --- .../ui/note/ChatroomMessageCompose.kt | 20 +++++++++---------- .../amethyst/ui/note/NoteCompose.kt | 20 +++++++++---------- 2 files changed, 20 insertions(+), 20 deletions(-) diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/note/ChatroomMessageCompose.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/ChatroomMessageCompose.kt index bba847864..57ed43b3a 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/note/ChatroomMessageCompose.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/ChatroomMessageCompose.kt @@ -110,6 +110,8 @@ fun ChatroomMessageCompose( note?.let { NoteQuickActionMenu(it, popupExpanded, { popupExpanded = false }, accountViewModel) } + } else if (account.isHidden(noteForReports.author!!)) { + // Does nothing } else { var showHiddenNote by remember { mutableStateOf(false) } var isAcceptableAndCanPreview by remember { mutableStateOf(Pair(true, true)) } @@ -131,16 +133,14 @@ fun ChatroomMessageCompose( } if (!isAcceptableAndCanPreview.first && !showHiddenNote) { - if (!account.isHidden(noteForReports.author!!)) { - HiddenNote( - account.getRelevantReports(noteForReports), - account.userProfile(), - Modifier, - innerQuote, - navController, - onClick = { showHiddenNote = true } - ) - } + HiddenNote( + account.getRelevantReports(noteForReports), + account.userProfile(), + Modifier, + innerQuote, + navController, + onClick = { showHiddenNote = true } + ) } else { val backgroundBubbleColor: Color val alignment: Arrangement.Horizontal 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 bfdcfb7dc..bff36a298 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 @@ -220,6 +220,8 @@ fun NoteComposeInner( note?.let { NoteQuickActionMenu(it, popupExpanded, { popupExpanded = false }, accountViewModel) } + } else if (account.isHidden(noteForReports.author!!)) { + // Does nothing } else { var showHiddenNote by remember { mutableStateOf(false) } var isAcceptableAndCanPreview by remember { mutableStateOf(Pair(true, true)) } @@ -241,16 +243,14 @@ fun NoteComposeInner( } if (!isAcceptableAndCanPreview.first && !showHiddenNote) { - if (!account.isHidden(noteForReports.author!!)) { - HiddenNote( - account.getRelevantReports(noteForReports), - account.userProfile(), - modifier, - isBoostedNote, - navController, - onClick = { showHiddenNote = true } - ) - } + HiddenNote( + account.getRelevantReports(noteForReports), + account.userProfile(), + modifier, + isBoostedNote, + navController, + onClick = { showHiddenNote = true } + ) } else if ((noteEvent is ChannelCreateEvent || noteEvent is ChannelMetadataEvent) && baseChannel != null) { ChannelHeader(baseChannel = baseChannel, account = account, navController = navController) } else if (noteEvent is BadgeDefinitionEvent) { From d2390208955658345c5b642f24e62a988eca5abe Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Wed, 17 May 2023 11:20:14 -0400 Subject: [PATCH 17/24] v0.50.2 --- app/build.gradle | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/build.gradle b/app/build.gradle index ed1e20fde..7341d633c 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -13,8 +13,8 @@ android { applicationId "com.vitorpamplona.amethyst" minSdk 26 targetSdk 33 - versionCode 169 - versionName "0.50.1" + versionCode 170 + versionName "0.50.2" testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" vectorDrawables { From c5176b8c4973be9b2527aadcbbe674c68b6f62b5 Mon Sep 17 00:00:00 2001 From: Zoltan <34719275+ZsZolee@users.noreply.github.com> Date: Wed, 17 May 2023 17:27:43 +0200 Subject: [PATCH 18/24] Replacing some missing Hungarian translation --- app/src/main/res/values-hu/strings.xml | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/app/src/main/res/values-hu/strings.xml b/app/src/main/res/values-hu/strings.xml index 2d5f6b037..86d2c42c4 100644 --- a/app/src/main/res/values-hu/strings.xml +++ b/app/src/main/res/values-hu/strings.xml @@ -284,6 +284,8 @@ "%1$s esemény keresése" Nyilvános üzenet hozzáadása + Adj hozzá egy privát üzenetet + A számlához adj hozzá egy üzenetet Köszönöm a kemény munkát! Létrehoz és Hozzáad @@ -334,6 +336,13 @@ Saját csomópontjaid (NIP-95) A fájlokat a csomópontokra töltik fel és ott tárolják. Rögzített URL-től mentesek (harmadik féltől való függőség). Győződj meg róla, hogy legalább egy NIP-95 csomópont a csomópontlistában szerepel + Tor/Orbot beállítása + Csatlakozás a saját Orbot beállításod alapján + + Lecsatlakozás a saját Orbot/Tor hálózatról? + Az adataid azonnal a hagyományos hálózaton lesz továbbítva + Igen + Nem Követek Lista @@ -374,4 +383,6 @@ npub, nevent vagy hex Létrehoz Csatlakozás + + Ma From d6c8651f0d46ed205e48788bd4e26daeaa2837dc Mon Sep 17 00:00:00 2001 From: KotlinGeek Date: Wed, 17 May 2023 17:26:31 +0100 Subject: [PATCH 19/24] Add lowercase variant of hashtag when creating event. --- .../com/vitorpamplona/amethyst/service/model/TextNoteEvent.kt | 1 + 1 file changed, 1 insertion(+) diff --git a/app/src/main/java/com/vitorpamplona/amethyst/service/model/TextNoteEvent.kt b/app/src/main/java/com/vitorpamplona/amethyst/service/model/TextNoteEvent.kt index b9bc6ab55..aa80ac016 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/service/model/TextNoteEvent.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/service/model/TextNoteEvent.kt @@ -43,6 +43,7 @@ class TextNoteEvent( } findHashtags(msg).forEach { tags.add(listOf("t", it)) + tags.add(listOf("t", it.lowercase())) } extraTags?.forEach { tags.add(listOf("t", it)) From 5040350be5ff659d0682f859741061c6e5a224b8 Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Wed, 17 May 2023 13:54:20 -0400 Subject: [PATCH 20/24] Fixes Auth for NIP-42 sporadic connections. --- .../java/com/vitorpamplona/amethyst/service/relays/Client.kt | 4 ++-- .../com/vitorpamplona/amethyst/service/relays/RelayPool.kt | 4 ++++ 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/app/src/main/java/com/vitorpamplona/amethyst/service/relays/Client.kt b/app/src/main/java/com/vitorpamplona/amethyst/service/relays/Client.kt index 3fb91722f..33a02c40e 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/service/relays/Client.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/service/relays/Client.kt @@ -70,7 +70,7 @@ object Client : RelayPool.Listener { if (relay == null) { RelayPool.send(signedEvent) } else { - val useConnectedRelayIfPresent = relays.filter { it.url == relay } + val useConnectedRelayIfPresent = RelayPool.getRelays(relay) if (useConnectedRelayIfPresent.isNotEmpty()) { useConnectedRelayIfPresent.forEach { @@ -103,7 +103,7 @@ object Client : RelayPool.Listener { onConnected(relay) GlobalScope.launch(Dispatchers.IO) { - delay(10000) // waits for a reply + delay(60000) // waits for a reply relay.disconnect() RelayPool.removeRelay(relay) diff --git a/app/src/main/java/com/vitorpamplona/amethyst/service/relays/RelayPool.kt b/app/src/main/java/com/vitorpamplona/amethyst/service/relays/RelayPool.kt index 702bdc1b2..23f2c4a25 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/service/relays/RelayPool.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/service/relays/RelayPool.kt @@ -30,6 +30,10 @@ object RelayPool : Relay.Listener { return relays.firstOrNull() { it.url == url } } + fun getRelays(url: String): List { + return relays.filter { it.url == url } + } + fun loadRelays(relayList: List) { if (!relayList.isNullOrEmpty()) { relayList.forEach { addRelay(it) } From cbe9462f2809a843a887b296ff7ab05e9ed4d550 Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Wed, 17 May 2023 13:55:30 -0400 Subject: [PATCH 21/24] v0.50.3 --- app/build.gradle | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/build.gradle b/app/build.gradle index 7341d633c..d44fd5d93 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -13,8 +13,8 @@ android { applicationId "com.vitorpamplona.amethyst" minSdk 26 targetSdk 33 - versionCode 170 - versionName "0.50.2" + versionCode 171 + versionName "0.50.3" testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" vectorDrawables { From 323e71c7cbbe27a1716f3c0a20d830866b3f1f02 Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Wed, 17 May 2023 15:47:20 -0400 Subject: [PATCH 22/24] Bugfix for saving Replaceable notes in Bookmarks. --- .../vitorpamplona/amethyst/model/Account.kt | 140 +++++++++++++----- .../amethyst/ui/note/NoteCompose.kt | 4 +- 2 files changed, 103 insertions(+), 41 deletions(-) diff --git a/app/src/main/java/com/vitorpamplona/amethyst/model/Account.kt b/app/src/main/java/com/vitorpamplona/amethyst/model/Account.kt index bf66a5f43..38d71ebda 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/model/Account.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/model/Account.kt @@ -593,18 +593,33 @@ class Account( val bookmarks = userProfile().latestBookmarkList - val event = BookmarkListEvent.create( - "bookmark", - bookmarks?.taggedEvents() ?: emptyList(), - bookmarks?.taggedUsers() ?: emptyList(), - bookmarks?.taggedAddresses() ?: emptyList(), + val event = if (note is AddressableNote) { + BookmarkListEvent.create( + "bookmark", + bookmarks?.taggedEvents() ?: emptyList(), + bookmarks?.taggedUsers() ?: emptyList(), + bookmarks?.taggedAddresses() ?: emptyList(), - bookmarks?.privateTaggedEvents(privKey = loggedIn.privKey!!)?.plus(note.idHex) ?: listOf(note.idHex), - bookmarks?.privateTaggedUsers(privKey = loggedIn.privKey!!) ?: emptyList(), - bookmarks?.privateTaggedAddresses(privKey = loggedIn.privKey!!) ?: emptyList(), + bookmarks?.privateTaggedEvents(privKey = loggedIn.privKey!!) ?: emptyList(), + bookmarks?.privateTaggedUsers(privKey = loggedIn.privKey!!) ?: emptyList(), + bookmarks?.privateTaggedAddresses(privKey = loggedIn.privKey!!)?.plus(note.address) ?: listOf(note.address), - loggedIn.privKey!! - ) + loggedIn.privKey!! + ) + } else { + BookmarkListEvent.create( + "bookmark", + bookmarks?.taggedEvents() ?: emptyList(), + bookmarks?.taggedUsers() ?: emptyList(), + bookmarks?.taggedAddresses() ?: emptyList(), + + bookmarks?.privateTaggedEvents(privKey = loggedIn.privKey!!)?.plus(note.idHex) ?: listOf(note.idHex), + bookmarks?.privateTaggedUsers(privKey = loggedIn.privKey!!) ?: emptyList(), + bookmarks?.privateTaggedAddresses(privKey = loggedIn.privKey!!) ?: emptyList(), + + loggedIn.privKey!! + ) + } Client.send(event) LocalCache.consume(event) @@ -615,18 +630,33 @@ class Account( val bookmarks = userProfile().latestBookmarkList - val event = BookmarkListEvent.create( - "bookmark", - bookmarks?.taggedEvents()?.plus(note.idHex) ?: listOf(note.idHex), - bookmarks?.taggedUsers() ?: emptyList(), - bookmarks?.taggedAddresses() ?: emptyList(), + val event = if (note is AddressableNote) { + BookmarkListEvent.create( + "bookmark", + bookmarks?.taggedEvents() ?: emptyList(), + bookmarks?.taggedUsers() ?: emptyList(), + bookmarks?.taggedAddresses()?.plus(note.address) ?: listOf(note.address), - bookmarks?.privateTaggedEvents(privKey = loggedIn.privKey!!) ?: emptyList(), - bookmarks?.privateTaggedUsers(privKey = loggedIn.privKey!!) ?: emptyList(), - bookmarks?.privateTaggedAddresses(privKey = loggedIn.privKey!!) ?: emptyList(), + bookmarks?.privateTaggedEvents(privKey = loggedIn.privKey!!) ?: emptyList(), + bookmarks?.privateTaggedUsers(privKey = loggedIn.privKey!!) ?: emptyList(), + bookmarks?.privateTaggedAddresses(privKey = loggedIn.privKey!!) ?: emptyList(), - loggedIn.privKey!! - ) + loggedIn.privKey!! + ) + } else { + BookmarkListEvent.create( + "bookmark", + bookmarks?.taggedEvents()?.plus(note.idHex) ?: listOf(note.idHex), + bookmarks?.taggedUsers() ?: emptyList(), + bookmarks?.taggedAddresses() ?: emptyList(), + + bookmarks?.privateTaggedEvents(privKey = loggedIn.privKey!!) ?: emptyList(), + bookmarks?.privateTaggedUsers(privKey = loggedIn.privKey!!) ?: emptyList(), + bookmarks?.privateTaggedAddresses(privKey = loggedIn.privKey!!) ?: emptyList(), + + loggedIn.privKey!! + ) + } Client.send(event) LocalCache.consume(event) @@ -637,18 +667,33 @@ class Account( val bookmarks = userProfile().latestBookmarkList - val event = BookmarkListEvent.create( - "bookmark", - bookmarks?.taggedEvents() ?: emptyList(), - bookmarks?.taggedUsers() ?: emptyList(), - bookmarks?.taggedAddresses() ?: emptyList(), + val event = if (note is AddressableNote) { + BookmarkListEvent.create( + "bookmark", + bookmarks?.taggedEvents() ?: emptyList(), + bookmarks?.taggedUsers() ?: emptyList(), + bookmarks?.taggedAddresses() ?: emptyList(), - bookmarks?.privateTaggedEvents(privKey = loggedIn.privKey!!)?.minus(note.idHex) ?: listOf(), - bookmarks?.privateTaggedUsers(privKey = loggedIn.privKey!!) ?: emptyList(), - bookmarks?.privateTaggedAddresses(privKey = loggedIn.privKey!!) ?: emptyList(), + bookmarks?.privateTaggedEvents(privKey = loggedIn.privKey!!) ?: emptyList(), + bookmarks?.privateTaggedUsers(privKey = loggedIn.privKey!!) ?: emptyList(), + bookmarks?.privateTaggedAddresses(privKey = loggedIn.privKey!!)?.minus(note.address) ?: listOf(), - loggedIn.privKey!! - ) + loggedIn.privKey!! + ) + } else { + BookmarkListEvent.create( + "bookmark", + bookmarks?.taggedEvents() ?: emptyList(), + bookmarks?.taggedUsers() ?: emptyList(), + bookmarks?.taggedAddresses() ?: emptyList(), + + bookmarks?.privateTaggedEvents(privKey = loggedIn.privKey!!)?.minus(note.idHex) ?: listOf(), + bookmarks?.privateTaggedUsers(privKey = loggedIn.privKey!!) ?: emptyList(), + bookmarks?.privateTaggedAddresses(privKey = loggedIn.privKey!!) ?: emptyList(), + + loggedIn.privKey!! + ) + } Client.send(event) LocalCache.consume(event) @@ -665,18 +710,33 @@ class Account( val bookmarks = userProfile().latestBookmarkList - val event = BookmarkListEvent.create( - "bookmark", - bookmarks?.taggedEvents()?.minus(note.idHex), - bookmarks?.taggedUsers() ?: emptyList(), - bookmarks?.taggedAddresses() ?: emptyList(), + val event = if (note is AddressableNote) { + BookmarkListEvent.create( + "bookmark", + bookmarks?.taggedEvents() ?: emptyList(), + bookmarks?.taggedUsers() ?: emptyList(), + bookmarks?.taggedAddresses()?.minus(note.address), - bookmarks?.privateTaggedEvents(privKey = loggedIn.privKey!!) ?: emptyList(), - bookmarks?.privateTaggedUsers(privKey = loggedIn.privKey!!) ?: emptyList(), - bookmarks?.privateTaggedAddresses(privKey = loggedIn.privKey!!) ?: emptyList(), + bookmarks?.privateTaggedEvents(privKey = loggedIn.privKey!!) ?: emptyList(), + bookmarks?.privateTaggedUsers(privKey = loggedIn.privKey!!) ?: emptyList(), + bookmarks?.privateTaggedAddresses(privKey = loggedIn.privKey!!) ?: emptyList(), - loggedIn.privKey!! - ) + loggedIn.privKey!! + ) + } else { + BookmarkListEvent.create( + "bookmark", + bookmarks?.taggedEvents()?.minus(note.idHex), + bookmarks?.taggedUsers() ?: emptyList(), + bookmarks?.taggedAddresses() ?: emptyList(), + + bookmarks?.privateTaggedEvents(privKey = loggedIn.privKey!!) ?: emptyList(), + bookmarks?.privateTaggedUsers(privKey = loggedIn.privKey!!) ?: emptyList(), + bookmarks?.privateTaggedAddresses(privKey = loggedIn.privKey!!) ?: emptyList(), + + loggedIn.privKey!! + ) + } Client.send(event) LocalCache.consume(event) 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 bff36a298..2715282dd 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 @@ -2183,13 +2183,15 @@ fun NoteDropDownMenu(note: Note, popupExpanded: Boolean, onDismiss: () -> Unit, val actContext = LocalContext.current var reportDialogShowing by remember { mutableStateOf(false) } + val bookmarkState by accountViewModel.userProfile().live().bookmarks.observeAsState() + var state by remember { mutableStateOf( DropDownParams(false, false, false, false) ) } - LaunchedEffect(key1 = note) { + LaunchedEffect(key1 = note, key2 = bookmarkState) { withContext(Dispatchers.IO) { state = DropDownParams( accountViewModel.isFollowing(note.author), From e41df98920764888199507378e35518eb1269d7e Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Wed, 17 May 2023 17:01:07 -0400 Subject: [PATCH 23/24] Bugfix for channel metadata messages show up before channel creation packages --- .../amethyst/model/LocalCache.kt | 29 +++++++++++-------- .../amethyst/ui/note/BlankNote.kt | 4 +-- .../ui/note/ChatroomMessageCompose.kt | 16 ++++++---- .../amethyst/ui/note/NoteCompose.kt | 8 ++--- .../amethyst/ui/note/NoteQuickActionMenu.kt | 7 +++-- .../ui/screen/loggedIn/ChannelScreen.kt | 7 ++++- 6 files changed, 44 insertions(+), 27 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 16257be07..eea2d7851 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/model/LocalCache.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/model/LocalCache.kt @@ -553,17 +553,20 @@ object LocalCache { // Log.d("MT", "New Event ${event.content} ${event.id.toHex()}") val oldChannel = getOrCreateChannel(event.id) val author = getOrCreateUser(event.pubKey) + + val note = getOrCreateNote(event.id) + if (note.event == null) { + oldChannel.addNote(note) + note.loadEvent(event, author, emptyList()) + + refreshObservers(note) + } + if (event.createdAt <= oldChannel.updatedMetadataAt) { return // older data, does nothing } if (oldChannel.creator == null || oldChannel.creator == author) { oldChannel.updateChannelInfo(author, event.channelInfo(), event.createdAt) - - val note = getOrCreateNote(event.id) - oldChannel.addNote(note) - note.loadEvent(event, author, emptyList()) - - refreshObservers(note) } } @@ -578,16 +581,18 @@ object LocalCache { if (event.createdAt > oldChannel.updatedMetadataAt) { if (oldChannel.creator == null || oldChannel.creator == author) { oldChannel.updateChannelInfo(author, event.channelInfo(), event.createdAt) - - val note = getOrCreateNote(event.id) - oldChannel.addNote(note) - note.loadEvent(event, author, emptyList()) - - refreshObservers(note) } } else { // Log.d("MT","Relay sent a previous Metadata Event ${oldUser.toBestDisplayName()} ${formattedDateTime(event.createdAt)} > ${formattedDateTime(oldUser.updatedAt)}") } + + val note = getOrCreateNote(event.id) + if (note.event == null) { + oldChannel.addNote(note) + note.loadEvent(event, author, emptyList()) + + refreshObservers(note) + } } fun consume(event: ChannelMessageEvent, relay: Relay?) { diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/note/BlankNote.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/BlankNote.kt index 98aa24cdb..a333a2b6c 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/note/BlankNote.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/BlankNote.kt @@ -24,7 +24,7 @@ import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.model.User @Composable -fun BlankNote(modifier: Modifier = Modifier, isQuote: Boolean = false) { +fun BlankNote(modifier: Modifier = Modifier, isQuote: Boolean = false, idHex: String? = null) { Column(modifier = modifier) { Row(modifier = Modifier.padding(horizontal = if (!isQuote) 12.dp else 6.dp)) { Column(modifier = Modifier.padding(start = if (!isQuote) 10.dp else 5.dp)) { @@ -39,7 +39,7 @@ fun BlankNote(modifier: Modifier = Modifier, isQuote: Boolean = false) { horizontalArrangement = Arrangement.Center ) { Text( - text = stringResource(R.string.post_not_found), + text = stringResource(R.string.post_not_found) + if (idHex != null) ": $idHex" else "", modifier = Modifier.padding(30.dp), color = Color.Gray ) diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/note/ChatroomMessageCompose.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/ChatroomMessageCompose.kt index 57ed43b3a..55fe08669 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/note/ChatroomMessageCompose.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/ChatroomMessageCompose.kt @@ -90,12 +90,12 @@ fun ChatroomMessageCompose( val loggedIn = remember(accountState) { accountState?.account?.userProfile() } ?: return val noteState by baseNote.live().metadata.observeAsState() - val note = remember(noteState) { noteState?.note } + val note = remember(noteState) { noteState?.note } ?: return val noteReportsState by baseNote.live().reports.observeAsState() val noteForReports = remember(noteReportsState) { noteReportsState?.note } ?: return - val noteEvent = note?.event + val noteEvent = remember(noteState) { note.event } var popupExpanded by remember { mutableStateOf(false) } @@ -107,7 +107,7 @@ fun ChatroomMessageCompose( ) ) - note?.let { + note.let { NoteQuickActionMenu(it, popupExpanded, { popupExpanded = false }, accountViewModel) } } else if (account.isHidden(noteForReports.author!!)) { @@ -209,7 +209,11 @@ fun ChatroomMessageCompose( shape = shape, modifier = Modifier .combinedClickable( - onClick = { }, + onClick = { + if (noteEvent is ChannelCreateEvent) { + navController.navigate("Channel/${note.idHex}") + } + }, onLongClick = { popupExpanded = true } ) ) { @@ -228,12 +232,14 @@ fun ChatroomMessageCompose( alignment, navController ) + } else { + Spacer(modifier = Modifier.height(5.dp)) } val replyTo = note.replyTo if (!innerQuote && !replyTo.isNullOrEmpty()) { Row(verticalAlignment = Alignment.CenterVertically) { - replyTo.toSet().mapIndexed { _, note -> + replyTo.lastOrNull()?.let { note -> ChatroomMessageCompose( note, null, diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/note/NoteCompose.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/NoteCompose.kt index 2715282dd..b6c279f1f 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 @@ -196,13 +196,13 @@ fun NoteComposeInner( val loggedIn = remember(accountState) { accountState?.account?.userProfile() } ?: return val noteState by baseNote.live().metadata.observeAsState() - val note = remember(noteState) { noteState?.note } + val note = remember(noteState) { noteState?.note } ?: return val noteReportsState by baseNote.live().reports.observeAsState() val noteForReports = remember(noteReportsState) { noteReportsState?.note } ?: return - val noteEvent = note?.event - val baseChannel = note?.channel() + val noteEvent = remember(noteState) { note.event } + val baseChannel = remember(noteState) { note.channel() } var popupExpanded by remember { mutableStateOf(false) } @@ -217,7 +217,7 @@ fun NoteComposeInner( isBoostedNote ) - note?.let { + note.let { NoteQuickActionMenu(it, popupExpanded, { popupExpanded = false }, accountViewModel) } } else if (account.isHidden(noteForReports.author!!)) { diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/note/NoteQuickActionMenu.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/NoteQuickActionMenu.kt index 02369406b..73c75812c 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/note/NoteQuickActionMenu.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/NoteQuickActionMenu.kt @@ -217,10 +217,11 @@ fun NoteQuickActionMenu(note: Note, popupExpanded: Boolean, onDismiss: () -> Uni VerticalDivider(primaryLight) NoteQuickActionItem( - icon = ImageVector.vectorResource(id = R.drawable.text_select_move_forward_character), - label = stringResource(R.string.quick_action_select) + icon = ImageVector.vectorResource(id = R.drawable.relays), + label = stringResource(R.string.broadcast) ) { - showSelectTextDialog = true + accountViewModel.broadcast(note) + // showSelectTextDialog = true onDismiss() } VerticalDivider(primaryLight) 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 d2cdff261..c6bcc52ab 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 @@ -2,6 +2,7 @@ package com.vitorpamplona.amethyst.ui.screen.loggedIn import android.widget.Toast import androidx.compose.animation.animateContentSize +import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.PaddingValues @@ -253,7 +254,11 @@ fun ChannelHeader(baseChannel: Channel, account: Account, navController: NavCont val context = LocalContext.current.applicationContext - Column() { + Column( + Modifier.clickable { + navController.navigate("Channel/${baseChannel.idHex}") + } + ) { Column(modifier = Modifier.padding(12.dp)) { Row(verticalAlignment = Alignment.CenterVertically) { RobohashAsyncImageProxy( From 639569cd866fb0e78206c805167de261b6440bd2 Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Wed, 17 May 2023 17:05:06 -0400 Subject: [PATCH 24/24] v0.50.4 --- app/build.gradle | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/build.gradle b/app/build.gradle index d44fd5d93..bf17bcf25 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -13,8 +13,8 @@ android { applicationId "com.vitorpamplona.amethyst" minSdk 26 targetSdk 33 - versionCode 171 - versionName "0.50.3" + versionCode 172 + versionName "0.50.4" testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" vectorDrawables {