pull/3/head
Vitor Pamplona 2023-01-12 12:47:31 -05:00
rodzic 1d24cb411c
commit 45a7a18ea7
19 zmienionych plików z 455 dodań i 39 usunięć

Wyświetl plik

@ -4,6 +4,9 @@ import androidx.lifecycle.LiveData
import com.vitorpamplona.amethyst.service.NostrSingleEventDataSource
import com.vitorpamplona.amethyst.ui.note.toDisplayHex
import fr.acinq.secp256k1.Hex
import java.time.Instant
import java.time.ZoneId
import java.time.format.DateTimeFormatter
import java.util.Collections
import nostr.postr.events.Event
@ -29,6 +32,33 @@ class Note(val idHex: String) {
refreshObservers()
}
fun formattedDateTime(timestamp: Long): String {
return Instant.ofEpochSecond(timestamp).atZone(ZoneId.systemDefault())
.format(DateTimeFormatter.ofPattern("uuuu-MM-dd-HH:mm:ss"))
}
fun replyLevelSignature(): String {
val replyTo = replyTo
if (replyTo == null || replyTo.isEmpty()) {
return "/" + formattedDateTime(event?.createdAt ?: 0) + ";"
}
return replyTo
.map { it.replyLevelSignature() }
.maxBy { it.length }.removeSuffix(";") + "/" + formattedDateTime(event?.createdAt ?: 0) + ";"
}
fun replyLevel(): Int {
val replyTo = replyTo
if (replyTo == null || replyTo.isEmpty()) {
return 0
}
return replyTo.maxOf {
it.replyLevel()
} + 1
}
fun addReply(note: Note) {
if (replies.add(note))
refreshObservers()

Wyświetl plik

@ -21,18 +21,26 @@ object NostrSingleEventDataSource: NostrDataSource("SingleEventFeed") {
}
fun createLoadEventsIfNotLoadedFilter(): JsonFilter? {
val eventsToLoad = eventsToWatch
.map { LocalCache.notes[it] }
.filterNotNull()
val directEventsToLoad = eventsToWatch
.mapNotNull { LocalCache.notes[it] }
.filter { it.event == null }
val threadingEventsToLoad = eventsToWatch
.mapNotNull { LocalCache.notes[it] }
.mapNotNull { it.replyTo }
.flatten()
.filter { it.event == null }
val interestedEvents =
(directEventsToLoad + threadingEventsToLoad)
.map { it.idHex.substring(0, 8) }
if (eventsToLoad.isEmpty()) {
if (interestedEvents.isEmpty()) {
return null
}
return JsonFilter(
ids = eventsToLoad
ids = interestedEvents
)
}

Wyświetl plik

@ -0,0 +1,82 @@
package com.vitorpamplona.amethyst.service
import com.vitorpamplona.amethyst.model.LocalCache
import com.vitorpamplona.amethyst.model.Note
import java.util.Collections
import nostr.postr.JsonFilter
object NostrThreadDataSource: NostrDataSource("SingleThreadFeed") {
val eventsToWatch = Collections.synchronizedList(mutableListOf<String>())
fun createRepliesAndReactionsFilter(): JsonFilter? {
val reactionsToWatch = eventsToWatch.map { it.substring(0, 8) }
if (reactionsToWatch.isEmpty()) {
return null
}
return JsonFilter(
tags = mapOf("e" to reactionsToWatch)
)
}
fun createLoadEventsIfNotLoadedFilter(): JsonFilter? {
val eventsToLoad = eventsToWatch
.map { LocalCache.notes[it] }
.filterNotNull()
.filter { it.event == null }
.map { it.idHex.substring(0, 8) }
if (eventsToLoad.isEmpty()) {
return null
}
return JsonFilter(
ids = eventsToLoad
)
}
val repliesAndReactionsChannel = requestNewChannel()
val loadEventsChannel = requestNewChannel()
override fun feed(): List<Note> {
return eventsToWatch.map {
LocalCache.notes[it]
}.filterNotNull()
}
override fun updateChannelFilters() {
repliesAndReactionsChannel.filter = createRepliesAndReactionsFilter()
loadEventsChannel.filter = createLoadEventsIfNotLoadedFilter()
}
fun loadThread(noteId: String) {
val note = LocalCache.notes[noteId] ?: return
val thread = mutableListOf<Note>()
val threadSet = mutableSetOf<Note>()
val threadRoot = note.replyTo?.firstOrNull() ?: note
loadDown(threadRoot, thread, threadSet)
// Currently orders by date of each event, descending, at each level of the reply stack
val order = compareByDescending<Note> { it.replyLevelSignature() }
eventsToWatch.clear()
eventsToWatch.addAll(thread.sortedWith(order).map { it.idHex })
resetFilters()
}
fun loadDown(note: Note, thread: MutableList<Note>, threadSet: MutableSet<Note>) {
if (note !in threadSet) {
thread.add(note)
threadSet.add(note)
note.replies.forEach {
loadDown(it, thread, threadSet)
}
}
}
}

Wyświetl plik

@ -15,6 +15,7 @@ import com.vitorpamplona.amethyst.service.NostrHomeDataSource
import com.vitorpamplona.amethyst.service.NostrNotificationDataSource
import com.vitorpamplona.amethyst.service.NostrSingleEventDataSource
import com.vitorpamplona.amethyst.service.NostrSingleUserDataSource
import com.vitorpamplona.amethyst.service.NostrThreadDataSource
import com.vitorpamplona.amethyst.service.relays.Client
import com.vitorpamplona.amethyst.ui.screen.AccountScreen
import com.vitorpamplona.amethyst.ui.screen.AccountStateViewModel
@ -54,6 +55,7 @@ class MainActivity : ComponentActivity() {
NostrNotificationDataSource.stop()
NostrSingleEventDataSource.stop()
NostrSingleUserDataSource.stop()
NostrThreadDataSource.stop()
Client.disconnect()
super.onPause()
}

Wyświetl plik

@ -4,6 +4,7 @@ import androidx.compose.runtime.Composable
import androidx.navigation.NavHostController
import androidx.navigation.compose.NavHost
import androidx.navigation.compose.composable
import androidx.navigation.compose.rememberNavController
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
@Composable
@ -13,7 +14,7 @@ fun AppNavigation(
) {
NavHost(navController, startDestination = Route.Home.route) {
Routes.forEach {
composable(it.route, content = it.buildScreen(accountViewModel))
composable(it.route, it.arguments, content = it.buildScreen(accountViewModel, navController))
}
}
}

Wyświetl plik

@ -2,12 +2,17 @@ package com.vitorpamplona.amethyst.ui.navigation
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.navigation.NamedNavArgument
import androidx.navigation.NavBackStackEntry
import androidx.navigation.NavController
import androidx.navigation.NavHostController
import androidx.navigation.NavType
import androidx.navigation.compose.currentBackStackEntryAsState
import androidx.navigation.navArgument
import com.vitorpamplona.amethyst.R
import com.vitorpamplona.amethyst.ui.screen.HomeScreen
import com.vitorpamplona.amethyst.ui.screen.MessageScreen
import com.vitorpamplona.amethyst.ui.screen.ThreadScreen
import com.vitorpamplona.amethyst.ui.screen.NotificationScreen
import com.vitorpamplona.amethyst.ui.screen.ProfileScreen
import com.vitorpamplona.amethyst.ui.screen.SearchScreen
@ -17,17 +22,23 @@ import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
sealed class Route(
val route: String,
val icon: Int,
val buildScreen: (AccountViewModel) -> @Composable (NavBackStackEntry) -> Unit
val arguments: List<NamedNavArgument> = emptyList(),
val buildScreen: (AccountViewModel, NavController) -> @Composable (NavBackStackEntry) -> Unit
) {
object Home : Route("Home", R.drawable.ic_home, { acc -> { _ -> HomeScreen(acc) } })
object Search : Route("Search", R.drawable.ic_search, { acc -> { _ -> SearchScreen(acc) }})
object Notification : Route("Notification", R.drawable.ic_notifications, { acc -> { _ -> NotificationScreen(acc) }})
object Message : Route("Message", R.drawable.ic_dm, { acc -> { _ -> MessageScreen(acc) }})
object Profile : Route("Profile", R.drawable.ic_profile, { acc -> { _ -> ProfileScreen(acc) }})
object Lists : Route("Lists", R.drawable.ic_lists, { acc -> { _ -> ProfileScreen(acc) }})
object Topics : Route("Topics", R.drawable.ic_topics, { acc -> { _ -> ProfileScreen(acc) }})
object Bookmarks : Route("Bookmarks", R.drawable.ic_bookmarks, { acc -> { _ -> ProfileScreen(acc) }})
object Moments : Route("Moments", R.drawable.ic_moments, { acc -> { _ -> ProfileScreen(acc) }})
object Home : Route("Home", R.drawable.ic_home, buildScreen = { acc, nav -> { _ -> HomeScreen(acc, nav) } })
object Search : Route("Search", R.drawable.ic_search, buildScreen = { acc, nav -> { _ -> SearchScreen(acc, nav) }})
object Notification : Route("Notification", R.drawable.ic_notifications,buildScreen = { acc, nav -> { _ -> NotificationScreen(acc, nav) }})
object Message : Route("Message", R.drawable.ic_dm, buildScreen = { acc, nav -> { _ -> MessageScreen(acc) }})
object Profile : Route("Profile", R.drawable.ic_profile, buildScreen = { acc, nav -> { _ -> ProfileScreen(acc) }})
object Lists : Route("Lists", R.drawable.ic_lists, buildScreen = { acc, nav -> { _ -> ProfileScreen(acc) }})
object Topics : Route("Topics", R.drawable.ic_topics, buildScreen = { acc, nav -> { _ -> ProfileScreen(acc) }})
object Bookmarks : Route("Bookmarks", R.drawable.ic_bookmarks, buildScreen = { acc, nav -> { _ -> ProfileScreen(acc) }})
object Moments : Route("Moments", R.drawable.ic_moments, buildScreen = { acc, nav -> { _ -> ProfileScreen(acc) }})
object Note : Route("Note/{id}", R.drawable.ic_moments,
arguments = listOf(navArgument("id") { type = NavType.StringType } ),
buildScreen = { acc, nav -> { ThreadScreen(it.arguments?.getString("id"), acc, nav) }}
)
}
val Routes = listOf(
@ -42,7 +53,10 @@ val Routes = listOf(
Route.Lists,
Route.Topics,
Route.Bookmarks,
Route.Moments
Route.Moments,
//inner
Route.Note
)
@Composable

Wyświetl plik

@ -12,7 +12,7 @@ import androidx.compose.ui.graphics.Color
import androidx.compose.ui.unit.dp
@Composable
fun BlankNote(modifier: Modifier = Modifier, isQuote: Boolean) {
fun BlankNote(modifier: Modifier = Modifier, isQuote: Boolean = false) {
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)) {

Wyświetl plik

@ -26,6 +26,7 @@ import androidx.compose.ui.graphics.Color
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp
import androidx.navigation.NavController
import coil.compose.AsyncImage
import com.google.accompanist.flowlayout.FlowRow
import com.vitorpamplona.amethyst.R
@ -39,7 +40,7 @@ import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
import nostr.postr.events.TextNoteEvent
@Composable
fun BoostSetCompose(likeSetCard: BoostSetCard, modifier: Modifier = Modifier, isInnerNote: Boolean = false, accountViewModel: AccountViewModel) {
fun BoostSetCompose(likeSetCard: BoostSetCard, modifier: Modifier = Modifier, isInnerNote: Boolean = false, accountViewModel: AccountViewModel, navController: NavController) {
val noteState by likeSetCard.note.live.observeAsState()
val note = noteState?.note
@ -84,7 +85,7 @@ fun BoostSetCompose(likeSetCard: BoostSetCard, modifier: Modifier = Modifier, is
}
}
NoteCompose(note, modifier = Modifier.padding(top = 5.dp), isInnerNote = true, accountViewModel = accountViewModel)
NoteCompose(note, Modifier.padding(top = 5.dp), true, accountViewModel, navController)
}
}
}

Wyświetl plik

@ -26,6 +26,7 @@ import androidx.compose.ui.graphics.Color
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp
import androidx.navigation.NavController
import coil.compose.AsyncImage
import com.google.accompanist.flowlayout.FlowRow
import com.vitorpamplona.amethyst.R
@ -38,7 +39,7 @@ import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
import nostr.postr.events.TextNoteEvent
@Composable
fun LikeSetCompose(likeSetCard: LikeSetCard, modifier: Modifier = Modifier, isInnerNote: Boolean = false, accountViewModel: AccountViewModel) {
fun LikeSetCompose(likeSetCard: LikeSetCard, modifier: Modifier = Modifier, isInnerNote: Boolean = false, accountViewModel: AccountViewModel, navController: NavController) {
val noteState by likeSetCard.note.live.observeAsState()
val note = noteState?.note
@ -83,7 +84,7 @@ fun LikeSetCompose(likeSetCard: LikeSetCard, modifier: Modifier = Modifier, isIn
}
}
NoteCompose(note, modifier = Modifier.padding(top = 5.dp), isInnerNote = true, accountViewModel = accountViewModel)
NoteCompose(note, Modifier.padding(top = 5.dp), true, accountViewModel, navController)
}
}
}

Wyświetl plik

@ -4,6 +4,7 @@ import android.text.format.DateUtils
import android.text.format.DateUtils.getRelativeTimeSpanString
import androidx.compose.foundation.background
import androidx.compose.foundation.border
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
@ -21,6 +22,8 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp
import androidx.navigation.NavController
import androidx.navigation.compose.rememberNavController
import coil.compose.AsyncImage
import com.vitorpamplona.amethyst.model.Note
import com.vitorpamplona.amethyst.service.model.ReactionEvent
@ -30,7 +33,7 @@ import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
import nostr.postr.events.TextNoteEvent
@Composable
fun NoteCompose(baseNote: Note, modifier: Modifier = Modifier, isInnerNote: Boolean = false, accountViewModel: AccountViewModel) {
fun NoteCompose(baseNote: Note, modifier: Modifier = Modifier, isInnerNote: Boolean = false, accountViewModel: AccountViewModel, navController: NavController) {
val noteState by baseNote.live.observeAsState()
val note = noteState?.note
@ -41,7 +44,14 @@ fun NoteCompose(baseNote: Note, modifier: Modifier = Modifier, isInnerNote: Bool
val author = authorState?.user
Column(modifier = modifier) {
Row(modifier = Modifier.padding(horizontal = if (!isInnerNote) 12.dp else 0.dp)) {
Row(
modifier = Modifier
.padding(
start = if (!isInnerNote) 12.dp else 0.dp,
end = if (!isInnerNote) 12.dp else 0.dp,
top = 10.dp)
.clickable ( onClick = { navController.navigate("Note/${note.idHex}") } )
) {
// Draws the boosted picture outside the boosted card.
if (!isInnerNote) {
@ -100,7 +110,8 @@ fun NoteCompose(baseNote: Note, modifier: Modifier = Modifier, isInnerNote: Bool
note,
modifier = Modifier.padding(top = 5.dp),
isInnerNote = true,
accountViewModel = accountViewModel
accountViewModel = accountViewModel,
navController = navController
)
}
@ -121,7 +132,7 @@ fun NoteCompose(baseNote: Note, modifier: Modifier = Modifier, isInnerNote: Bool
ReactionsRowState(note, accountViewModel)
Divider(
modifier = Modifier.padding(vertical = 10.dp),
modifier = Modifier.padding(top = 10.dp),
thickness = 0.25.dp
)
}

Wyświetl plik

@ -10,6 +10,7 @@ import com.vitorpamplona.amethyst.service.NostrHomeDataSource
import com.vitorpamplona.amethyst.service.NostrNotificationDataSource
import com.vitorpamplona.amethyst.service.NostrSingleEventDataSource
import com.vitorpamplona.amethyst.service.NostrSingleUserDataSource
import com.vitorpamplona.amethyst.service.NostrThreadDataSource
import fr.acinq.secp256k1.Hex
import java.util.regex.Pattern
import kotlinx.coroutines.flow.MutableStateFlow
@ -65,6 +66,7 @@ class AccountStateViewModel(private val encryptedPreferences: EncryptedSharedPre
NostrNotificationDataSource.start()
NostrSingleEventDataSource.start()
NostrSingleUserDataSource.start()
NostrThreadDataSource.start()
}
fun newKey() {

Wyświetl plik

@ -23,6 +23,7 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import androidx.lifecycle.compose.ExperimentalLifecycleComposeApi
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.ui.note.BoostSetCompose
@ -32,7 +33,7 @@ import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
@OptIn(ExperimentalLifecycleComposeApi::class)
@Composable
fun CardFeedView(viewModel: CardFeedViewModel, accountViewModel: AccountViewModel) {
fun CardFeedView(viewModel: CardFeedViewModel, accountViewModel: AccountViewModel, navController: NavController) {
val feedState by viewModel.feedContent.collectAsStateWithLifecycle()
var isRefreshing by remember { mutableStateOf(false) }
@ -76,9 +77,9 @@ fun CardFeedView(viewModel: CardFeedViewModel, accountViewModel: AccountViewMode
) {
itemsIndexed(state.feed) { index, item ->
when (item) {
is NoteCard -> NoteCompose(item.note, isInnerNote = false, accountViewModel = accountViewModel)
is LikeSetCard -> LikeSetCompose(item, isInnerNote = false, accountViewModel = accountViewModel)
is BoostSetCard -> BoostSetCompose(item, isInnerNote = false, accountViewModel = accountViewModel)
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)
}
}
}

Wyświetl plik

@ -23,6 +23,7 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import androidx.lifecycle.compose.ExperimentalLifecycleComposeApi
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.ui.note.NoteCompose
@ -30,7 +31,7 @@ import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
@OptIn(ExperimentalLifecycleComposeApi::class)
@Composable
fun FeedView(viewModel: FeedViewModel, accountViewModel: AccountViewModel) {
fun FeedView(viewModel: FeedViewModel, accountViewModel: AccountViewModel, navController: NavController) {
val feedState by viewModel.feedContent.collectAsStateWithLifecycle()
var isRefreshing by remember { mutableStateOf(false) }
@ -73,7 +74,7 @@ fun FeedView(viewModel: FeedViewModel, accountViewModel: AccountViewModel) {
state = listState
) {
itemsIndexed(state.feed, key = { _, item -> item.idHex }) { index, item ->
NoteCompose(item, isInnerNote = false, accountViewModel = accountViewModel)
NoteCompose(item, isInnerNote = false, accountViewModel = accountViewModel, navController = navController)
}
}
}

Wyświetl plik

@ -6,7 +6,6 @@ import com.vitorpamplona.amethyst.model.LocalCache
import com.vitorpamplona.amethyst.model.LocalCacheState
import com.vitorpamplona.amethyst.model.Note
import com.vitorpamplona.amethyst.service.NostrDataSource
import com.vitorpamplona.amethyst.service.model.ReactionEvent
import kotlinx.coroutines.cancel
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asStateFlow

Wyświetl plik

@ -0,0 +1,227 @@
package com.vitorpamplona.amethyst.ui.screen
import android.content.res.Resources.Theme
import androidx.compose.animation.Crossfade
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.Column
import androidx.compose.foundation.layout.IntrinsicSize
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxHeight
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.lazy.LazyColumn
import androidx.compose.foundation.lazy.itemsIndexed
import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.foundation.shape.GenericShape
import androidx.compose.material.Button
import androidx.compose.material.Divider
import androidx.compose.material.MaterialTheme
import androidx.compose.material.OutlinedButton
import androidx.compose.material.Text
import androidx.compose.material.rememberScaffoldState
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.livedata.observeAsState
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.draw.drawBehind
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.Shape
import androidx.compose.ui.graphics.drawscope.DrawScope
import androidx.compose.ui.platform.debugInspectorInfo
import androidx.compose.ui.unit.dp
import androidx.lifecycle.compose.ExperimentalLifecycleComposeApi
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import androidx.navigation.NavController
import coil.compose.AsyncImage
import com.google.accompanist.swiperefresh.SwipeRefresh
import com.google.accompanist.swiperefresh.rememberSwipeRefreshState
import com.vitorpamplona.amethyst.model.LocalCache
import com.vitorpamplona.amethyst.model.Note
import com.vitorpamplona.amethyst.ui.components.RichTextViewer
import com.vitorpamplona.amethyst.ui.note.BlankNote
import com.vitorpamplona.amethyst.ui.note.NoteCompose
import com.vitorpamplona.amethyst.ui.note.ReactionsRowState
import com.vitorpamplona.amethyst.ui.note.UserDisplay
import com.vitorpamplona.amethyst.ui.note.timeAgo
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
import kotlinx.coroutines.launch
@OptIn(ExperimentalLifecycleComposeApi::class)
@Composable
fun ThreadFeedView(noteId: String, viewModel: FeedViewModel, accountViewModel: AccountViewModel, navController: NavController) {
val feedState by viewModel.feedContent.collectAsStateWithLifecycle()
var isRefreshing by remember { mutableStateOf(false) }
val swipeRefreshState = rememberSwipeRefreshState(isRefreshing)
val listState = rememberLazyListState()
LaunchedEffect(isRefreshing) {
if (isRefreshing) {
viewModel.refresh()
isRefreshing = false
}
}
SwipeRefresh(
state = swipeRefreshState,
onRefresh = {
isRefreshing = true
},
) {
Column() {
Crossfade(targetState = feedState) { state ->
when (state) {
is FeedState.Empty -> {
FeedEmpty {
isRefreshing = true
}
}
is FeedState.FeedError -> {
FeedError(state.errorMessage) {
isRefreshing = true
}
}
is FeedState.Loaded -> {
var noteIdPositionInThread by remember { mutableStateOf(0) }
// only in the first transition
LaunchedEffect(noteIdPositionInThread) {
listState.animateScrollToItem(noteIdPositionInThread, 0)
}
val notePosition = state.feed.filter { it.idHex == noteId}.firstOrNull()
if (notePosition != null) {
noteIdPositionInThread = state.feed.indexOf(notePosition)
}
LazyColumn(
contentPadding = PaddingValues(
top = 10.dp,
bottom = 10.dp
),
state = listState
) {
itemsIndexed(state.feed, key = { _, item -> item.idHex }) { index, item ->
if (index == 0)
NoteMaster(item, accountViewModel = accountViewModel, navController = navController)
else {
Column() {
Row() {
NoteCompose(
item,
Modifier.drawReplyLevel(item.replyLevel(), MaterialTheme.colors.onSurface.copy(alpha = 0.32f)),
isInnerNote = false,
accountViewModel = accountViewModel,
navController = navController,
)
}
}
}
}
}
}
FeedState.Loading -> {
LoadingFeed()
}
}
}
}
}
}
// Creates a Zebra pattern where each bar is a reply level.
fun Modifier.drawReplyLevel(level: Int, color: Color): Modifier = this
.drawBehind {
val paddingDp = 2
val strokeWidthDp = 2
val levelWidthDp = strokeWidthDp + 1
val padding = paddingDp.dp.toPx()
val strokeWidth = strokeWidthDp.dp.toPx()
val levelWidth = levelWidthDp.dp.toPx()
repeat(level) {
this.drawLine(
color,
Offset(padding + it * levelWidth, 0f),
Offset(padding + it * levelWidth, size.height),
strokeWidth = strokeWidth
)
}
return@drawBehind
}
.padding(start = (2 + (level * 3)).dp)
@Composable
fun NoteMaster(baseNote: Note, accountViewModel: AccountViewModel, navController: NavController) {
val noteState by baseNote.live.observeAsState()
val note = noteState?.note
if (note?.event == null) {
BlankNote()
} else {
val authorState by note.author!!.live.observeAsState()
val author = authorState?.user
Column(
Modifier
.fillMaxWidth()
.padding(top = 10.dp)) {
Row(modifier = Modifier.padding(start = 12.dp, end = 12.dp)) {
// Draws the boosted picture outside the boosted card.
AsyncImage(
model = author?.profilePicture(),
contentDescription = "Profile Image",
modifier = Modifier
.width(55.dp)
.clip(shape = CircleShape)
)
Column(modifier = Modifier.padding(start = 10.dp)) {
Row(verticalAlignment = Alignment.CenterVertically) {
if (author != null)
UserDisplay(author)
}
Row(verticalAlignment = Alignment.CenterVertically) {
Text(
timeAgo(note.event?.createdAt),
color = MaterialTheme.colors.onSurface.copy(alpha = 0.32f)
)
}
}
}
Row(modifier = Modifier.padding(horizontal = 12.dp)) {
Column() {
val eventContent = note.event?.content
if (eventContent != null)
RichTextViewer(eventContent, note.event?.tags)
ReactionsRowState(note, accountViewModel)
Divider(
modifier = Modifier.padding(top = 10.dp),
thickness = 0.25.dp
)
}
}
}
}
}

Wyświetl plik

@ -10,12 +10,13 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import androidx.lifecycle.compose.ExperimentalLifecycleComposeApi
import androidx.lifecycle.viewmodel.compose.viewModel
import androidx.navigation.NavController
import com.vitorpamplona.amethyst.service.NostrHomeDataSource
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
@OptIn(ExperimentalLifecycleComposeApi::class)
@Composable
fun HomeScreen(accountViewModel: AccountViewModel) {
fun HomeScreen(accountViewModel: AccountViewModel, navController: NavController) {
val account by accountViewModel.accountLiveData.observeAsState()
if (account != null) {
@ -25,7 +26,7 @@ fun HomeScreen(accountViewModel: AccountViewModel) {
Column(
modifier = Modifier.padding(vertical = 0.dp)
) {
FeedView(feedViewModel, accountViewModel)
FeedView(feedViewModel, accountViewModel, navController)
}
}
}

Wyświetl plik

@ -10,12 +10,13 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import androidx.lifecycle.compose.ExperimentalLifecycleComposeApi
import androidx.lifecycle.viewmodel.compose.viewModel
import androidx.navigation.NavController
import com.vitorpamplona.amethyst.service.NostrNotificationDataSource
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
@OptIn(ExperimentalLifecycleComposeApi::class)
@Composable
fun NotificationScreen(accountViewModel: AccountViewModel) {
fun NotificationScreen(accountViewModel: AccountViewModel, navController: NavController) {
val account by accountViewModel.accountLiveData.observeAsState()
if (account != null) {
@ -26,7 +27,7 @@ fun NotificationScreen(accountViewModel: AccountViewModel) {
Column(
modifier = Modifier.padding(vertical = 0.dp)
) {
CardFeedView(feedViewModel, accountViewModel = accountViewModel)
CardFeedView(feedViewModel, accountViewModel = accountViewModel, navController)
}
}
}

Wyświetl plik

@ -8,19 +8,20 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import androidx.lifecycle.compose.ExperimentalLifecycleComposeApi
import androidx.lifecycle.viewmodel.compose.viewModel
import androidx.navigation.NavController
import com.vitorpamplona.amethyst.service.NostrGlobalDataSource
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
@OptIn(ExperimentalLifecycleComposeApi::class)
@Composable
fun SearchScreen(accountViewModel: AccountViewModel) {
fun SearchScreen(accountViewModel: AccountViewModel, navController: NavController) {
val feedViewModel: FeedViewModel = viewModel { FeedViewModel( NostrGlobalDataSource ) }
Column(Modifier.fillMaxHeight()) {
Column(
modifier = Modifier.padding(vertical = 0.dp)
) {
FeedView(feedViewModel, accountViewModel)
FeedView(feedViewModel, accountViewModel, navController)
}
}
}

Wyświetl plik

@ -0,0 +1,33 @@
package com.vitorpamplona.amethyst.ui.screen
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxHeight
import androidx.compose.foundation.layout.padding
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.livedata.observeAsState
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import androidx.lifecycle.viewmodel.compose.viewModel
import androidx.navigation.NavController
import com.vitorpamplona.amethyst.service.NostrThreadDataSource
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
@Composable
fun ThreadScreen(noteId: String?, accountViewModel: AccountViewModel, navController: NavController) {
val account by accountViewModel.accountLiveData.observeAsState()
if (account != null && noteId != null) {
NostrThreadDataSource.loadThread(noteId)
val feedViewModel: FeedViewModel = viewModel { FeedViewModel( NostrThreadDataSource ) }
Column(Modifier.fillMaxHeight()) {
Column(
modifier = Modifier.padding(vertical = 0.dp)
) {
ThreadFeedView(noteId, feedViewModel, accountViewModel, navController)
}
}
}
}