diff --git a/app/src/main/java/com/vitorpamplona/amethyst/LocalPreferences.kt b/app/src/main/java/com/vitorpamplona/amethyst/LocalPreferences.kt index 07bd34fa0e..1c6f1bbc91 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/LocalPreferences.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/LocalPreferences.kt @@ -2,8 +2,11 @@ package com.vitorpamplona.amethyst import android.content.Context import com.vitorpamplona.amethyst.model.Account +import com.vitorpamplona.amethyst.model.Channel import com.vitorpamplona.amethyst.model.DefaultChannels +import com.vitorpamplona.amethyst.model.User import com.vitorpamplona.amethyst.model.toByteArray +import com.vitorpamplona.amethyst.ui.navigation.Route import nostr.postr.Persona import nostr.postr.toHex @@ -47,4 +50,16 @@ class LocalPreferences(context: Context) { } } + fun saveLastRead(route: String, timestampInSecs: Long) { + encryptedPreferences.edit().apply { + putLong("last_read_route_${route}", timestampInSecs) + }.apply() + } + + fun loadLastRead(route: String): Long { + encryptedPreferences.run { + return getLong("last_read_route_${route}", 0) + } + } + } \ No newline at end of file diff --git a/app/src/main/java/com/vitorpamplona/amethyst/NotificationCache.kt b/app/src/main/java/com/vitorpamplona/amethyst/NotificationCache.kt new file mode 100644 index 0000000000..a8f5bd4a5f --- /dev/null +++ b/app/src/main/java/com/vitorpamplona/amethyst/NotificationCache.kt @@ -0,0 +1,62 @@ +package com.vitorpamplona.amethyst + +import android.content.Context +import androidx.lifecycle.LiveData +import com.vitorpamplona.amethyst.model.Note +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.Job +import kotlinx.coroutines.delay +import kotlinx.coroutines.launch + +object NotificationCache { + val lastReadByRoute = mutableMapOf() + + fun markAsRead(route: String, timestampInSecs: Long, context: Context) { + val lastTime = lastReadByRoute[route] + if (lastTime == null || timestampInSecs > lastTime) { + lastReadByRoute.put(route, timestampInSecs) + + val scope = CoroutineScope(Job() + Dispatchers.IO) + scope.launch { + LocalPreferences(context).saveLastRead(route, timestampInSecs) + invalidateData() + } + } + } + + fun load(route: String, context: Context): Long { + var lastTime = lastReadByRoute[route] + if (lastTime == null) { + lastTime = LocalPreferences(context).loadLastRead(route) + lastReadByRoute[route] = lastTime + } + return lastTime + } + + // Observers line up here. + val live: NotificationLiveData = NotificationLiveData(this) + + // Refreshes observers in batches. + var handlerWaiting = false + @Synchronized + fun invalidateData() { + if (handlerWaiting) return + + handlerWaiting = true + val scope = CoroutineScope(Job() + Dispatchers.Default) + scope.launch { + delay(100) + live.refresh() + handlerWaiting = false + } + } +} + +class NotificationLiveData(val cache: NotificationCache): LiveData(NotificationState(cache)) { + fun refresh() { + postValue(NotificationState(cache)) + } +} + +class NotificationState(val cache: NotificationCache) \ No newline at end of file diff --git a/app/src/main/java/com/vitorpamplona/amethyst/model/Note.kt b/app/src/main/java/com/vitorpamplona/amethyst/model/Note.kt index 6226c323fb..fd67f27957 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/model/Note.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/model/Note.kt @@ -10,6 +10,7 @@ import java.time.Instant import java.time.ZoneId import java.time.format.DateTimeFormatter import java.util.Collections +import java.util.Date import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Job diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/navigation/AppBottomBar.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/navigation/AppBottomBar.kt index 7ae3f990e4..75677423c9 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/navigation/AppBottomBar.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/navigation/AppBottomBar.kt @@ -1,18 +1,36 @@ package com.vitorpamplona.amethyst.ui.navigation +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.layout.wrapContentHeight +import androidx.compose.foundation.shape.CircleShape import androidx.compose.material.BottomNavigation import androidx.compose.material.BottomNavigationItem import androidx.compose.material.Divider import androidx.compose.material.Icon import androidx.compose.material.MaterialTheme +import androidx.compose.material.Text import androidx.compose.runtime.Composable +import androidx.compose.runtime.livedata.observeAsState +import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip import androidx.compose.ui.graphics.Color import androidx.compose.ui.res.painterResource +import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp import androidx.navigation.NavHostController +import com.vitorpamplona.amethyst.NotificationCache +import com.vitorpamplona.amethyst.model.LocalCache +import com.vitorpamplona.amethyst.ui.note.NewItemsBubble +import kotlinx.coroutines.launch val bottomNavigationItems = listOf( Route.Home, @@ -24,6 +42,7 @@ val bottomNavigationItems = listOf( @Composable fun AppBottomBar(navController: NavHostController) { val currentRoute = currentRoute(navController) + val coroutineScope = rememberCoroutineScope() Column() { Divider( @@ -36,35 +55,30 @@ fun AppBottomBar(navController: NavHostController) { ) { bottomNavigationItems.forEach { item -> BottomNavigationItem( - icon = { - Icon( - painter = painterResource(id = item.icon), - null, - modifier = Modifier.size(if ("Home" == item.route) 24.dp else 20.dp), - tint = if (currentRoute == item.route) MaterialTheme.colors.primary else Color.Unspecified - ) - }, + icon = { NotifiableIcon(item, currentRoute) }, selected = currentRoute == item.route, onClick = { - if (currentRoute != item.route) { - navController.navigate(item.route){ - navController.graph.startDestinationRoute?.let { start -> - popUpTo(start) - restoreState = true - } - launchSingleTop = true - restoreState = true - } - } else { - // TODO: Make it scrool to the top - navController.navigate(item.route){ - navController.graph.startDestinationRoute?.let { start -> - popUpTo(start) { inclusive = item.route == Route.Home.route } + coroutineScope.launch { + if (currentRoute != item.route) { + navController.navigate(item.route){ + navController.graph.startDestinationRoute?.let { start -> + popUpTo(start) + restoreState = true + } + launchSingleTop = true restoreState = true } + } else { + // TODO: Make it scrool to the top + navController.navigate(item.route){ + navController.graph.startDestinationRoute?.let { start -> + popUpTo(start) { inclusive = item.route == Route.Home.route } + restoreState = true + } - launchSingleTop = true - restoreState = true + launchSingleTop = true + restoreState = true + } } } } @@ -73,3 +87,52 @@ fun AppBottomBar(navController: NavHostController) { } } } + +@Composable +private fun NotifiableIcon(item: Route, currentRoute: String?) { + Box(Modifier.size(if ("Home" == item.route) 25.dp else 23.dp)) { + Icon( + painter = painterResource(id = item.icon), + null, + modifier = Modifier.size(if ("Home" == item.route) 24.dp else 20.dp), + tint = if (currentRoute == item.route) MaterialTheme.colors.primary else Color.Unspecified + ) + + // Notification + val dbState = LocalCache.live.observeAsState() + val notifState = NotificationCache.live.observeAsState() + + val db = dbState.value + val notif = notifState.value + + if (db != null && notif != null) { + if (item.hasNewItems(db.cache, notif.cache)) { + Box( + Modifier + .width(10.dp) + .height(10.dp) + .align(Alignment.TopEnd) + ) { + Box( + modifier = Modifier + .width(10.dp) + .height(10.dp) + .clip(shape = CircleShape) + .background(MaterialTheme.colors.primary), + contentAlignment = Alignment.TopEnd + ) { + Text( + "", + color = Color.White, + textAlign = TextAlign.Center, + fontSize = 12.sp, + modifier = Modifier + .wrapContentHeight() + .align(Alignment.TopEnd) + ) + } + } + } + } + } +} diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/navigation/Routes.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/navigation/Routes.kt index 31112c0702..07a944de5c 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/navigation/Routes.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/navigation/Routes.kt @@ -2,6 +2,7 @@ package com.vitorpamplona.amethyst.ui.navigation import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue +import androidx.compose.ui.platform.LocalContext import androidx.navigation.NamedNavArgument import androidx.navigation.NavBackStackEntry import androidx.navigation.NavController @@ -9,13 +10,23 @@ import androidx.navigation.NavHostController import androidx.navigation.NavType import androidx.navigation.compose.currentBackStackEntryAsState import androidx.navigation.navArgument +import com.vitorpamplona.amethyst.NotificationCache import com.vitorpamplona.amethyst.R +import com.vitorpamplona.amethyst.model.LocalCache +import com.vitorpamplona.amethyst.model.Note +import com.vitorpamplona.amethyst.service.NostrChatroomListDataSource +import com.vitorpamplona.amethyst.service.NostrDataSource +import com.vitorpamplona.amethyst.service.NostrHomeDataSource +import com.vitorpamplona.amethyst.service.NostrNotificationDataSource +import com.vitorpamplona.amethyst.service.model.RepostEvent import com.vitorpamplona.amethyst.ui.screen.AccountStateViewModel import com.vitorpamplona.amethyst.ui.screen.ChannelScreen import com.vitorpamplona.amethyst.ui.screen.ChatroomListScreen import com.vitorpamplona.amethyst.ui.screen.ChatroomScreen import com.vitorpamplona.amethyst.ui.screen.FiltersScreen import com.vitorpamplona.amethyst.ui.screen.HomeScreen +import com.vitorpamplona.amethyst.ui.screen.NostrChatroomListKnownFeedViewModel +import com.vitorpamplona.amethyst.ui.screen.NostrHomeFeedViewModel import com.vitorpamplona.amethyst.ui.screen.NotificationScreen import com.vitorpamplona.amethyst.ui.screen.ProfileScreen import com.vitorpamplona.amethyst.ui.screen.SearchScreen @@ -26,14 +37,30 @@ import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel sealed class Route( val route: String, val icon: Int, + val hasNewItems: @Composable (LocalCache, NotificationCache) -> Boolean = @Composable { _,_ -> false }, val arguments: List = emptyList(), val buildScreen: (AccountViewModel, AccountStateViewModel, NavController) -> @Composable (NavBackStackEntry) -> Unit ) { - object Home : Route("Home", R.drawable.ic_home, buildScreen = { acc, accSt, nav -> { _ -> HomeScreen(acc, nav) } }) - object Search : Route("Search", R.drawable.ic_search, buildScreen = { acc, accSt, nav -> { _ -> SearchScreen(acc, nav) }}) - object Notification : Route("Notification", R.drawable.ic_notifications,buildScreen = { acc, accSt, nav -> { _ -> NotificationScreen(acc, nav) }}) - object Message : Route("Message", R.drawable.ic_dm, buildScreen = { acc, accSt, nav -> { _ -> ChatroomListScreen(acc, nav) }}) - object Filters : Route("Filters", R.drawable.ic_dm, buildScreen = { acc, accSt, nav -> { _ -> FiltersScreen(acc, nav) }}) + object Home : Route("Home", R.drawable.ic_home, + hasNewItems = { _, cache -> homeHasNewItems(cache) }, + buildScreen = { acc, accSt, nav -> { _ -> HomeScreen(acc, nav) } } + ) + object Search : Route("Search", R.drawable.ic_search, + buildScreen = { acc, accSt, nav -> { _ -> SearchScreen(acc, nav) }} + ) + object Notification : Route("Notification", R.drawable.ic_notifications, + hasNewItems = { _, cache -> notificationHasNewItems(cache) }, + buildScreen = { acc, accSt, nav -> { _ -> NotificationScreen(acc, nav) }} + ) + + object Message : Route("Message", R.drawable.ic_dm, + hasNewItems = { _, cache -> messagesHasNewItems(cache) }, + buildScreen = { acc, accSt, nav -> { _ -> ChatroomListScreen(acc, nav) }} + ) + + object Filters : Route("Filters", R.drawable.ic_dm, + buildScreen = { acc, accSt, nav -> { _ -> FiltersScreen(acc, nav) }} + ) object Profile : Route("User/{id}", R.drawable.ic_profile, arguments = listOf(navArgument("id") { type = NavType.StringType } ), @@ -71,8 +98,58 @@ val Routes = listOf( Route.Filters ) +//** +//* Functions below only exist because we have not broken the datasource classes into backend and frontend. +//** @Composable public fun currentRoute(navController: NavHostController): String? { val navBackStackEntry by navController.currentBackStackEntryAsState() return navBackStackEntry?.destination?.route +} + +@Composable +private fun homeHasNewItems(cache: NotificationCache): Boolean { + val context = LocalContext.current.applicationContext + val lastTimeFollows = cache.load("HomeFollows", context) + + val homeFeed = NostrHomeDataSource.feed().take(100) + + val hasNewInFollows = homeFeed.filter { + it.event is RepostEvent || it.replyTo == null || it.replyTo?.size == 0 + }.filter { + (it.event?.createdAt ?: 0) > lastTimeFollows + }.isNotEmpty() + + return hasNewInFollows +} + +@Composable +private fun notificationHasNewItems(cache: NotificationCache): Boolean { + val context = LocalContext.current.applicationContext + val lastTime = cache.load("Notification", context) + return NostrNotificationDataSource.loadTop() + .filter { it.event != null && it.event!!.createdAt > lastTime } + .isNotEmpty() +} + +@Composable +private fun messagesHasNewItems(cache: NotificationCache): Boolean { + val context = LocalContext.current.applicationContext + return NostrChatroomListDataSource.feed().take(100).filter { + // only for known sources + val me = NostrChatroomListDataSource.account.userProfile() + it.channel != null || me.messages[it.author]?.firstOrNull { me == it.author } != null + }.filter { + val lastTime = if (it.channel != null) { + cache.load("Channel/${it.channel!!.idHex}", context) + } else { + cache.load("Room/${it.author?.pubkeyHex}", context) + } + + if (NostrChatroomListDataSource.account.isAcceptable(it) && it.event != null && it.event!!.createdAt > lastTime) { + println("${it.author?.toBestDisplayName()}") + } + + NostrChatroomListDataSource.account.isAcceptable(it) && it.event != null && it.event!!.createdAt > lastTime + }.isNotEmpty() } \ No newline at end of file diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/note/BoostSetCompose.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/BoostSetCompose.kt index 9759728b6f..99e39c5278 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/note/BoostSetCompose.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/BoostSetCompose.kt @@ -1,5 +1,6 @@ package com.vitorpamplona.amethyst.ui.note +import androidx.compose.foundation.background import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column @@ -10,6 +11,7 @@ import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.width import androidx.compose.foundation.shape.CircleShape import androidx.compose.material.Icon +import androidx.compose.material.MaterialTheme import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.compose.runtime.livedata.observeAsState @@ -17,25 +19,36 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip import androidx.compose.ui.graphics.Color +import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.painterResource import androidx.compose.ui.unit.dp import androidx.navigation.NavController import coil.compose.AsyncImage import coil.compose.rememberAsyncImagePainter import com.google.accompanist.flowlayout.FlowRow +import com.vitorpamplona.amethyst.NotificationCache import com.vitorpamplona.amethyst.R import com.vitorpamplona.amethyst.ui.screen.BoostSetCard import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel @Composable -fun BoostSetCompose(likeSetCard: BoostSetCard, isInnerNote: Boolean = false, accountViewModel: AccountViewModel, navController: NavController) { +fun BoostSetCompose(likeSetCard: BoostSetCard, isInnerNote: Boolean = false, routeForLastRead: String, accountViewModel: AccountViewModel, navController: NavController) { val noteState by likeSetCard.note.live.observeAsState() val note = noteState?.note + val context = LocalContext.current.applicationContext + if (note?.event == null) { BlankNote(Modifier, isInnerNote) } else { - Column() { + val isNew = likeSetCard.createdAt > NotificationCache.load(routeForLastRead, context) + NotificationCache.markAsRead(routeForLastRead, likeSetCard.createdAt, context) + + Column( + modifier = Modifier.background( + if (isNew) MaterialTheme.colors.primary.copy(0.12f) else MaterialTheme.colors.background + ) + ) { Row(modifier = Modifier .padding( start = if (!isInnerNote) 12.dp else 0.dp, @@ -84,7 +97,7 @@ fun BoostSetCompose(likeSetCard: BoostSetCard, isInnerNote: Boolean = false, acc } } - NoteCompose(note, Modifier.padding(top = 5.dp), true, accountViewModel, navController) + NoteCompose(note, null, Modifier.padding(top = 5.dp), true, accountViewModel, navController) } } } diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/note/ChatroomCompose.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/ChatroomCompose.kt index d8904e37d0..15b9b71124 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/note/ChatroomCompose.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/ChatroomCompose.kt @@ -1,12 +1,19 @@ package com.vitorpamplona.amethyst.ui.note +import androidx.compose.foundation.background +import androidx.compose.foundation.border import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Arrangement.Center +import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.width +import androidx.compose.foundation.layout.wrapContentHeight import androidx.compose.foundation.shape.CircleShape import androidx.compose.material.Divider import androidx.compose.material.LocalTextStyle @@ -18,9 +25,12 @@ import androidx.compose.runtime.livedata.observeAsState import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.painter.Painter +import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.text.TextStyle import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.text.style.TextDirection import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp @@ -28,6 +38,8 @@ import androidx.compose.ui.unit.sp import androidx.navigation.NavController import coil.compose.AsyncImage import coil.compose.rememberAsyncImagePainter +import com.vitorpamplona.amethyst.LocalPreferences +import com.vitorpamplona.amethyst.NotificationCache import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.service.model.ChannelCreateEvent import com.vitorpamplona.amethyst.service.model.ChannelMetadataEvent @@ -44,6 +56,11 @@ fun ChatroomCompose(baseNote: Note, accountViewModel: AccountViewModel, navContr val accountUserState by account.userProfile().live.observeAsState() val accountUser = accountUserState?.user ?: return + val notificationCacheState = NotificationCache.live.observeAsState() + val notificationCache = notificationCacheState.value ?: return + + val context = LocalContext.current.applicationContext + if (note?.event == null) { BlankNote(Modifier) } else if (note.channel != null) { @@ -53,14 +70,22 @@ fun ChatroomCompose(baseNote: Note, accountViewModel: AccountViewModel, navContr val channelState by note.channel!!.live.observeAsState() val channel = channelState?.channel - val description = if (note.event is ChannelCreateEvent) { + val noteEvent = note.event + + val description = if (noteEvent is ChannelCreateEvent) { "Channel created" - } else if (note.event is ChannelMetadataEvent) { + } else if (noteEvent is ChannelMetadataEvent) { "Channel Information changed to " } else { - note.event?.content + noteEvent?.content } channel?.let { channel -> + val hasNewMessages = + if (noteEvent != null) + noteEvent.createdAt > notificationCache.cache.load("Channel/${channel.idHex}", context) + else + false + ChannelName( channelPicture = channel.profilePicture(), channelPicturePlaceholder = null, @@ -78,6 +103,7 @@ fun ChatroomCompose(baseNote: Note, accountViewModel: AccountViewModel, navContr }, channelLastTime = note.event?.createdAt, channelLastContent = "${author?.toBestDisplayName()}: " + description, + hasNewMessages = hasNewMessages, onClick = { navController.navigate("Channel/${channel.idHex}") }) } @@ -98,12 +124,21 @@ fun ChatroomCompose(baseNote: Note, accountViewModel: AccountViewModel, navContr } } + val noteEvent = note.event + userToComposeOn?.let { user -> + val hasNewMessages = + if (noteEvent != null) + noteEvent.createdAt > notificationCache.cache.load("Room/${userToComposeOn.pubkeyHex}", context) + else + false + ChannelName( channelPicture = { UserPicture(user = user, userAccount = accountUser, size = 55.dp) }, channelTitle = { UsernameDisplay(user, it) }, - channelLastTime = note.event?.createdAt, + channelLastTime = noteEvent?.createdAt, channelLastContent = accountViewModel.decrypt(note), + hasNewMessages = hasNewMessages, onClick = { navController.navigate("Room/${user.pubkeyHex}") }) } } @@ -117,6 +152,7 @@ fun ChannelName( channelTitle: @Composable (Modifier) -> Unit, channelLastTime: Long?, channelLastContent: String?, + hasNewMessages: Boolean, onClick: () -> Unit ) { ChannelName( @@ -134,6 +170,7 @@ fun ChannelName( channelTitle, channelLastTime, channelLastContent, + hasNewMessages, onClick ) } @@ -144,6 +181,7 @@ fun ChannelName( channelTitle: @Composable (Modifier) -> Unit, channelLastTime: Long?, channelLastContent: String?, + hasNewMessages: Boolean, onClick: () -> Unit ) { Column(modifier = Modifier.clickable(onClick = onClick) ) { @@ -169,21 +207,29 @@ fun ChannelName( } - if (channelLastContent != null) - Text( - channelLastContent, - color = MaterialTheme.colors.onSurface.copy(alpha = 0.52f), - maxLines = 1, - overflow = TextOverflow.Ellipsis, - style = LocalTextStyle.current.copy(textDirection = TextDirection.Content) - ) - else - Text( - "Referenced event not found", - color = MaterialTheme.colors.onSurface.copy(alpha = 0.52f), - maxLines = 1, - overflow = TextOverflow.Ellipsis - ) + Row(modifier = Modifier.fillMaxWidth(), verticalAlignment = Alignment.CenterVertically) { + if (channelLastContent != null) + Text( + channelLastContent, + color = MaterialTheme.colors.onSurface.copy(alpha = 0.52f), + maxLines = 1, + overflow = TextOverflow.Ellipsis, + style = LocalTextStyle.current.copy(textDirection = TextDirection.Content), + modifier = Modifier.weight(1f) + ) + else + Text( + "Referenced event not found", + color = MaterialTheme.colors.onSurface.copy(alpha = 0.52f), + maxLines = 1, + overflow = TextOverflow.Ellipsis, + modifier = Modifier.weight(1f) + ) + + if (hasNewMessages) { + NewItemsBubble() + } + } } } @@ -192,4 +238,27 @@ fun ChannelName( thickness = 0.25.dp ) } +} + +@Composable +fun NewItemsBubble() { + Box( + modifier = Modifier + .padding(start = 3.dp) + .width(10.dp) + .height(10.dp) + .clip(shape = CircleShape) + .background(MaterialTheme.colors.primary), + contentAlignment = Alignment.Center + ) { + Text( + "", + color = Color.White, + textAlign = TextAlign.Center, + fontSize = 12.sp, + modifier = Modifier + .wrapContentHeight() + .align(Alignment.Center) + ) + } } \ No newline at end of file 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 c6192f6c13..076b4c2f0c 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 @@ -30,12 +30,14 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Shape +import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import androidx.navigation.NavController import coil.compose.AsyncImage import coil.compose.rememberAsyncImagePainter +import com.vitorpamplona.amethyst.NotificationCache import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.service.model.ChannelCreateEvent import com.vitorpamplona.amethyst.service.model.ChannelMessageEvent @@ -50,7 +52,7 @@ val ChatBubbleShapeThem = RoundedCornerShape(3.dp, 15.dp, 15.dp, 15.dp) @OptIn(ExperimentalFoundationApi::class) @Composable -fun ChatroomMessageCompose(baseNote: Note, innerQuote: Boolean = false, accountViewModel: AccountViewModel, navController: NavController) { +fun ChatroomMessageCompose(baseNote: Note, routeForLastRead: String?, innerQuote: Boolean = false, accountViewModel: AccountViewModel, navController: NavController) { val noteState by baseNote.live.observeAsState() val note = noteState?.note @@ -62,6 +64,8 @@ fun ChatroomMessageCompose(baseNote: Note, innerQuote: Boolean = false, accountV var popupExpanded by remember { mutableStateOf(false) } + val context = LocalContext.current.applicationContext + if (note?.event == null) { BlankNote(Modifier) } else { @@ -82,6 +86,17 @@ fun ChatroomMessageCompose(baseNote: Note, innerQuote: Boolean = false, accountV shape = ChatBubbleShapeThem } + // Mark read + val isNew = routeForLastRead?.run { + val isNew = NotificationCache.load(this, context) + + val createdAt = note.event?.createdAt + if (createdAt != null) + NotificationCache.markAsRead(this, createdAt, context) + + isNew + } + Column() { Row( modifier = Modifier.fillMaxWidth(1f).padding( @@ -150,6 +165,7 @@ fun ChatroomMessageCompose(baseNote: Note, innerQuote: Boolean = false, accountV if (note.event != null) ChatroomMessageCompose( note, + null, innerQuote = true, accountViewModel = accountViewModel, navController = navController diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/note/LikeSetCompose.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/LikeSetCompose.kt index 3365cf02ce..85bc9c962d 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/note/LikeSetCompose.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/LikeSetCompose.kt @@ -1,5 +1,6 @@ package com.vitorpamplona.amethyst.ui.note +import androidx.compose.foundation.background import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column @@ -10,6 +11,7 @@ import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.width import androidx.compose.foundation.shape.CircleShape import androidx.compose.material.Icon +import androidx.compose.material.MaterialTheme import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.compose.runtime.livedata.observeAsState @@ -17,26 +19,37 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip import androidx.compose.ui.graphics.Color +import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.painterResource import androidx.compose.ui.unit.dp import androidx.navigation.NavController import coil.compose.AsyncImage import coil.compose.rememberAsyncImagePainter import com.google.accompanist.flowlayout.FlowRow +import com.vitorpamplona.amethyst.NotificationCache import com.vitorpamplona.amethyst.R import com.vitorpamplona.amethyst.ui.screen.LikeSetCard import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel @Composable -fun LikeSetCompose(likeSetCard: LikeSetCard, modifier: Modifier = Modifier, isInnerNote: Boolean = false, accountViewModel: AccountViewModel, navController: NavController) { +fun LikeSetCompose(likeSetCard: LikeSetCard, modifier: Modifier = Modifier, isInnerNote: Boolean = false, routeForLastRead: String, accountViewModel: AccountViewModel, navController: NavController) { val noteState by likeSetCard.note.live.observeAsState() val note = noteState?.note + val context = LocalContext.current.applicationContext + if (note == null) { BlankNote(Modifier, isInnerNote) } else { - Column(modifier = modifier) { - Row( modifier = Modifier + val isNew = likeSetCard.createdAt > NotificationCache.load(routeForLastRead, context) + NotificationCache.markAsRead(routeForLastRead, likeSetCard.createdAt, context) + + Column( + modifier = Modifier.background( + if (isNew) MaterialTheme.colors.primary.copy(0.12f) else MaterialTheme.colors.background + ) + ) { + Row(modifier = Modifier .padding( start = if (!isInnerNote) 12.dp else 0.dp, end = if (!isInnerNote) 12.dp else 0.dp, @@ -84,7 +97,7 @@ fun LikeSetCompose(likeSetCard: LikeSetCard, modifier: Modifier = Modifier, isIn } } - NoteCompose(note, Modifier.padding(top = 5.dp), true, accountViewModel, navController) + NoteCompose(note, null, Modifier.padding(top = 5.dp), true, accountViewModel, navController) } } } 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 fad2ef4320..23503b4a9a 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 @@ -43,6 +43,7 @@ import androidx.compose.ui.unit.dp import androidx.navigation.NavController import coil.compose.AsyncImage import coil.compose.rememberAsyncImagePainter +import com.vitorpamplona.amethyst.NotificationCache import com.vitorpamplona.amethyst.R import com.vitorpamplona.amethyst.model.Account import com.vitorpamplona.amethyst.model.Note @@ -59,7 +60,14 @@ import nostr.postr.toNpub @OptIn(ExperimentalFoundationApi::class) @Composable -fun NoteCompose(baseNote: Note, modifier: Modifier = Modifier, isInnerNote: Boolean = false, accountViewModel: AccountViewModel, navController: NavController) { +fun NoteCompose( + baseNote: Note, + routeForLastRead: String? = null, + modifier: Modifier = Modifier, + isInnerNote: Boolean = false, + accountViewModel: AccountViewModel, + navController: NavController +) { val accountState by accountViewModel.accountLiveData.observeAsState() val account = accountState?.account ?: return @@ -68,6 +76,8 @@ fun NoteCompose(baseNote: Note, modifier: Modifier = Modifier, isInnerNote: Bool var popupExpanded by remember { mutableStateOf(false) } + val context = LocalContext.current.applicationContext + if (note?.event == null) { BlankNote(modifier.combinedClickable( onClick = { }, @@ -82,6 +92,18 @@ fun NoteCompose(baseNote: Note, modifier: Modifier = Modifier, isInnerNote: Bool val authorState by note.author?.live!!.observeAsState() val author = authorState?.user ?: return // if it has event, it should have an author + val isNew = routeForLastRead?.run { + val lastTime = NotificationCache.load(this, context) + + val createdAt = note.event?.createdAt + if (createdAt != null) { + NotificationCache.markAsRead(this, createdAt, context) + createdAt > lastTime + } else { + false + } + } ?: false + Column(modifier = modifier.combinedClickable( onClick = { @@ -95,8 +117,14 @@ fun NoteCompose(baseNote: Note, modifier: Modifier = Modifier, isInnerNote: Bool } } }, - onLongClick = { popupExpanded = true }, - ) + onLongClick = { popupExpanded = true } + ).run { + if (isNew) { + this.background(MaterialTheme.colors.primary.copy(0.12f)) + } else { + this + } + } ) { Row( modifier = Modifier diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/CardFeedView.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/CardFeedView.kt index 967d3613c7..532dacb744 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/CardFeedView.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/CardFeedView.kt @@ -24,7 +24,7 @@ import com.vitorpamplona.amethyst.ui.note.NoteCompose import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel @Composable -fun CardFeedView(viewModel: CardFeedViewModel, accountViewModel: AccountViewModel, navController: NavController) { +fun CardFeedView(viewModel: CardFeedViewModel, accountViewModel: AccountViewModel, navController: NavController, routeForLastRead: String) { val feedState by viewModel.feedContent.collectAsState() var isRefreshing by remember { mutableStateOf(false) } @@ -68,9 +68,9 @@ fun CardFeedView(viewModel: CardFeedViewModel, accountViewModel: AccountViewMode ) { itemsIndexed(state.feed, key = { _, item -> item.id() }) { index, item -> when (item) { - is NoteCard -> NoteCompose(item.note, isInnerNote = false, accountViewModel = accountViewModel, navController = navController) - is LikeSetCard -> LikeSetCompose(item, isInnerNote = false, accountViewModel = accountViewModel, navController = navController) - is BoostSetCard -> BoostSetCompose(item, isInnerNote = false, accountViewModel = accountViewModel, navController = navController) + is NoteCard -> NoteCompose(item.note, isInnerNote = false, accountViewModel = accountViewModel, navController = navController, routeForLastRead = routeForLastRead) + is LikeSetCard -> LikeSetCompose(item, isInnerNote = false, accountViewModel = accountViewModel, navController = navController, routeForLastRead = routeForLastRead) + is BoostSetCard -> BoostSetCompose(item, isInnerNote = false, accountViewModel = accountViewModel, navController = navController, routeForLastRead = routeForLastRead) } } } diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/ChatroomFeedView.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/ChatroomFeedView.kt index c59f06e109..e73ba0bd10 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/ChatroomFeedView.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/ChatroomFeedView.kt @@ -21,7 +21,7 @@ import com.vitorpamplona.amethyst.ui.note.ChatroomMessageCompose import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel @Composable -fun ChatroomFeedView(viewModel: FeedViewModel, accountViewModel: AccountViewModel, navController: NavController) { +fun ChatroomFeedView(viewModel: FeedViewModel, accountViewModel: AccountViewModel, navController: NavController, routeForLastRead: String?) { val feedState by viewModel.feedContent.collectAsState() var isRefreshing by remember { mutableStateOf(false) } @@ -66,7 +66,7 @@ fun ChatroomFeedView(viewModel: FeedViewModel, accountViewModel: AccountViewMode ) { var previousDate: String = "" itemsIndexed(state.feed, key = { index, item -> if (index == 0) index else item.idHex }) { index, item -> - ChatroomMessageCompose(item, accountViewModel = accountViewModel, navController = navController) + ChatroomMessageCompose(item, routeForLastRead, accountViewModel = accountViewModel, navController = navController) } } } diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/FeedView.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/FeedView.kt index c4b9c72b62..d582ec28bb 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/FeedView.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/FeedView.kt @@ -21,16 +21,20 @@ import androidx.compose.runtime.remember import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.unit.dp import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.navigation.NavController import com.google.accompanist.swiperefresh.SwipeRefresh import com.google.accompanist.swiperefresh.rememberSwipeRefreshState +import com.vitorpamplona.amethyst.NotificationCache +import com.vitorpamplona.amethyst.model.Note +import com.vitorpamplona.amethyst.ui.navigation.Route import com.vitorpamplona.amethyst.ui.note.NoteCompose import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel @Composable -fun FeedView(viewModel: FeedViewModel, accountViewModel: AccountViewModel, navController: NavController) { +fun FeedView(viewModel: FeedViewModel, accountViewModel: AccountViewModel, navController: NavController, routeForLastRead: String?) { val feedState by viewModel.feedContent.collectAsState() var isRefreshing by remember { mutableStateOf(false) } @@ -73,7 +77,12 @@ fun FeedView(viewModel: FeedViewModel, accountViewModel: AccountViewModel, navCo state = listState ) { itemsIndexed(state.feed, key = { _, item -> item.idHex }) { index, item -> - NoteCompose(item, isInnerNote = false, accountViewModel = accountViewModel, navController = navController) + NoteCompose(item, + isInnerNote = false, + routeForLastRead = routeForLastRead, + accountViewModel = accountViewModel, + navController = navController + ) } } } diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/ThreadFeedView.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/ThreadFeedView.kt index 7008662d26..2b35fcc34a 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/ThreadFeedView.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/ThreadFeedView.kt @@ -107,7 +107,7 @@ fun ThreadFeedView(noteId: String, viewModel: FeedViewModel, accountViewModel: A Row() { NoteCompose( item, - Modifier.drawReplyLevel(item.replyLevel(), MaterialTheme.colors.onSurface.copy(alpha = 0.32f)), + modifier = Modifier.drawReplyLevel(item.replyLevel(), MaterialTheme.colors.onSurface.copy(alpha = 0.32f)), isInnerNote = false, accountViewModel = accountViewModel, navController = navController, 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 f0f8436c3c..7c97b565f2 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 @@ -94,7 +94,7 @@ fun ChannelScreen(channelId: String?, accountViewModel: AccountViewModel, accoun .padding(vertical = 0.dp) .weight(1f, true) ) { - ChatroomFeedView(feedViewModel, accountViewModel, navController) + ChatroomFeedView(feedViewModel, accountViewModel, navController, "Channel/${channelId}") } //LAST ROW diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/ChatroomScreen.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/ChatroomScreen.kt index 9b6df79639..915464d2af 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/ChatroomScreen.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/ChatroomScreen.kt @@ -69,7 +69,7 @@ fun ChatroomScreen(userId: String?, accountViewModel: AccountViewModel, navContr Column( modifier = Modifier.fillMaxHeight().padding(vertical = 0.dp).weight(1f, true) ) { - ChatroomFeedView(feedViewModel, accountViewModel, navController) + ChatroomFeedView(feedViewModel, accountViewModel, navController, "Room/${userId}") } //LAST ROW diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/HomeScreen.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/HomeScreen.kt index 4401126c28..b44689968b 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/HomeScreen.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/HomeScreen.kt @@ -16,6 +16,7 @@ import androidx.compose.runtime.livedata.observeAsState import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.unit.dp import androidx.lifecycle.viewmodel.compose.viewModel import androidx.navigation.NavController @@ -23,10 +24,13 @@ import com.google.accompanist.pager.ExperimentalPagerApi import com.google.accompanist.pager.HorizontalPager import com.google.accompanist.pager.pagerTabIndicatorOffset import com.google.accompanist.pager.rememberPagerState +import com.vitorpamplona.amethyst.NotificationCache import com.vitorpamplona.amethyst.service.NostrHomeDataSource import com.vitorpamplona.amethyst.service.NostrUserProfileDataSource import com.vitorpamplona.amethyst.service.NostrUserProfileFollowersDataSource import com.vitorpamplona.amethyst.service.NostrUserProfileFollowsDataSource +import com.vitorpamplona.amethyst.ui.navigation.Route +import com.vitorpamplona.amethyst.ui.navigation.Routes import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel import java.lang.System.currentTimeMillis import kotlinx.coroutines.launch @@ -75,8 +79,8 @@ fun HomeScreen(accountViewModel: AccountViewModel, navController: NavController) } HorizontalPager(count = 2, state = pagerState) { when (pagerState.currentPage) { - 0 -> FeedView(feedViewModel, accountViewModel, navController) - 1 -> FeedView(feedViewModelReplies, accountViewModel, navController) + 0 -> FeedView(feedViewModel, accountViewModel, navController, Route.Home.route + "Follows") + 1 -> FeedView(feedViewModelReplies, accountViewModel, navController, Route.Home.route + "FollowsReplies") } } } 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 ceff271d11..f122e7e935 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 @@ -12,6 +12,7 @@ import androidx.compose.ui.unit.dp import androidx.lifecycle.viewmodel.compose.viewModel import androidx.navigation.NavController import com.vitorpamplona.amethyst.service.NostrNotificationDataSource +import com.vitorpamplona.amethyst.ui.navigation.Route import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel @Composable @@ -26,7 +27,7 @@ fun NotificationScreen(accountViewModel: AccountViewModel, navController: NavCon Column( modifier = Modifier.padding(vertical = 0.dp) ) { - CardFeedView(feedViewModel, accountViewModel = accountViewModel, navController) + CardFeedView(feedViewModel, accountViewModel = accountViewModel, navController, Route.Notification.route) } } } \ No newline at end of file diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/ProfileScreen.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/ProfileScreen.kt index 86566cb38a..53ce06c722 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/ProfileScreen.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/ProfileScreen.kt @@ -248,7 +248,7 @@ fun TabNotes(user: User, accountViewModel: AccountViewModel, navController: NavC Column( modifier = Modifier.padding(vertical = 0.dp) ) { - FeedView(feedViewModel, accountViewModel, navController) + FeedView(feedViewModel, accountViewModel, navController, null) } } } 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 2d4503f0bc..07d7f9da0a 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 @@ -74,7 +74,7 @@ fun SearchScreen(accountViewModel: AccountViewModel, navController: NavControlle modifier = Modifier.padding(vertical = 0.dp) ) { SearchBar(accountViewModel, navController) - FeedView(feedViewModel, accountViewModel, navController) + FeedView(feedViewModel, accountViewModel, navController, null) } } } @@ -186,6 +186,7 @@ private fun SearchBar(accountViewModel: AccountViewModel, navController: NavCont }, channelLastTime = null, channelLastContent = item.info.about, + false, onClick = { navController.navigate("Channel/${item.idHex}") }) } diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/theme/Color.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/theme/Color.kt index 0e5e6ca79c..96ca6bb85c 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/theme/Color.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/theme/Color.kt @@ -7,6 +7,6 @@ val Purple500 = Color(0xFF6200EE) val Purple700 = Color(0xFF3700B3) val Teal200 = Color(0xFF03DAC5) -val Following = Color(0xFF2CC03D) +val Following = Color(0xFF03DAC5) val FollowsFollow = Color.Yellow val NIP05Verified = Color.Blue