Restructuring ViewModels
This commit is contained in:
@@ -39,14 +39,14 @@ object NotificationCache {
|
|||||||
|
|
||||||
class NotificationLiveData(val cache: NotificationCache) : LiveData<NotificationState>(NotificationState(cache)) {
|
class NotificationLiveData(val cache: NotificationCache) : LiveData<NotificationState>(NotificationState(cache)) {
|
||||||
// Refreshes observers in batches.
|
// Refreshes observers in batches.
|
||||||
private val bundler = BundledUpdate(300, Dispatchers.IO) {
|
private val bundler = BundledUpdate(300, Dispatchers.IO)
|
||||||
if (hasActiveObservers()) {
|
|
||||||
postValue(NotificationState(cache))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun invalidateData() {
|
fun invalidateData() {
|
||||||
bundler.invalidate()
|
bundler.invalidate() {
|
||||||
|
if (hasActiveObservers()) {
|
||||||
|
postValue(NotificationState(cache))
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1166,14 +1166,14 @@ class Account(
|
|||||||
|
|
||||||
class AccountLiveData(private val account: Account) : LiveData<AccountState>(AccountState(account)) {
|
class AccountLiveData(private val account: Account) : LiveData<AccountState>(AccountState(account)) {
|
||||||
// Refreshes observers in batches.
|
// Refreshes observers in batches.
|
||||||
private val bundler = BundledUpdate(300, Dispatchers.Default) {
|
private val bundler = BundledUpdate(300, Dispatchers.Default)
|
||||||
if (hasActiveObservers()) {
|
|
||||||
refresh()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun invalidateData() {
|
fun invalidateData() {
|
||||||
bundler.invalidate()
|
bundler.invalidate() {
|
||||||
|
if (hasActiveObservers()) {
|
||||||
|
refresh()
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun refresh() {
|
fun refresh() {
|
||||||
|
|||||||
@@ -55,14 +55,14 @@ class AntiSpamFilter {
|
|||||||
class AntiSpamLiveData(val cache: AntiSpamFilter) : LiveData<AntiSpamState>(AntiSpamState(cache)) {
|
class AntiSpamLiveData(val cache: AntiSpamFilter) : LiveData<AntiSpamState>(AntiSpamState(cache)) {
|
||||||
|
|
||||||
// Refreshes observers in batches.
|
// Refreshes observers in batches.
|
||||||
private val bundler = BundledUpdate(300, Dispatchers.IO) {
|
private val bundler = BundledUpdate(300, Dispatchers.IO)
|
||||||
if (hasActiveObservers()) {
|
|
||||||
postValue(AntiSpamState(cache))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun invalidateData() {
|
fun invalidateData() {
|
||||||
bundler.invalidate()
|
bundler.invalidate() {
|
||||||
|
if (hasActiveObservers()) {
|
||||||
|
postValue(AntiSpamState(cache))
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -74,14 +74,14 @@ class Channel(val idHex: String) {
|
|||||||
|
|
||||||
class ChannelLiveData(val channel: Channel) : LiveData<ChannelState>(ChannelState(channel)) {
|
class ChannelLiveData(val channel: Channel) : LiveData<ChannelState>(ChannelState(channel)) {
|
||||||
// Refreshes observers in batches.
|
// Refreshes observers in batches.
|
||||||
private val bundler = BundledUpdate(300, Dispatchers.IO) {
|
private val bundler = BundledUpdate(300, Dispatchers.IO)
|
||||||
if (hasActiveObservers()) {
|
|
||||||
postValue(ChannelState(channel))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun invalidateData() {
|
fun invalidateData() {
|
||||||
bundler.invalidate()
|
bundler.invalidate() {
|
||||||
|
if (hasActiveObservers()) {
|
||||||
|
postValue(ChannelState(channel))
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onActive() {
|
override fun onActive() {
|
||||||
|
|||||||
@@ -414,14 +414,14 @@ class NoteLiveSet(u: Note) {
|
|||||||
|
|
||||||
class NoteLiveData(val note: Note) : LiveData<NoteState>(NoteState(note)) {
|
class NoteLiveData(val note: Note) : LiveData<NoteState>(NoteState(note)) {
|
||||||
// Refreshes observers in batches.
|
// Refreshes observers in batches.
|
||||||
private val bundler = BundledUpdate(300, Dispatchers.IO) {
|
private val bundler = BundledUpdate(300, Dispatchers.IO)
|
||||||
// if (hasObservers()) {
|
|
||||||
postValue(NoteState(note))
|
|
||||||
// }
|
|
||||||
}
|
|
||||||
|
|
||||||
fun invalidateData() {
|
fun invalidateData() {
|
||||||
bundler.invalidate()
|
bundler.invalidate() {
|
||||||
|
// if (hasObservers()) {
|
||||||
|
postValue(NoteState(note))
|
||||||
|
// }
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onActive() {
|
override fun onActive() {
|
||||||
|
|||||||
@@ -400,14 +400,14 @@ class UserMetadata {
|
|||||||
|
|
||||||
class UserLiveData(val user: User) : LiveData<UserState>(UserState(user)) {
|
class UserLiveData(val user: User) : LiveData<UserState>(UserState(user)) {
|
||||||
// Refreshes observers in batches.
|
// Refreshes observers in batches.
|
||||||
private val bundler = BundledUpdate(300, Dispatchers.IO) {
|
private val bundler = BundledUpdate(300, Dispatchers.IO)
|
||||||
if (hasActiveObservers()) {
|
|
||||||
postValue(UserState(user))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun invalidateData() {
|
fun invalidateData() {
|
||||||
bundler.invalidate()
|
bundler.invalidate() {
|
||||||
|
if (hasActiveObservers()) {
|
||||||
|
postValue(UserState(user))
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onActive() {
|
override fun onActive() {
|
||||||
|
|||||||
@@ -103,16 +103,16 @@ abstract class NostrDataSource(val debugName: String) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Refreshes observers in batches.
|
// Refreshes observers in batches.
|
||||||
private val bundler = BundledUpdate(300, Dispatchers.IO) {
|
private val bundler = BundledUpdate(300, Dispatchers.IO)
|
||||||
// println("DataSource: ${this.javaClass.simpleName} InvalidateFilters")
|
|
||||||
|
|
||||||
// adds the time to perform the refresh into this delay
|
|
||||||
// holding off new updates in case of heavy refresh routines.
|
|
||||||
resetFiltersSuspend()
|
|
||||||
}
|
|
||||||
|
|
||||||
fun invalidateFilters() {
|
fun invalidateFilters() {
|
||||||
bundler.invalidate()
|
bundler.invalidate() {
|
||||||
|
// println("DataSource: ${this.javaClass.simpleName} InvalidateFilters")
|
||||||
|
|
||||||
|
// adds the time to perform the refresh into this delay
|
||||||
|
// holding off new updates in case of heavy refresh routines.
|
||||||
|
resetFiltersSuspend()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun resetFilters() {
|
fun resetFilters() {
|
||||||
|
|||||||
@@ -49,18 +49,18 @@ import androidx.compose.ui.window.Dialog
|
|||||||
import androidx.compose.ui.window.DialogProperties
|
import androidx.compose.ui.window.DialogProperties
|
||||||
import androidx.lifecycle.viewmodel.compose.viewModel
|
import androidx.lifecycle.viewmodel.compose.viewModel
|
||||||
import com.vitorpamplona.amethyst.R
|
import com.vitorpamplona.amethyst.R
|
||||||
import com.vitorpamplona.amethyst.model.Account
|
|
||||||
import com.vitorpamplona.amethyst.model.RelaySetupInfo
|
import com.vitorpamplona.amethyst.model.RelaySetupInfo
|
||||||
import com.vitorpamplona.amethyst.service.relays.FeedType
|
import com.vitorpamplona.amethyst.service.relays.FeedType
|
||||||
|
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
||||||
import java.lang.Math.round
|
import java.lang.Math.round
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun NewRelayListView(onClose: () -> Unit, account: Account, relayToAdd: String = "") {
|
fun NewRelayListView(onClose: () -> Unit, accountViewModel: AccountViewModel, relayToAdd: String = "") {
|
||||||
val postViewModel: NewRelayListViewModel = viewModel()
|
val postViewModel: NewRelayListViewModel = viewModel()
|
||||||
val feedState by postViewModel.relays.collectAsState()
|
val feedState by postViewModel.relays.collectAsState()
|
||||||
|
|
||||||
LaunchedEffect(Unit) {
|
LaunchedEffect(Unit) {
|
||||||
postViewModel.load(account)
|
postViewModel.load(accountViewModel.account)
|
||||||
}
|
}
|
||||||
|
|
||||||
Dialog(
|
Dialog(
|
||||||
|
|||||||
@@ -16,13 +16,12 @@ import java.util.concurrent.atomic.AtomicBoolean
|
|||||||
*/
|
*/
|
||||||
class BundledUpdate(
|
class BundledUpdate(
|
||||||
val delay: Long,
|
val delay: Long,
|
||||||
val dispatcher: CoroutineDispatcher = Dispatchers.Default,
|
val dispatcher: CoroutineDispatcher = Dispatchers.Default
|
||||||
val onUpdate: () -> Unit
|
|
||||||
) {
|
) {
|
||||||
private var onlyOneInBlock = AtomicBoolean()
|
private var onlyOneInBlock = AtomicBoolean()
|
||||||
private var invalidatesAgain = false
|
private var invalidatesAgain = false
|
||||||
|
|
||||||
fun invalidate(ignoreIfDoing: Boolean = false) {
|
fun invalidate(ignoreIfDoing: Boolean = false, onUpdate: suspend () -> Unit) {
|
||||||
if (onlyOneInBlock.getAndSet(true)) {
|
if (onlyOneInBlock.getAndSet(true)) {
|
||||||
if (!ignoreIfDoing) {
|
if (!ignoreIfDoing) {
|
||||||
invalidatesAgain = true
|
invalidatesAgain = true
|
||||||
|
|||||||
@@ -157,145 +157,159 @@ private fun RenderRegular(
|
|||||||
// FlowRow doesn't work well with paragraphs. So we need to split them
|
// FlowRow doesn't work well with paragraphs. So we need to split them
|
||||||
content.split('\n').forEach { paragraph ->
|
content.split('\n').forEach { paragraph ->
|
||||||
FlowRow() {
|
FlowRow() {
|
||||||
val s = if (isArabic(paragraph)) {
|
val s = remember(paragraph) {
|
||||||
paragraph.trim().split(' ')
|
if (isArabic(paragraph)) {
|
||||||
.reversed()
|
paragraph.trim().split(' ').reversed()
|
||||||
} else {
|
|
||||||
paragraph.trim().split(' ')
|
|
||||||
}
|
|
||||||
s.forEach { word: String ->
|
|
||||||
if (canPreview) {
|
|
||||||
// Explicit URL
|
|
||||||
val img = state.imagesForPager[word]
|
|
||||||
if (img != null) {
|
|
||||||
ZoomableContentView(img, state.imageList)
|
|
||||||
} else if (state.urlSet.contains(word)) {
|
|
||||||
UrlPreview(word, "$word ")
|
|
||||||
} else if (state.customEmoji.any { word.contains(it.key) }) {
|
|
||||||
RenderCustomEmoji(word, state.customEmoji)
|
|
||||||
} else if (word.startsWith("lnbc", true)) {
|
|
||||||
MayBeInvoicePreview(word)
|
|
||||||
} else if (word.startsWith("lnurl", true)) {
|
|
||||||
MayBeWithdrawal(word)
|
|
||||||
} else if (Patterns.EMAIL_ADDRESS.matcher(word).matches()) {
|
|
||||||
ClickableEmail(word)
|
|
||||||
} else if (word.length > 6 && Patterns.PHONE.matcher(word).matches()) {
|
|
||||||
ClickablePhone(word)
|
|
||||||
} else if (isBechLink(word)) {
|
|
||||||
BechLink(
|
|
||||||
word,
|
|
||||||
canPreview,
|
|
||||||
backgroundColor,
|
|
||||||
accountViewModel,
|
|
||||||
nav
|
|
||||||
)
|
|
||||||
} else if (word.startsWith("#")) {
|
|
||||||
if (tagIndex.matcher(word).matches() && tags != null) {
|
|
||||||
TagLink(
|
|
||||||
word,
|
|
||||||
tags,
|
|
||||||
canPreview,
|
|
||||||
backgroundColor,
|
|
||||||
accountViewModel,
|
|
||||||
nav
|
|
||||||
)
|
|
||||||
} else if (hashTagsPattern.matcher(word).matches()) {
|
|
||||||
HashTag(word, nav)
|
|
||||||
} else {
|
|
||||||
Text(
|
|
||||||
text = "$word ",
|
|
||||||
style = LocalTextStyle.current.copy(textDirection = TextDirection.Content)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
} else if (noProtocolUrlValidator.matcher(word).matches()) {
|
|
||||||
val matcher = noProtocolUrlValidator.matcher(word)
|
|
||||||
matcher.find()
|
|
||||||
val url = matcher.group(1) // url
|
|
||||||
val additionalChars = matcher.group(4) ?: "" // additional chars
|
|
||||||
|
|
||||||
if (url != null) {
|
|
||||||
ClickableUrl(url, "https://$url")
|
|
||||||
Text("$additionalChars ")
|
|
||||||
} else {
|
|
||||||
Text(
|
|
||||||
text = "$word ",
|
|
||||||
style = LocalTextStyle.current.copy(textDirection = TextDirection.Content)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
Text(
|
|
||||||
text = "$word ",
|
|
||||||
style = LocalTextStyle.current.copy(textDirection = TextDirection.Content)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
if (state.urlSet.contains(word)) {
|
paragraph.trim().split(' ')
|
||||||
ClickableUrl("$word ", word)
|
|
||||||
} else if (word.startsWith("lnurl", true)) {
|
|
||||||
val lnWithdrawal = LnWithdrawalUtil.findWithdrawal(word)
|
|
||||||
if (lnWithdrawal != null) {
|
|
||||||
ClickableWithdrawal(withdrawalString = lnWithdrawal)
|
|
||||||
} else {
|
|
||||||
Text(
|
|
||||||
text = "$word ",
|
|
||||||
style = LocalTextStyle.current.copy(textDirection = TextDirection.Content)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
} else if (state.customEmoji.any { word.contains(it.key) }) {
|
|
||||||
RenderCustomEmoji(word, state.customEmoji)
|
|
||||||
} else if (Patterns.EMAIL_ADDRESS.matcher(word).matches()) {
|
|
||||||
ClickableEmail(word)
|
|
||||||
} else if (Patterns.PHONE.matcher(word).matches() && word.length > 6) {
|
|
||||||
ClickablePhone(word)
|
|
||||||
} else if (isBechLink(word)) {
|
|
||||||
BechLink(
|
|
||||||
word,
|
|
||||||
canPreview,
|
|
||||||
backgroundColor,
|
|
||||||
accountViewModel,
|
|
||||||
nav
|
|
||||||
)
|
|
||||||
} else if (word.startsWith("#")) {
|
|
||||||
if (tagIndex.matcher(word).matches() && tags != null) {
|
|
||||||
TagLink(
|
|
||||||
word,
|
|
||||||
tags,
|
|
||||||
canPreview,
|
|
||||||
backgroundColor,
|
|
||||||
accountViewModel,
|
|
||||||
nav
|
|
||||||
)
|
|
||||||
} else if (hashTagsPattern.matcher(word).matches()) {
|
|
||||||
HashTag(word, nav)
|
|
||||||
} else {
|
|
||||||
Text(
|
|
||||||
text = "$word ",
|
|
||||||
style = LocalTextStyle.current.copy(textDirection = TextDirection.Content)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
} else if (noProtocolUrlValidator.matcher(word).matches()) {
|
|
||||||
val matcher = noProtocolUrlValidator.matcher(word)
|
|
||||||
matcher.find()
|
|
||||||
val url = matcher.group(1) // url
|
|
||||||
val additionalChars = matcher.group(4) ?: "" // additional chars
|
|
||||||
|
|
||||||
if (url != null) {
|
|
||||||
ClickableUrl(url, "https://$url")
|
|
||||||
Text("$additionalChars ")
|
|
||||||
} else {
|
|
||||||
Text(
|
|
||||||
text = "$word ",
|
|
||||||
style = LocalTextStyle.current.copy(textDirection = TextDirection.Content)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
Text(
|
|
||||||
text = "$word ",
|
|
||||||
style = LocalTextStyle.current.copy(textDirection = TextDirection.Content)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
s.forEach { word: String ->
|
||||||
|
RenderWord(word, state, canPreview, backgroundColor, accountViewModel, nav, tags)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun RenderWord(
|
||||||
|
word: String,
|
||||||
|
state: RichTextViewerState,
|
||||||
|
canPreview: Boolean,
|
||||||
|
backgroundColor: Color,
|
||||||
|
accountViewModel: AccountViewModel,
|
||||||
|
nav: (String) -> Unit,
|
||||||
|
tags: List<List<String>>?
|
||||||
|
) {
|
||||||
|
if (canPreview) {
|
||||||
|
// Explicit URL
|
||||||
|
val img = state.imagesForPager[word]
|
||||||
|
if (img != null) {
|
||||||
|
ZoomableContentView(img, state.imageList)
|
||||||
|
} else if (state.urlSet.contains(word)) {
|
||||||
|
UrlPreview(word, "$word ")
|
||||||
|
} else if (state.customEmoji.any { word.contains(it.key) }) {
|
||||||
|
RenderCustomEmoji(word, state.customEmoji)
|
||||||
|
} else if (word.startsWith("lnbc", true)) {
|
||||||
|
MayBeInvoicePreview(word)
|
||||||
|
} else if (word.startsWith("lnurl", true)) {
|
||||||
|
MayBeWithdrawal(word)
|
||||||
|
} else if (Patterns.EMAIL_ADDRESS.matcher(word).matches()) {
|
||||||
|
ClickableEmail(word)
|
||||||
|
} else if (word.length > 6 && Patterns.PHONE.matcher(word).matches()) {
|
||||||
|
ClickablePhone(word)
|
||||||
|
} else if (isBechLink(word)) {
|
||||||
|
BechLink(
|
||||||
|
word,
|
||||||
|
canPreview,
|
||||||
|
backgroundColor,
|
||||||
|
accountViewModel,
|
||||||
|
nav
|
||||||
|
)
|
||||||
|
} else if (word.startsWith("#")) {
|
||||||
|
if (tagIndex.matcher(word).matches() && tags != null) {
|
||||||
|
TagLink(
|
||||||
|
word,
|
||||||
|
tags,
|
||||||
|
canPreview,
|
||||||
|
backgroundColor,
|
||||||
|
accountViewModel,
|
||||||
|
nav
|
||||||
|
)
|
||||||
|
} else if (hashTagsPattern.matcher(word).matches()) {
|
||||||
|
HashTag(word, nav)
|
||||||
|
} else {
|
||||||
|
Text(
|
||||||
|
text = "$word ",
|
||||||
|
style = LocalTextStyle.current.copy(textDirection = TextDirection.Content)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
} else if (noProtocolUrlValidator.matcher(word).matches()) {
|
||||||
|
val matcher = noProtocolUrlValidator.matcher(word)
|
||||||
|
matcher.find()
|
||||||
|
val url = matcher.group(1) // url
|
||||||
|
val additionalChars = matcher.group(4) ?: "" // additional chars
|
||||||
|
|
||||||
|
if (url != null) {
|
||||||
|
ClickableUrl(url, "https://$url")
|
||||||
|
Text("$additionalChars ")
|
||||||
|
} else {
|
||||||
|
Text(
|
||||||
|
text = "$word ",
|
||||||
|
style = LocalTextStyle.current.copy(textDirection = TextDirection.Content)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
Text(
|
||||||
|
text = "$word ",
|
||||||
|
style = LocalTextStyle.current.copy(textDirection = TextDirection.Content)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (state.urlSet.contains(word)) {
|
||||||
|
ClickableUrl("$word ", word)
|
||||||
|
} else if (word.startsWith("lnurl", true)) {
|
||||||
|
val lnWithdrawal = LnWithdrawalUtil.findWithdrawal(word)
|
||||||
|
if (lnWithdrawal != null) {
|
||||||
|
ClickableWithdrawal(withdrawalString = lnWithdrawal)
|
||||||
|
} else {
|
||||||
|
Text(
|
||||||
|
text = "$word ",
|
||||||
|
style = LocalTextStyle.current.copy(textDirection = TextDirection.Content)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
} else if (state.customEmoji.any { word.contains(it.key) }) {
|
||||||
|
RenderCustomEmoji(word, state.customEmoji)
|
||||||
|
} else if (Patterns.EMAIL_ADDRESS.matcher(word).matches()) {
|
||||||
|
ClickableEmail(word)
|
||||||
|
} else if (Patterns.PHONE.matcher(word).matches() && word.length > 6) {
|
||||||
|
ClickablePhone(word)
|
||||||
|
} else if (isBechLink(word)) {
|
||||||
|
BechLink(
|
||||||
|
word,
|
||||||
|
canPreview,
|
||||||
|
backgroundColor,
|
||||||
|
accountViewModel,
|
||||||
|
nav
|
||||||
|
)
|
||||||
|
} else if (word.startsWith("#")) {
|
||||||
|
if (tagIndex.matcher(word).matches() && tags != null) {
|
||||||
|
TagLink(
|
||||||
|
word,
|
||||||
|
tags,
|
||||||
|
canPreview,
|
||||||
|
backgroundColor,
|
||||||
|
accountViewModel,
|
||||||
|
nav
|
||||||
|
)
|
||||||
|
} else if (hashTagsPattern.matcher(word).matches()) {
|
||||||
|
HashTag(word, nav)
|
||||||
|
} else {
|
||||||
|
Text(
|
||||||
|
text = "$word ",
|
||||||
|
style = LocalTextStyle.current.copy(textDirection = TextDirection.Content)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
} else if (noProtocolUrlValidator.matcher(word).matches()) {
|
||||||
|
val matcher = noProtocolUrlValidator.matcher(word)
|
||||||
|
matcher.find()
|
||||||
|
val url = matcher.group(1) // url
|
||||||
|
val additionalChars = matcher.group(4) ?: "" // additional chars
|
||||||
|
|
||||||
|
if (url != null) {
|
||||||
|
ClickableUrl(url, "https://$url")
|
||||||
|
Text("$additionalChars ")
|
||||||
|
} else {
|
||||||
|
Text(
|
||||||
|
text = "$word ",
|
||||||
|
style = LocalTextStyle.current.copy(textDirection = TextDirection.Content)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
Text(
|
||||||
|
text = "$word ",
|
||||||
|
style = LocalTextStyle.current.copy(textDirection = TextDirection.Content)
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
package com.vitorpamplona.amethyst.ui.components
|
package com.vitorpamplona.amethyst.ui.components
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
import android.graphics.drawable.Drawable
|
import android.graphics.drawable.Drawable
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
@@ -124,7 +125,9 @@ fun VideoView(videoUri: Uri, description: String? = null, thumb: Drawable? = nul
|
|||||||
|
|
||||||
val mutedInstance = remember { mutableStateOf(DefaultMutedSetting.value) }
|
val mutedInstance = remember { mutableStateOf(DefaultMutedSetting.value) }
|
||||||
|
|
||||||
val exoPlayer = remember(videoUri) {
|
var exoPlayer by remember { mutableStateOf<ExoPlayer?>(null) }
|
||||||
|
|
||||||
|
LaunchedEffect(key1 = videoUri) {
|
||||||
val mediaBuilder = MediaItem.Builder().setUri(videoUri)
|
val mediaBuilder = MediaItem.Builder().setUri(videoUri)
|
||||||
|
|
||||||
description?.let {
|
description?.let {
|
||||||
@@ -135,7 +138,7 @@ fun VideoView(videoUri: Uri, description: String? = null, thumb: Drawable? = nul
|
|||||||
|
|
||||||
val media = mediaBuilder.build()
|
val media = mediaBuilder.build()
|
||||||
|
|
||||||
ExoPlayer.Builder(context).build().apply {
|
exoPlayer = ExoPlayer.Builder(context).build().apply {
|
||||||
repeatMode = Player.REPEAT_MODE_ALL
|
repeatMode = Player.REPEAT_MODE_ALL
|
||||||
videoScalingMode = C.VIDEO_SCALING_MODE_SCALE_TO_FIT
|
videoScalingMode = C.VIDEO_SCALING_MODE_SCALE_TO_FIT
|
||||||
volume = if (mutedInstance.value) 0f else 1f
|
volume = if (mutedInstance.value) 0f else 1f
|
||||||
@@ -152,53 +155,15 @@ fun VideoView(videoUri: Uri, description: String? = null, thumb: Drawable? = nul
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
DisposableEffect(
|
exoPlayer?.let {
|
||||||
BoxWithConstraints() {
|
RenderVideoPlayer(it, context, thumb, onDialog, mutedInstance)
|
||||||
AndroidView(
|
}
|
||||||
modifier = Modifier
|
|
||||||
.fillMaxWidth()
|
|
||||||
.defaultMinSize(minHeight = 70.dp)
|
|
||||||
.align(Alignment.Center)
|
|
||||||
.onVisibilityChanges { visible ->
|
|
||||||
if (visible && !exoPlayer.isPlaying) {
|
|
||||||
exoPlayer.play()
|
|
||||||
} else if (!visible && exoPlayer.isPlaying) {
|
|
||||||
exoPlayer.pause()
|
|
||||||
}
|
|
||||||
},
|
|
||||||
factory = {
|
|
||||||
StyledPlayerView(context).apply {
|
|
||||||
player = exoPlayer
|
|
||||||
layoutParams = FrameLayout.LayoutParams(
|
|
||||||
ViewGroup.LayoutParams.MATCH_PARENT,
|
|
||||||
ViewGroup.LayoutParams.WRAP_CONTENT
|
|
||||||
)
|
|
||||||
controllerAutoShow = false
|
|
||||||
thumb?.let { defaultArtwork = thumb }
|
|
||||||
hideController()
|
|
||||||
resizeMode = if (maxHeight.isFinite) AspectRatioFrameLayout.RESIZE_MODE_FIT else AspectRatioFrameLayout.RESIZE_MODE_FIXED_WIDTH
|
|
||||||
onDialog?.let { innerOnDialog ->
|
|
||||||
setFullscreenButtonClickListener {
|
|
||||||
exoPlayer.pause()
|
|
||||||
innerOnDialog(it)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
MuteButton(mutedInstance) {
|
DisposableEffect(Unit) {
|
||||||
mutedInstance.value = !mutedInstance.value
|
|
||||||
DefaultMutedSetting.value = mutedInstance.value
|
|
||||||
|
|
||||||
exoPlayer.volume = if (mutedInstance.value) 0f else 1f
|
|
||||||
}
|
|
||||||
}
|
|
||||||
) {
|
|
||||||
val observer = LifecycleEventObserver { _, event ->
|
val observer = LifecycleEventObserver { _, event ->
|
||||||
when (event) {
|
when (event) {
|
||||||
Lifecycle.Event.ON_PAUSE -> {
|
Lifecycle.Event.ON_PAUSE -> {
|
||||||
exoPlayer.pause()
|
exoPlayer?.pause()
|
||||||
}
|
}
|
||||||
else -> {}
|
else -> {}
|
||||||
}
|
}
|
||||||
@@ -207,12 +172,64 @@ fun VideoView(videoUri: Uri, description: String? = null, thumb: Drawable? = nul
|
|||||||
lifecycle.addObserver(observer)
|
lifecycle.addObserver(observer)
|
||||||
|
|
||||||
onDispose {
|
onDispose {
|
||||||
exoPlayer.release()
|
exoPlayer?.release()
|
||||||
lifecycle.removeObserver(observer)
|
lifecycle.removeObserver(observer)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun RenderVideoPlayer(
|
||||||
|
exoPlayer: ExoPlayer,
|
||||||
|
context: Context,
|
||||||
|
thumb: Drawable?,
|
||||||
|
onDialog: ((Boolean) -> Unit)?,
|
||||||
|
mutedInstance: MutableState<Boolean>
|
||||||
|
) {
|
||||||
|
BoxWithConstraints() {
|
||||||
|
AndroidView(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.defaultMinSize(minHeight = 70.dp)
|
||||||
|
.align(Alignment.Center)
|
||||||
|
.onVisibilityChanges { visible ->
|
||||||
|
if (visible && !exoPlayer.isPlaying) {
|
||||||
|
exoPlayer.play()
|
||||||
|
} else if (!visible && exoPlayer.isPlaying) {
|
||||||
|
exoPlayer.pause()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
factory = {
|
||||||
|
StyledPlayerView(context).apply {
|
||||||
|
player = exoPlayer
|
||||||
|
layoutParams = FrameLayout.LayoutParams(
|
||||||
|
ViewGroup.LayoutParams.MATCH_PARENT,
|
||||||
|
ViewGroup.LayoutParams.WRAP_CONTENT
|
||||||
|
)
|
||||||
|
controllerAutoShow = false
|
||||||
|
thumb?.let { defaultArtwork = thumb }
|
||||||
|
hideController()
|
||||||
|
resizeMode =
|
||||||
|
if (maxHeight.isFinite) AspectRatioFrameLayout.RESIZE_MODE_FIT else AspectRatioFrameLayout.RESIZE_MODE_FIXED_WIDTH
|
||||||
|
onDialog?.let { innerOnDialog ->
|
||||||
|
setFullscreenButtonClickListener {
|
||||||
|
exoPlayer.pause()
|
||||||
|
innerOnDialog(it)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
MuteButton(mutedInstance) {
|
||||||
|
mutedInstance.value = !mutedInstance.value
|
||||||
|
DefaultMutedSetting.value = mutedInstance.value
|
||||||
|
|
||||||
|
exoPlayer.volume = if (mutedInstance.value) 0f else 1f
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fun Modifier.onVisibilityChanges(onVisibilityChanges: (Boolean) -> Unit): Modifier = composed {
|
fun Modifier.onVisibilityChanges(onVisibilityChanges: (Boolean) -> Unit): Modifier = composed {
|
||||||
val view = LocalView.current
|
val view = LocalView.current
|
||||||
var isVisible: Boolean? by remember { mutableStateOf(null) }
|
var isVisible: Boolean? by remember { mutableStateOf(null) }
|
||||||
|
|||||||
@@ -10,8 +10,7 @@ import com.vitorpamplona.amethyst.ui.actions.updated
|
|||||||
import kotlin.time.ExperimentalTime
|
import kotlin.time.ExperimentalTime
|
||||||
import kotlin.time.measureTimedValue
|
import kotlin.time.measureTimedValue
|
||||||
|
|
||||||
object ChatroomListKnownFeedFilter : AdditiveFeedFilter<Note>() {
|
class ChatroomListKnownFeedFilter(val account: Account) : AdditiveFeedFilter<Note>() {
|
||||||
lateinit var account: Account
|
|
||||||
|
|
||||||
// returns the last Note of each user.
|
// returns the last Note of each user.
|
||||||
override fun feed(): List<Note> {
|
override fun feed(): List<Note> {
|
||||||
|
|||||||
@@ -3,13 +3,12 @@ package com.vitorpamplona.amethyst.ui.dal
|
|||||||
import com.vitorpamplona.amethyst.model.Account
|
import com.vitorpamplona.amethyst.model.Account
|
||||||
import com.vitorpamplona.amethyst.model.Note
|
import com.vitorpamplona.amethyst.model.Note
|
||||||
|
|
||||||
object ChatroomListNewFeedFilter : FeedFilter<Note>() {
|
class ChatroomListNewFeedFilter(val account: Account) : FeedFilter<Note>() {
|
||||||
lateinit var account: Account
|
|
||||||
|
|
||||||
// returns the last Note of each user.
|
// returns the last Note of each user.
|
||||||
override fun feed(): List<Note> {
|
override fun feed(): List<Note> {
|
||||||
val me = account.userProfile()
|
val me = account.userProfile()
|
||||||
val followingKeySet = ChatroomListKnownFeedFilter.account.followingKeySet()
|
val followingKeySet = account.followingKeySet()
|
||||||
|
|
||||||
val privateChatrooms = account.userProfile().privateChatrooms
|
val privateChatrooms = account.userProfile().privateChatrooms
|
||||||
val messagingWith = privateChatrooms.keys.filter {
|
val messagingWith = privateChatrooms.keys.filter {
|
||||||
|
|||||||
@@ -5,8 +5,7 @@ import com.vitorpamplona.amethyst.model.LocalCache
|
|||||||
import com.vitorpamplona.amethyst.model.Note
|
import com.vitorpamplona.amethyst.model.Note
|
||||||
import com.vitorpamplona.amethyst.service.model.*
|
import com.vitorpamplona.amethyst.service.model.*
|
||||||
|
|
||||||
object GlobalFeedFilter : AdditiveFeedFilter<Note>() {
|
class GlobalFeedFilter(val account: Account) : AdditiveFeedFilter<Note>() {
|
||||||
lateinit var account: Account
|
|
||||||
|
|
||||||
override fun feed(): List<Note> {
|
override fun feed(): List<Note> {
|
||||||
val notes = innerApplyFilter(LocalCache.notes.values)
|
val notes = innerApplyFilter(LocalCache.notes.values)
|
||||||
|
|||||||
@@ -6,9 +6,7 @@ import com.vitorpamplona.amethyst.model.Note
|
|||||||
import com.vitorpamplona.amethyst.service.model.PollNoteEvent
|
import com.vitorpamplona.amethyst.service.model.PollNoteEvent
|
||||||
import com.vitorpamplona.amethyst.service.model.TextNoteEvent
|
import com.vitorpamplona.amethyst.service.model.TextNoteEvent
|
||||||
|
|
||||||
object HomeConversationsFeedFilter : AdditiveFeedFilter<Note>() {
|
class HomeConversationsFeedFilter(val account: Account) : AdditiveFeedFilter<Note>() {
|
||||||
lateinit var account: Account
|
|
||||||
|
|
||||||
override fun feed(): List<Note> {
|
override fun feed(): List<Note> {
|
||||||
return sort(innerApplyFilter(LocalCache.notes.values))
|
return sort(innerApplyFilter(LocalCache.notes.values))
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,9 +10,7 @@ import com.vitorpamplona.amethyst.service.model.PollNoteEvent
|
|||||||
import com.vitorpamplona.amethyst.service.model.RepostEvent
|
import com.vitorpamplona.amethyst.service.model.RepostEvent
|
||||||
import com.vitorpamplona.amethyst.service.model.TextNoteEvent
|
import com.vitorpamplona.amethyst.service.model.TextNoteEvent
|
||||||
|
|
||||||
object HomeNewThreadFeedFilter : AdditiveFeedFilter<Note>() {
|
class HomeNewThreadFeedFilter(val account: Account) : AdditiveFeedFilter<Note>() {
|
||||||
lateinit var account: Account
|
|
||||||
|
|
||||||
override fun feed(): List<Note> {
|
override fun feed(): List<Note> {
|
||||||
val notes = innerApplyFilter(LocalCache.notes.values)
|
val notes = innerApplyFilter(LocalCache.notes.values)
|
||||||
val longFormNotes = innerApplyFilter(LocalCache.addressables.values)
|
val longFormNotes = innerApplyFilter(LocalCache.addressables.values)
|
||||||
|
|||||||
@@ -7,9 +7,7 @@ import com.vitorpamplona.amethyst.model.LocalCache
|
|||||||
import com.vitorpamplona.amethyst.model.Note
|
import com.vitorpamplona.amethyst.model.Note
|
||||||
import com.vitorpamplona.amethyst.service.model.*
|
import com.vitorpamplona.amethyst.service.model.*
|
||||||
|
|
||||||
object NotificationFeedFilter : AdditiveFeedFilter<Note>() {
|
class NotificationFeedFilter(val account: Account) : AdditiveFeedFilter<Note>() {
|
||||||
lateinit var account: Account
|
|
||||||
|
|
||||||
override fun feed(): List<Note> {
|
override fun feed(): List<Note> {
|
||||||
return sort(innerApplyFilter(LocalCache.notes.values))
|
return sort(innerApplyFilter(LocalCache.notes.values))
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,21 +1,18 @@
|
|||||||
package com.vitorpamplona.amethyst.ui.dal
|
package com.vitorpamplona.amethyst.ui.dal
|
||||||
|
|
||||||
|
import com.google.errorprone.annotations.Immutable
|
||||||
import com.vitorpamplona.amethyst.model.Note
|
import com.vitorpamplona.amethyst.model.Note
|
||||||
import com.vitorpamplona.amethyst.model.ThreadAssembler
|
import com.vitorpamplona.amethyst.model.ThreadAssembler
|
||||||
|
|
||||||
object ThreadFeedFilter : FeedFilter<Note>() {
|
@Immutable
|
||||||
var noteId: String? = null
|
class ThreadFeedFilter(val noteId: String) : FeedFilter<Note>() {
|
||||||
|
|
||||||
override fun feed(): List<Note> {
|
override fun feed(): List<Note> {
|
||||||
val cachedSignatures: MutableMap<Note, String> = mutableMapOf()
|
val cachedSignatures: MutableMap<Note, String> = mutableMapOf()
|
||||||
val eventsToWatch = noteId?.let { ThreadAssembler().findThreadFor(it) } ?: emptySet()
|
val eventsToWatch = ThreadAssembler().findThreadFor(noteId) ?: emptySet()
|
||||||
// Currently orders by date of each event, descending, at each level of the reply stack
|
// Currently orders by date of each event, descending, at each level of the reply stack
|
||||||
val order = compareByDescending<Note> { it.replyLevelSignature(cachedSignatures) }
|
val order = compareByDescending<Note> { it.replyLevelSignature(cachedSignatures) }
|
||||||
|
|
||||||
return eventsToWatch.sortedWith(order)
|
return eventsToWatch.sortedWith(order)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun loadThread(noteId: String?) {
|
|
||||||
this.noteId = noteId
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,9 +6,7 @@ import com.vitorpamplona.amethyst.model.LocalCache
|
|||||||
import com.vitorpamplona.amethyst.model.Note
|
import com.vitorpamplona.amethyst.model.Note
|
||||||
import com.vitorpamplona.amethyst.service.model.*
|
import com.vitorpamplona.amethyst.service.model.*
|
||||||
|
|
||||||
object VideoFeedFilter : AdditiveFeedFilter<Note>() {
|
class VideoFeedFilter(val account: Account) : AdditiveFeedFilter<Note>() {
|
||||||
lateinit var account: Account
|
|
||||||
|
|
||||||
override fun feed(): List<Note> {
|
override fun feed(): List<Note> {
|
||||||
val notes = innerApplyFilter(LocalCache.notes.values)
|
val notes = innerApplyFilter(LocalCache.notes.values)
|
||||||
|
|
||||||
|
|||||||
@@ -151,21 +151,14 @@ private fun RowScope.HasNewItemsIcon(
|
|||||||
scope.launch {
|
scope.launch {
|
||||||
if (!selected) {
|
if (!selected) {
|
||||||
navController.navigate(route.base) {
|
navController.navigate(route.base) {
|
||||||
navController.graph.startDestinationRoute?.let { start ->
|
popUpTo(Route.Home.route)
|
||||||
popUpTo(start)
|
|
||||||
restoreState = true
|
|
||||||
}
|
|
||||||
launchSingleTop = true
|
launchSingleTop = true
|
||||||
restoreState = true
|
restoreState = true
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
val newRoute = route.route.replace("{scrollToTop}", "true")
|
val newRoute = route.route.replace("{scrollToTop}", "true")
|
||||||
navController.navigate(newRoute) {
|
navController.navigate(newRoute) {
|
||||||
navController.graph.startDestinationRoute?.let { start ->
|
popUpTo(Route.Home.route)
|
||||||
popUpTo(start) { inclusive = route.route == Route.Home.route }
|
|
||||||
restoreState = true
|
|
||||||
}
|
|
||||||
|
|
||||||
launchSingleTop = true
|
launchSingleTop = true
|
||||||
restoreState = true
|
restoreState = true
|
||||||
}
|
}
|
||||||
@@ -209,27 +202,30 @@ private fun RowScope.BottomIcon(
|
|||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
private fun NotifiableIcon(icon: Int, size: Dp, iconSize: Dp, selected: Boolean, hasNewItems: Boolean) {
|
private fun NotifiableIcon(icon: Int, size: Dp, iconSize: Dp, selected: Boolean, hasNewItems: Boolean) {
|
||||||
Box(Modifier.size(size)) {
|
Box(remember { Modifier.size(size) }) {
|
||||||
Icon(
|
Icon(
|
||||||
painter = painterResource(id = icon),
|
painter = painterResource(id = icon),
|
||||||
contentDescription = null,
|
contentDescription = null,
|
||||||
modifier = Modifier.size(iconSize),
|
modifier = remember { Modifier.size(iconSize) },
|
||||||
tint = if (selected) MaterialTheme.colors.primary else Color.Unspecified
|
tint = if (selected) MaterialTheme.colors.primary else Color.Unspecified
|
||||||
)
|
)
|
||||||
|
|
||||||
if (hasNewItems) {
|
if (hasNewItems) {
|
||||||
Box(
|
Box(
|
||||||
Modifier
|
remember {
|
||||||
.width(10.dp)
|
Modifier
|
||||||
.height(10.dp)
|
|
||||||
.align(Alignment.TopEnd)
|
|
||||||
) {
|
|
||||||
Box(
|
|
||||||
modifier = Modifier
|
|
||||||
.width(10.dp)
|
.width(10.dp)
|
||||||
.height(10.dp)
|
.height(10.dp)
|
||||||
.clip(shape = CircleShape)
|
.align(Alignment.TopEnd)
|
||||||
.background(MaterialTheme.colors.primary),
|
}
|
||||||
|
) {
|
||||||
|
Box(
|
||||||
|
modifier = remember {
|
||||||
|
Modifier
|
||||||
|
.width(10.dp)
|
||||||
|
.height(10.dp)
|
||||||
|
.clip(shape = CircleShape)
|
||||||
|
}.background(MaterialTheme.colors.primary),
|
||||||
contentAlignment = Alignment.TopEnd
|
contentAlignment = Alignment.TopEnd
|
||||||
) {
|
) {
|
||||||
Text(
|
Text(
|
||||||
@@ -237,9 +233,11 @@ private fun NotifiableIcon(icon: Int, size: Dp, iconSize: Dp, selected: Boolean,
|
|||||||
color = Color.White,
|
color = Color.White,
|
||||||
textAlign = TextAlign.Center,
|
textAlign = TextAlign.Center,
|
||||||
fontSize = 12.sp,
|
fontSize = 12.sp,
|
||||||
modifier = Modifier
|
modifier = remember {
|
||||||
.wrapContentHeight()
|
Modifier
|
||||||
.align(Alignment.TopEnd)
|
.wrapContentHeight()
|
||||||
|
.align(Alignment.TopEnd)
|
||||||
|
}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,25 +1,18 @@
|
|||||||
package com.vitorpamplona.amethyst.ui.navigation
|
package com.vitorpamplona.amethyst.ui.navigation
|
||||||
|
|
||||||
import androidx.compose.foundation.ExperimentalFoundationApi
|
|
||||||
import androidx.compose.foundation.pager.rememberPagerState
|
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.LaunchedEffect
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.runtime.livedata.observeAsState
|
|
||||||
import androidx.compose.runtime.mutableStateOf
|
import androidx.compose.runtime.mutableStateOf
|
||||||
import androidx.compose.runtime.remember
|
import androidx.compose.runtime.remember
|
||||||
import androidx.compose.runtime.rememberCoroutineScope
|
import androidx.compose.runtime.rememberCoroutineScope
|
||||||
import androidx.compose.runtime.setValue
|
import androidx.compose.runtime.setValue
|
||||||
import androidx.lifecycle.viewmodel.compose.viewModel
|
|
||||||
import androidx.navigation.NavHostController
|
import androidx.navigation.NavHostController
|
||||||
import androidx.navigation.compose.NavHost
|
import androidx.navigation.compose.NavHost
|
||||||
import androidx.navigation.compose.composable
|
import androidx.navigation.compose.composable
|
||||||
import com.vitorpamplona.amethyst.ui.dal.GlobalFeedFilter
|
|
||||||
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.note.UserReactionsViewModel
|
||||||
|
import com.vitorpamplona.amethyst.ui.screen.NostrChatroomListKnownFeedViewModel
|
||||||
|
import com.vitorpamplona.amethyst.ui.screen.NostrChatroomListNewFeedViewModel
|
||||||
import com.vitorpamplona.amethyst.ui.screen.NostrGlobalFeedViewModel
|
import com.vitorpamplona.amethyst.ui.screen.NostrGlobalFeedViewModel
|
||||||
import com.vitorpamplona.amethyst.ui.screen.NostrHomeFeedViewModel
|
import com.vitorpamplona.amethyst.ui.screen.NostrHomeFeedViewModel
|
||||||
import com.vitorpamplona.amethyst.ui.screen.NostrHomeRepliesFeedViewModel
|
import com.vitorpamplona.amethyst.ui.screen.NostrHomeRepliesFeedViewModel
|
||||||
@@ -39,50 +32,27 @@ import com.vitorpamplona.amethyst.ui.screen.loggedIn.ProfileScreen
|
|||||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.SearchScreen
|
import com.vitorpamplona.amethyst.ui.screen.loggedIn.SearchScreen
|
||||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.ThreadScreen
|
import com.vitorpamplona.amethyst.ui.screen.loggedIn.ThreadScreen
|
||||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.VideoScreen
|
import com.vitorpamplona.amethyst.ui.screen.loggedIn.VideoScreen
|
||||||
import kotlinx.coroutines.Dispatchers
|
|
||||||
import kotlinx.coroutines.delay
|
import kotlinx.coroutines.delay
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
|
|
||||||
@OptIn(ExperimentalFoundationApi::class)
|
|
||||||
@Composable
|
@Composable
|
||||||
fun AppNavigation(
|
fun AppNavigation(
|
||||||
|
homeFeedViewModel: NostrHomeFeedViewModel,
|
||||||
|
repliesFeedViewModel: NostrHomeRepliesFeedViewModel,
|
||||||
|
knownFeedViewModel: NostrChatroomListKnownFeedViewModel,
|
||||||
|
newFeedViewModel: NostrChatroomListNewFeedViewModel,
|
||||||
|
searchFeedViewModel: NostrGlobalFeedViewModel,
|
||||||
|
videoFeedViewModel: NostrVideoFeedViewModel,
|
||||||
|
notifFeedViewModel: NotificationViewModel,
|
||||||
|
userReactionsStatsModel: UserReactionsViewModel,
|
||||||
|
|
||||||
navController: NavHostController,
|
navController: NavHostController,
|
||||||
accountViewModel: AccountViewModel,
|
accountViewModel: AccountViewModel,
|
||||||
nextPage: String? = null
|
nextPage: String? = null
|
||||||
) {
|
) {
|
||||||
val homePagerState = rememberPagerState()
|
|
||||||
var actionableNextPage by remember { mutableStateOf<String?>(nextPage) }
|
var actionableNextPage by remember { mutableStateOf<String?>(nextPage) }
|
||||||
|
|
||||||
// Avoids creating ViewModels for performance reasons (up to 1 second delays)
|
|
||||||
val accountState by accountViewModel.accountLiveData.observeAsState()
|
|
||||||
val account = remember(accountState) { accountState?.account } ?: return
|
|
||||||
val accountHex = remember(accountState) { accountState?.account?.userProfile()?.pubkeyHex }
|
|
||||||
|
|
||||||
HomeNewThreadFeedFilter.account = account
|
|
||||||
HomeConversationsFeedFilter.account = account
|
|
||||||
|
|
||||||
val homeFeedViewModel: NostrHomeFeedViewModel = viewModel()
|
|
||||||
val repliesFeedViewModel: NostrHomeRepliesFeedViewModel = viewModel()
|
|
||||||
|
|
||||||
GlobalFeedFilter.account = account
|
|
||||||
val searchFeedViewModel: NostrGlobalFeedViewModel = viewModel()
|
|
||||||
|
|
||||||
VideoFeedFilter.account = account
|
|
||||||
val videoFeedViewModel: NostrVideoFeedViewModel = viewModel()
|
|
||||||
|
|
||||||
NotificationFeedFilter.account = account
|
|
||||||
val notifFeedViewModel: NotificationViewModel = viewModel()
|
|
||||||
|
|
||||||
val userReactionsStatsModel: UserReactionsViewModel = viewModel()
|
|
||||||
val scope = rememberCoroutineScope()
|
val scope = rememberCoroutineScope()
|
||||||
|
|
||||||
LaunchedEffect(accountHex) {
|
|
||||||
scope.launch(Dispatchers.IO) {
|
|
||||||
userReactionsStatsModel.load(account.userProfile())
|
|
||||||
userReactionsStatsModel.initializeSuspend()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
val nav = remember {
|
val nav = remember {
|
||||||
{ route: String ->
|
{ route: String ->
|
||||||
if (getRouteWithArguments(navController) != route) {
|
if (getRouteWithArguments(navController) != route) {
|
||||||
@@ -96,23 +66,20 @@ fun AppNavigation(
|
|||||||
composable(route.route, route.arguments, content = {
|
composable(route.route, route.arguments, content = {
|
||||||
val scrollToTop = it.arguments?.getBoolean("scrollToTop") ?: false
|
val scrollToTop = it.arguments?.getBoolean("scrollToTop") ?: false
|
||||||
|
|
||||||
VideoScreen(
|
LaunchedEffect(key1 = it) {
|
||||||
videoFeedView = videoFeedViewModel,
|
if (scrollToTop) {
|
||||||
accountViewModel = accountViewModel,
|
|
||||||
nav = nav,
|
|
||||||
scrollToTop = scrollToTop
|
|
||||||
)
|
|
||||||
|
|
||||||
// Avoids running scroll to top when back button is pressed
|
|
||||||
// Changes this on a thread to avoid changing before it finishes the composition
|
|
||||||
if (scrollToTop) {
|
|
||||||
LaunchedEffect(key1 = Unit) {
|
|
||||||
scope.launch {
|
scope.launch {
|
||||||
delay(1000)
|
videoFeedViewModel.sendToTop()
|
||||||
it.arguments?.remove("scrollToTop")
|
it.arguments?.remove("scrollToTop")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
VideoScreen(
|
||||||
|
videoFeedView = videoFeedViewModel,
|
||||||
|
accountViewModel = accountViewModel,
|
||||||
|
nav = nav
|
||||||
|
)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -120,23 +87,20 @@ fun AppNavigation(
|
|||||||
composable(route.route, route.arguments, content = {
|
composable(route.route, route.arguments, content = {
|
||||||
val scrollToTop = it.arguments?.getBoolean("scrollToTop") ?: false
|
val scrollToTop = it.arguments?.getBoolean("scrollToTop") ?: false
|
||||||
|
|
||||||
SearchScreen(
|
LaunchedEffect(key1 = it) {
|
||||||
searchFeedViewModel = searchFeedViewModel,
|
if (scrollToTop) {
|
||||||
accountViewModel = accountViewModel,
|
|
||||||
nav = nav,
|
|
||||||
scrollToTop = scrollToTop
|
|
||||||
)
|
|
||||||
|
|
||||||
// Avoids running scroll to top when back button is pressed
|
|
||||||
// Changes this on a thread to avoid changing before it finishes the composition
|
|
||||||
if (scrollToTop) {
|
|
||||||
LaunchedEffect(key1 = Unit) {
|
|
||||||
scope.launch {
|
scope.launch {
|
||||||
delay(1000)
|
searchFeedViewModel.sendToTop()
|
||||||
it.arguments?.remove("scrollToTop")
|
it.arguments?.remove("scrollToTop")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
SearchScreen(
|
||||||
|
searchFeedViewModel = searchFeedViewModel,
|
||||||
|
accountViewModel = accountViewModel,
|
||||||
|
nav = nav
|
||||||
|
)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -145,25 +109,24 @@ fun AppNavigation(
|
|||||||
val scrollToTop = it.arguments?.getBoolean("scrollToTop") ?: false
|
val scrollToTop = it.arguments?.getBoolean("scrollToTop") ?: false
|
||||||
val nip47 = it.arguments?.getString("nip47")
|
val nip47 = it.arguments?.getString("nip47")
|
||||||
|
|
||||||
|
LaunchedEffect(key1 = it) {
|
||||||
|
if (scrollToTop) {
|
||||||
|
scope.launch {
|
||||||
|
homeFeedViewModel.sendToTop()
|
||||||
|
repliesFeedViewModel.sendToTop()
|
||||||
|
it.arguments?.remove("scrollToTop")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
HomeScreen(
|
HomeScreen(
|
||||||
homeFeedViewModel = homeFeedViewModel,
|
homeFeedViewModel = homeFeedViewModel,
|
||||||
repliesFeedViewModel = repliesFeedViewModel,
|
repliesFeedViewModel = repliesFeedViewModel,
|
||||||
accountViewModel = accountViewModel,
|
accountViewModel = accountViewModel,
|
||||||
nav = nav,
|
nav = nav,
|
||||||
pagerState = homePagerState,
|
|
||||||
scrollToTop = scrollToTop,
|
|
||||||
nip47 = nip47
|
nip47 = nip47
|
||||||
)
|
)
|
||||||
|
|
||||||
// Avoids running scroll to top when back button is pressed
|
|
||||||
if (scrollToTop) {
|
|
||||||
LaunchedEffect(key1 = Unit) {
|
|
||||||
scope.launch {
|
|
||||||
delay(1000)
|
|
||||||
it.arguments?.remove("scrollToTop")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (nip47 != null) {
|
if (nip47 != null) {
|
||||||
LaunchedEffect(key1 = Unit) {
|
LaunchedEffect(key1 = Unit) {
|
||||||
scope.launch {
|
scope.launch {
|
||||||
@@ -179,28 +142,37 @@ fun AppNavigation(
|
|||||||
composable(route.route, route.arguments, content = {
|
composable(route.route, route.arguments, content = {
|
||||||
val scrollToTop = it.arguments?.getBoolean("scrollToTop") ?: false
|
val scrollToTop = it.arguments?.getBoolean("scrollToTop") ?: false
|
||||||
|
|
||||||
NotificationScreen(
|
LaunchedEffect(key1 = it) {
|
||||||
notifFeedViewModel = notifFeedViewModel,
|
if (scrollToTop) {
|
||||||
userReactionsStatsModel = userReactionsStatsModel,
|
|
||||||
accountViewModel = accountViewModel,
|
|
||||||
nav = nav,
|
|
||||||
scrollToTop = scrollToTop
|
|
||||||
)
|
|
||||||
|
|
||||||
// Avoids running scroll to top when back button is pressed
|
|
||||||
// Changes this on a thread to avoid changing before it finishes the composition
|
|
||||||
if (scrollToTop) {
|
|
||||||
LaunchedEffect(key1 = Unit) {
|
|
||||||
scope.launch {
|
scope.launch {
|
||||||
delay(1000)
|
notifFeedViewModel.clear()
|
||||||
|
notifFeedViewModel.sendToTop()
|
||||||
it.arguments?.remove("scrollToTop")
|
it.arguments?.remove("scrollToTop")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
NotificationScreen(
|
||||||
|
notifFeedViewModel = notifFeedViewModel,
|
||||||
|
userReactionsStatsModel = userReactionsStatsModel,
|
||||||
|
accountViewModel = accountViewModel,
|
||||||
|
nav = nav
|
||||||
|
)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
composable(Route.Message.route, content = { ChatroomListScreen(accountViewModel, nav) })
|
composable(
|
||||||
|
Route.Message.route,
|
||||||
|
content = {
|
||||||
|
ChatroomListScreen(
|
||||||
|
knownFeedViewModel,
|
||||||
|
newFeedViewModel,
|
||||||
|
accountViewModel,
|
||||||
|
nav
|
||||||
|
)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
composable(Route.BlockedUsers.route, content = { HiddenUsersScreen(accountViewModel, nav) })
|
composable(Route.BlockedUsers.route, content = { HiddenUsersScreen(accountViewModel, nav) })
|
||||||
composable(Route.Bookmarks.route, content = { BookmarkListScreen(accountViewModel, nav) })
|
composable(Route.Bookmarks.route, content = { BookmarkListScreen(accountViewModel, nav) })
|
||||||
|
|
||||||
|
|||||||
@@ -26,6 +26,8 @@ import androidx.compose.material.icons.Icons
|
|||||||
import androidx.compose.material.icons.filled.ArrowBack
|
import androidx.compose.material.icons.filled.ArrowBack
|
||||||
import androidx.compose.material.icons.filled.ExpandMore
|
import androidx.compose.material.icons.filled.ExpandMore
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.Stable
|
||||||
|
import androidx.compose.runtime.collectAsState
|
||||||
import androidx.compose.runtime.derivedStateOf
|
import androidx.compose.runtime.derivedStateOf
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.runtime.livedata.observeAsState
|
import androidx.compose.runtime.livedata.observeAsState
|
||||||
@@ -42,6 +44,7 @@ import androidx.compose.ui.res.painterResource
|
|||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.lifecycle.ViewModel
|
import androidx.lifecycle.ViewModel
|
||||||
|
import androidx.lifecycle.ViewModelProvider
|
||||||
import androidx.lifecycle.viewModelScope
|
import androidx.lifecycle.viewModelScope
|
||||||
import androidx.lifecycle.viewmodel.compose.viewModel
|
import androidx.lifecycle.viewmodel.compose.viewModel
|
||||||
import androidx.navigation.NavHostController
|
import androidx.navigation.NavHostController
|
||||||
@@ -72,13 +75,18 @@ import com.vitorpamplona.amethyst.ui.actions.NewRelayListView
|
|||||||
import com.vitorpamplona.amethyst.ui.components.ResizeImage
|
import com.vitorpamplona.amethyst.ui.components.ResizeImage
|
||||||
import com.vitorpamplona.amethyst.ui.components.RobohashAsyncImageProxy
|
import com.vitorpamplona.amethyst.ui.components.RobohashAsyncImageProxy
|
||||||
import com.vitorpamplona.amethyst.ui.screen.RelayPoolViewModel
|
import com.vitorpamplona.amethyst.ui.screen.RelayPoolViewModel
|
||||||
|
import com.vitorpamplona.amethyst.ui.screen.equalImmutableLists
|
||||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
||||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.SpinnerSelectionDialog
|
import com.vitorpamplona.amethyst.ui.screen.loggedIn.SpinnerSelectionDialog
|
||||||
|
import kotlinx.collections.immutable.ImmutableList
|
||||||
|
import kotlinx.collections.immutable.toImmutableList
|
||||||
|
import kotlinx.collections.immutable.toPersistentList
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.Job
|
import kotlinx.coroutines.Job
|
||||||
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
|
import kotlinx.coroutines.flow.asStateFlow
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import kotlinx.coroutines.withContext
|
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun AppTopBar(followLists: FollowListViewModel, navController: NavHostController, scaffoldState: ScaffoldState, accountViewModel: AccountViewModel) {
|
fun AppTopBar(followLists: FollowListViewModel, navController: NavHostController, scaffoldState: ScaffoldState, accountViewModel: AccountViewModel) {
|
||||||
@@ -93,27 +101,51 @@ fun AppTopBar(followLists: FollowListViewModel, navController: NavHostController
|
|||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun StoriesTopBar(followLists: FollowListViewModel, scaffoldState: ScaffoldState, accountViewModel: AccountViewModel) {
|
fun StoriesTopBar(followLists: FollowListViewModel, scaffoldState: ScaffoldState, accountViewModel: AccountViewModel) {
|
||||||
GenericTopBar(scaffoldState, accountViewModel) { account ->
|
GenericTopBar(scaffoldState, accountViewModel) { accountViewModel ->
|
||||||
FollowList(followLists, account.defaultStoriesFollowList, account.userProfile(), true) { listName ->
|
val accountState by accountViewModel.accountLiveData.observeAsState()
|
||||||
account.changeDefaultStoriesFollowList(listName)
|
accountState?.account?.let { account ->
|
||||||
|
FollowList(
|
||||||
|
followLists,
|
||||||
|
account.defaultStoriesFollowList,
|
||||||
|
account.userProfile(),
|
||||||
|
true
|
||||||
|
) { listName ->
|
||||||
|
account.changeDefaultStoriesFollowList(listName)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun HomeTopBar(followLists: FollowListViewModel, scaffoldState: ScaffoldState, accountViewModel: AccountViewModel) {
|
fun HomeTopBar(followLists: FollowListViewModel, scaffoldState: ScaffoldState, accountViewModel: AccountViewModel) {
|
||||||
GenericTopBar(scaffoldState, accountViewModel) { account ->
|
GenericTopBar(scaffoldState, accountViewModel) { accountViewModel ->
|
||||||
FollowList(followLists, account.defaultHomeFollowList, account.userProfile(), false) { listName ->
|
val accountState by accountViewModel.accountLiveData.observeAsState()
|
||||||
account.changeDefaultHomeFollowList(listName)
|
accountState?.account?.let { account ->
|
||||||
|
FollowList(
|
||||||
|
followLists,
|
||||||
|
account.defaultHomeFollowList,
|
||||||
|
account.userProfile(),
|
||||||
|
false
|
||||||
|
) { listName ->
|
||||||
|
account.changeDefaultHomeFollowList(listName)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun NotificationTopBar(followLists: FollowListViewModel, scaffoldState: ScaffoldState, accountViewModel: AccountViewModel) {
|
fun NotificationTopBar(followLists: FollowListViewModel, scaffoldState: ScaffoldState, accountViewModel: AccountViewModel) {
|
||||||
GenericTopBar(scaffoldState, accountViewModel) { account ->
|
GenericTopBar(scaffoldState, accountViewModel) { accountViewModel ->
|
||||||
FollowList(followLists, account.defaultNotificationFollowList, account.userProfile(), true) { listName ->
|
val accountState by accountViewModel.accountLiveData.observeAsState()
|
||||||
account.changeDefaultNotificationFollowList(listName)
|
accountState?.account?.let { account ->
|
||||||
|
FollowList(
|
||||||
|
followLists,
|
||||||
|
account.defaultNotificationFollowList,
|
||||||
|
account.userProfile(),
|
||||||
|
true
|
||||||
|
) { listName ->
|
||||||
|
account.changeDefaultNotificationFollowList(listName)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -127,10 +159,7 @@ fun MainTopBar(scaffoldState: ScaffoldState, accountViewModel: AccountViewModel)
|
|||||||
|
|
||||||
@OptIn(coil.annotation.ExperimentalCoilApi::class)
|
@OptIn(coil.annotation.ExperimentalCoilApi::class)
|
||||||
@Composable
|
@Composable
|
||||||
fun GenericTopBar(scaffoldState: ScaffoldState, accountViewModel: AccountViewModel, content: @Composable (Account) -> Unit) {
|
fun GenericTopBar(scaffoldState: ScaffoldState, accountViewModel: AccountViewModel, content: @Composable (AccountViewModel) -> Unit) {
|
||||||
val accountState by accountViewModel.accountLiveData.observeAsState()
|
|
||||||
val account = accountState?.account ?: return
|
|
||||||
|
|
||||||
val relayViewModel: RelayPoolViewModel = viewModel { RelayPoolViewModel() }
|
val relayViewModel: RelayPoolViewModel = viewModel { RelayPoolViewModel() }
|
||||||
val connectedRelaysLiveData by relayViewModel.connectedRelaysLiveData.observeAsState()
|
val connectedRelaysLiveData by relayViewModel.connectedRelaysLiveData.observeAsState()
|
||||||
val availableRelaysLiveData by relayViewModel.availableRelaysLiveData.observeAsState()
|
val availableRelaysLiveData by relayViewModel.availableRelaysLiveData.observeAsState()
|
||||||
@@ -142,7 +171,7 @@ fun GenericTopBar(scaffoldState: ScaffoldState, accountViewModel: AccountViewMod
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (wantsToEditRelays) {
|
if (wantsToEditRelays) {
|
||||||
NewRelayListView({ wantsToEditRelays = false }, account)
|
NewRelayListView({ wantsToEditRelays = false }, accountViewModel)
|
||||||
}
|
}
|
||||||
|
|
||||||
Column() {
|
Column() {
|
||||||
@@ -163,7 +192,7 @@ fun GenericTopBar(scaffoldState: ScaffoldState, accountViewModel: AccountViewMod
|
|||||||
horizontalAlignment = Alignment.CenterHorizontally,
|
horizontalAlignment = Alignment.CenterHorizontally,
|
||||||
verticalArrangement = Arrangement.Center
|
verticalArrangement = Arrangement.Center
|
||||||
) {
|
) {
|
||||||
content(account)
|
content(accountViewModel)
|
||||||
}
|
}
|
||||||
|
|
||||||
Column(
|
Column(
|
||||||
@@ -193,7 +222,7 @@ fun GenericTopBar(scaffoldState: ScaffoldState, accountViewModel: AccountViewMod
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
navigationIcon = {
|
navigationIcon = {
|
||||||
LoggedInUserPictureDrawer(account) {
|
LoggedInUserPictureDrawer(accountViewModel) {
|
||||||
coroutineScope.launch {
|
coroutineScope.launch {
|
||||||
scaffoldState.drawerState.open()
|
scaffoldState.drawerState.open()
|
||||||
}
|
}
|
||||||
@@ -219,10 +248,10 @@ fun GenericTopBar(scaffoldState: ScaffoldState, accountViewModel: AccountViewMod
|
|||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
private fun LoggedInUserPictureDrawer(
|
private fun LoggedInUserPictureDrawer(
|
||||||
account: Account,
|
accountViewModel: AccountViewModel,
|
||||||
onClick: () -> Unit
|
onClick: () -> Unit
|
||||||
) {
|
) {
|
||||||
val accountUserState by account.userProfile().live().metadata.observeAsState()
|
val accountUserState by accountViewModel.account.userProfile().live().metadata.observeAsState()
|
||||||
val accountUser = accountUserState?.user ?: return
|
val accountUser = accountUserState?.user ?: return
|
||||||
|
|
||||||
IconButton(
|
IconButton(
|
||||||
@@ -230,8 +259,8 @@ private fun LoggedInUserPictureDrawer(
|
|||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
) {
|
) {
|
||||||
RobohashAsyncImageProxy(
|
RobohashAsyncImageProxy(
|
||||||
robot = accountUser.pubkeyHex,
|
robot = remember { accountUser.pubkeyHex },
|
||||||
model = ResizeImage(accountUser.profilePicture(), 34.dp),
|
model = remember(accountUser) { ResizeImage(accountUser.profilePicture(), 34.dp) },
|
||||||
contentDescription = stringResource(id = R.string.profile_image),
|
contentDescription = stringResource(id = R.string.profile_image),
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.width(34.dp)
|
.width(34.dp)
|
||||||
@@ -248,33 +277,31 @@ fun FollowList(followListsModel: FollowListViewModel, listName: String, loggedIn
|
|||||||
|
|
||||||
val defaultOptions = if (withGlobal) listOf(kind3Follow, globalFollow) else listOf(kind3Follow)
|
val defaultOptions = if (withGlobal) listOf(kind3Follow, globalFollow) else listOf(kind3Follow)
|
||||||
|
|
||||||
val followLists = remember(followListsModel.followLists) {
|
val followLists by followListsModel.followLists.collectAsState()
|
||||||
(defaultOptions + followListsModel.followLists)
|
|
||||||
|
val allLists = remember(followLists) {
|
||||||
|
(defaultOptions + followLists)
|
||||||
}
|
}
|
||||||
|
|
||||||
val followNames = remember(followLists) {
|
val followNames = remember(followLists) {
|
||||||
derivedStateOf {
|
derivedStateOf {
|
||||||
followLists.map { it.second }
|
allLists.map { it.second }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
SimpleTextSpinner(
|
SimpleTextSpinner(
|
||||||
placeholder = followLists.firstOrNull { it.first == listName }?.second ?: "Select an Option",
|
placeholder = allLists.firstOrNull { it.first == listName }?.second ?: "Select an Option",
|
||||||
options = followNames.value,
|
options = followNames.value,
|
||||||
onSelect = {
|
onSelect = {
|
||||||
onChange(followLists.getOrNull(it)?.first ?: KIND3_FOLLOWS)
|
onChange(allLists.getOrNull(it)?.first ?: KIND3_FOLLOWS)
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
class FollowListViewModel : ViewModel() {
|
@Stable
|
||||||
var followLists by mutableStateOf<List<Pair<String, String>>>(emptyList())
|
class FollowListViewModel(val account: Account) : ViewModel() {
|
||||||
var account: Account? = null
|
private var _followLists = MutableStateFlow<ImmutableList<Pair<String, String>>>(emptyList<Pair<String, String>>().toPersistentList())
|
||||||
|
val followLists = _followLists.asStateFlow()
|
||||||
fun load(account: Account?) {
|
|
||||||
this.account = account
|
|
||||||
refresh()
|
|
||||||
}
|
|
||||||
|
|
||||||
fun refresh() {
|
fun refresh() {
|
||||||
val scope = CoroutineScope(Job() + Dispatchers.Default)
|
val scope = CoroutineScope(Job() + Dispatchers.Default)
|
||||||
@@ -284,28 +311,25 @@ class FollowListViewModel : ViewModel() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private suspend fun refreshFollows() {
|
private suspend fun refreshFollows() {
|
||||||
val myAccount = account ?: return
|
|
||||||
|
|
||||||
val newFollowLists = LocalCache.addressables.mapNotNull {
|
val newFollowLists = LocalCache.addressables.mapNotNull {
|
||||||
val event = (it.value.event as? PeopleListEvent)
|
val event = (it.value.event as? PeopleListEvent)
|
||||||
// Has to have an list
|
// Has to have an list
|
||||||
if (event != null && event.pubKey == myAccount.userProfile().pubkeyHex && (event.tags.size > 1 || event.content.length > 50)) {
|
if (event != null && event.pubKey == account.userProfile().pubkeyHex && (event.tags.size > 1 || event.content.length > 50)) {
|
||||||
Pair(event.dTag(), event.dTag())
|
Pair(event.dTag(), event.dTag())
|
||||||
} else {
|
} else {
|
||||||
null
|
null
|
||||||
}
|
}
|
||||||
}.sortedBy { it.second }
|
}.sortedBy { it.second }.toImmutableList()
|
||||||
|
|
||||||
withContext(Dispatchers.Main) {
|
if (!equalImmutableLists(_followLists.value, newFollowLists)) {
|
||||||
if (followLists != newFollowLists) {
|
_followLists.emit(newFollowLists)
|
||||||
followLists = newFollowLists
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
var collectorJob: Job? = null
|
var collectorJob: Job? = null
|
||||||
|
|
||||||
init {
|
init {
|
||||||
|
refresh()
|
||||||
collectorJob = viewModelScope.launch(Dispatchers.IO) {
|
collectorJob = viewModelScope.launch(Dispatchers.IO) {
|
||||||
LocalCache.live.newEventBundles.collect { newNotes ->
|
LocalCache.live.newEventBundles.collect { newNotes ->
|
||||||
newNotes.forEach {
|
newNotes.forEach {
|
||||||
@@ -321,6 +345,12 @@ class FollowListViewModel : ViewModel() {
|
|||||||
collectorJob?.cancel()
|
collectorJob?.cancel()
|
||||||
super.onCleared()
|
super.onCleared()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class Factory(val account: Account) : ViewModelProvider.Factory {
|
||||||
|
override fun <FollowListViewModel : ViewModel> create(modelClass: Class<FollowListViewModel>): FollowListViewModel {
|
||||||
|
return FollowListViewModel(account) as FollowListViewModel
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
|
|||||||
@@ -156,9 +156,8 @@ object HomeLatestItem : LatestItem() {
|
|||||||
newNotes: Set<Note>
|
newNotes: Set<Note>
|
||||||
): Boolean {
|
): Boolean {
|
||||||
val lastTime = cache.load("HomeFollows")
|
val lastTime = cache.load("HomeFollows")
|
||||||
HomeNewThreadFeedFilter.account = account
|
|
||||||
|
|
||||||
val newestItem = updateNewestItem(newNotes, account, HomeNewThreadFeedFilter)
|
val newestItem = updateNewestItem(newNotes, account, HomeNewThreadFeedFilter(account))
|
||||||
|
|
||||||
return (newestItem?.createdAt() ?: 0) > lastTime
|
return (newestItem?.createdAt() ?: 0) > lastTime
|
||||||
}
|
}
|
||||||
@@ -171,9 +170,8 @@ object NotificationLatestItem : LatestItem() {
|
|||||||
newNotes: Set<Note>
|
newNotes: Set<Note>
|
||||||
): Boolean {
|
): Boolean {
|
||||||
val lastTime = cache.load("Notification")
|
val lastTime = cache.load("Notification")
|
||||||
NotificationFeedFilter.account = account
|
|
||||||
|
|
||||||
val newestItem = updateNewestItem(newNotes, account, NotificationFeedFilter)
|
val newestItem = updateNewestItem(newNotes, account, NotificationFeedFilter(account))
|
||||||
|
|
||||||
return (newestItem?.createdAt() ?: 0) > lastTime
|
return (newestItem?.createdAt() ?: 0) > lastTime
|
||||||
}
|
}
|
||||||
@@ -185,9 +183,7 @@ object MessagesLatestItem : LatestItem() {
|
|||||||
cache: NotificationCache,
|
cache: NotificationCache,
|
||||||
newNotes: Set<Note>
|
newNotes: Set<Note>
|
||||||
): Boolean {
|
): Boolean {
|
||||||
ChatroomListKnownFeedFilter.account = account
|
val newestItem = updateNewestItem(newNotes, account, ChatroomListKnownFeedFilter(account))
|
||||||
|
|
||||||
val newestItem = updateNewestItem(newNotes, account, ChatroomListKnownFeedFilter)
|
|
||||||
|
|
||||||
val roomUserHex = (newestItem?.event as? PrivateDmEvent)?.talkingWith(account.userProfile().pubkeyHex)
|
val roomUserHex = (newestItem?.event as? PrivateDmEvent)?.talkingWith(account.userProfile().pubkeyHex)
|
||||||
|
|
||||||
|
|||||||
@@ -107,7 +107,7 @@ fun MessageSetCompose(messageSetCard: MessageSetCard, routeForLastRead: String,
|
|||||||
routeForLastRead = routeForLastRead,
|
routeForLastRead = routeForLastRead,
|
||||||
isBoostedNote = true,
|
isBoostedNote = true,
|
||||||
addMarginTop = false,
|
addMarginTop = false,
|
||||||
parentBackgroundColor = backgroundColor,
|
parentBackgroundColor = null,
|
||||||
accountViewModel = accountViewModel,
|
accountViewModel = accountViewModel,
|
||||||
nav = nav
|
nav = nav
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -79,9 +79,9 @@ fun MultiSetCompose(multiSetCard: MultiSetCard, routeForLastRead: String, accoun
|
|||||||
|
|
||||||
LaunchedEffect(key1 = multiSetCard.createdAt()) {
|
LaunchedEffect(key1 = multiSetCard.createdAt()) {
|
||||||
scope.launch(Dispatchers.IO) {
|
scope.launch(Dispatchers.IO) {
|
||||||
val newIsNew = multiSetCard.createdAt > NotificationCache.load(routeForLastRead)
|
val newIsNew = multiSetCard.maxCreatedAt > NotificationCache.load(routeForLastRead)
|
||||||
|
|
||||||
NotificationCache.markAsRead(routeForLastRead, multiSetCard.createdAt)
|
NotificationCache.markAsRead(routeForLastRead, multiSetCard.maxCreatedAt)
|
||||||
|
|
||||||
if (newIsNew != isNew) {
|
if (newIsNew != isNew) {
|
||||||
isNew = newIsNew
|
isNew = newIsNew
|
||||||
|
|||||||
@@ -476,7 +476,7 @@ fun ZapReaction(
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (wantsToChangeZapAmount) {
|
if (wantsToChangeZapAmount) {
|
||||||
UpdateZapAmountDialog({ wantsToChangeZapAmount = false }, account = account)
|
UpdateZapAmountDialog({ wantsToChangeZapAmount = false }, accountViewModel = accountViewModel)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (wantsToSetCustomZap) {
|
if (wantsToSetCustomZap) {
|
||||||
|
|||||||
@@ -65,6 +65,7 @@ import androidx.compose.ui.unit.sp
|
|||||||
import androidx.compose.ui.window.Dialog
|
import androidx.compose.ui.window.Dialog
|
||||||
import androidx.compose.ui.window.DialogProperties
|
import androidx.compose.ui.window.DialogProperties
|
||||||
import androidx.lifecycle.ViewModel
|
import androidx.lifecycle.ViewModel
|
||||||
|
import androidx.lifecycle.ViewModelProvider
|
||||||
import androidx.lifecycle.viewmodel.compose.viewModel
|
import androidx.lifecycle.viewmodel.compose.viewModel
|
||||||
import com.vitorpamplona.amethyst.R
|
import com.vitorpamplona.amethyst.R
|
||||||
import com.vitorpamplona.amethyst.model.Account
|
import com.vitorpamplona.amethyst.model.Account
|
||||||
@@ -74,15 +75,14 @@ import com.vitorpamplona.amethyst.service.model.LnZapEvent
|
|||||||
import com.vitorpamplona.amethyst.ui.actions.CloseButton
|
import com.vitorpamplona.amethyst.ui.actions.CloseButton
|
||||||
import com.vitorpamplona.amethyst.ui.actions.SaveButton
|
import com.vitorpamplona.amethyst.ui.actions.SaveButton
|
||||||
import com.vitorpamplona.amethyst.ui.qrcode.SimpleQrCodeScanner
|
import com.vitorpamplona.amethyst.ui.qrcode.SimpleQrCodeScanner
|
||||||
|
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
||||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.TextSpinner
|
import com.vitorpamplona.amethyst.ui.screen.loggedIn.TextSpinner
|
||||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.getFragmentActivity
|
import com.vitorpamplona.amethyst.ui.screen.loggedIn.getFragmentActivity
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import androidx.compose.runtime.rememberCoroutineScope as rememberCoroutineScope
|
import androidx.compose.runtime.rememberCoroutineScope as rememberCoroutineScope
|
||||||
|
|
||||||
class UpdateZapAmountViewModel : ViewModel() {
|
class UpdateZapAmountViewModel(val account: Account) : ViewModel() {
|
||||||
private var account: Account? = null
|
|
||||||
|
|
||||||
var nextAmount by mutableStateOf(TextFieldValue(""))
|
var nextAmount by mutableStateOf(TextFieldValue(""))
|
||||||
var amountSet by mutableStateOf(listOf<Long>())
|
var amountSet by mutableStateOf(listOf<Long>())
|
||||||
var walletConnectRelay by mutableStateOf(TextFieldValue(""))
|
var walletConnectRelay by mutableStateOf(TextFieldValue(""))
|
||||||
@@ -90,8 +90,7 @@ class UpdateZapAmountViewModel : ViewModel() {
|
|||||||
var walletConnectSecret by mutableStateOf(TextFieldValue(""))
|
var walletConnectSecret by mutableStateOf(TextFieldValue(""))
|
||||||
var selectedZapType by mutableStateOf(LnZapEvent.ZapType.PRIVATE)
|
var selectedZapType by mutableStateOf(LnZapEvent.ZapType.PRIVATE)
|
||||||
|
|
||||||
fun load(account: Account) {
|
fun load() {
|
||||||
this.account = account
|
|
||||||
this.amountSet = account.zapAmountChoices
|
this.amountSet = account.zapAmountChoices
|
||||||
this.walletConnectPubkey = account.zapPaymentRequest?.pubKeyHex?.let { TextFieldValue(it) } ?: TextFieldValue("")
|
this.walletConnectPubkey = account.zapPaymentRequest?.pubKeyHex?.let { TextFieldValue(it) } ?: TextFieldValue("")
|
||||||
this.walletConnectRelay = account.zapPaymentRequest?.relayUri?.let { TextFieldValue(it) } ?: TextFieldValue("")
|
this.walletConnectRelay = account.zapPaymentRequest?.relayUri?.let { TextFieldValue(it) } ?: TextFieldValue("")
|
||||||
@@ -185,14 +184,25 @@ class UpdateZapAmountViewModel : ViewModel() {
|
|||||||
TextFieldValue(contact.secret ?: "")
|
TextFieldValue(contact.secret ?: "")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class Factory(val account: Account) : ViewModelProvider.Factory {
|
||||||
|
override fun <UpdateZapAmountViewModel : ViewModel> create(modelClass: Class<UpdateZapAmountViewModel>): UpdateZapAmountViewModel {
|
||||||
|
return UpdateZapAmountViewModel(account) as UpdateZapAmountViewModel
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@OptIn(ExperimentalLayoutApi::class)
|
@OptIn(ExperimentalLayoutApi::class)
|
||||||
@Composable
|
@Composable
|
||||||
fun UpdateZapAmountDialog(onClose: () -> Unit, account: Account, nip47uri: String? = null) {
|
fun UpdateZapAmountDialog(onClose: () -> Unit, nip47uri: String? = null, accountViewModel: AccountViewModel) {
|
||||||
val context = LocalContext.current
|
val context = LocalContext.current
|
||||||
val scope = rememberCoroutineScope()
|
val scope = rememberCoroutineScope()
|
||||||
val postViewModel: UpdateZapAmountViewModel = viewModel()
|
|
||||||
|
val postViewModel: UpdateZapAmountViewModel = viewModel(
|
||||||
|
key = accountViewModel.userProfile().pubkeyHex,
|
||||||
|
factory = UpdateZapAmountViewModel.Factory(accountViewModel.account)
|
||||||
|
)
|
||||||
|
|
||||||
val uri = LocalUriHandler.current
|
val uri = LocalUriHandler.current
|
||||||
|
|
||||||
val zapTypes = listOf(
|
val zapTypes = listOf(
|
||||||
@@ -205,8 +215,8 @@ fun UpdateZapAmountDialog(onClose: () -> Unit, account: Account, nip47uri: Strin
|
|||||||
val zapOptions = zapTypes.map { it.second }
|
val zapOptions = zapTypes.map { it.second }
|
||||||
val zapOptionExplainers = zapTypes.map { it.third }
|
val zapOptionExplainers = zapTypes.map { it.third }
|
||||||
|
|
||||||
LaunchedEffect(account) {
|
LaunchedEffect(accountViewModel) {
|
||||||
postViewModel.load(account)
|
postViewModel.load()
|
||||||
if (nip47uri != null) {
|
if (nip47uri != null) {
|
||||||
try {
|
try {
|
||||||
postViewModel.updateNIP47(nip47uri)
|
postViewModel.updateNIP47(nip47uri)
|
||||||
@@ -341,7 +351,7 @@ fun UpdateZapAmountDialog(onClose: () -> Unit, account: Account, nip47uri: Strin
|
|||||||
) {
|
) {
|
||||||
TextSpinner(
|
TextSpinner(
|
||||||
label = stringResource(id = R.string.zap_type_explainer),
|
label = stringResource(id = R.string.zap_type_explainer),
|
||||||
placeholder = zapTypes.filter { it.first == account.defaultZapType }
|
placeholder = zapTypes.filter { it.first == accountViewModel.defaultZapType() }
|
||||||
.first().second,
|
.first().second,
|
||||||
options = zapOptions,
|
options = zapOptions,
|
||||||
explainers = zapOptionExplainers,
|
explainers = zapOptionExplainers,
|
||||||
|
|||||||
@@ -14,10 +14,9 @@ import androidx.compose.material.icons.filled.Bolt
|
|||||||
import androidx.compose.material.icons.filled.ExpandMore
|
import androidx.compose.material.icons.filled.ExpandMore
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.Stable
|
import androidx.compose.runtime.Stable
|
||||||
|
import androidx.compose.runtime.collectAsState
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.runtime.mutableStateOf
|
|
||||||
import androidx.compose.runtime.remember
|
import androidx.compose.runtime.remember
|
||||||
import androidx.compose.runtime.setValue
|
|
||||||
import androidx.compose.ui.Alignment.Companion.CenterVertically
|
import androidx.compose.ui.Alignment.Companion.CenterVertically
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.graphics.Color
|
import androidx.compose.ui.graphics.Color
|
||||||
@@ -27,6 +26,7 @@ import androidx.compose.ui.text.font.FontWeight
|
|||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.compose.ui.unit.sp
|
import androidx.compose.ui.unit.sp
|
||||||
import androidx.lifecycle.ViewModel
|
import androidx.lifecycle.ViewModel
|
||||||
|
import androidx.lifecycle.ViewModelProvider
|
||||||
import androidx.lifecycle.viewModelScope
|
import androidx.lifecycle.viewModelScope
|
||||||
import com.patrykandpatrick.vico.core.chart.composed.ComposedChartEntryModel
|
import com.patrykandpatrick.vico.core.chart.composed.ComposedChartEntryModel
|
||||||
import com.patrykandpatrick.vico.core.entry.ChartEntryModel
|
import com.patrykandpatrick.vico.core.entry.ChartEntryModel
|
||||||
@@ -34,6 +34,7 @@ import com.patrykandpatrick.vico.core.entry.ChartEntryModelProducer
|
|||||||
import com.patrykandpatrick.vico.core.entry.composed.plus
|
import com.patrykandpatrick.vico.core.entry.composed.plus
|
||||||
import com.patrykandpatrick.vico.core.entry.entryOf
|
import com.patrykandpatrick.vico.core.entry.entryOf
|
||||||
import com.vitorpamplona.amethyst.R
|
import com.vitorpamplona.amethyst.R
|
||||||
|
import com.vitorpamplona.amethyst.model.Account
|
||||||
import com.vitorpamplona.amethyst.model.HexKey
|
import com.vitorpamplona.amethyst.model.HexKey
|
||||||
import com.vitorpamplona.amethyst.model.LocalCache
|
import com.vitorpamplona.amethyst.model.LocalCache
|
||||||
import com.vitorpamplona.amethyst.model.Note
|
import com.vitorpamplona.amethyst.model.Note
|
||||||
@@ -47,6 +48,8 @@ import com.vitorpamplona.amethyst.ui.theme.BitcoinOrange
|
|||||||
import com.vitorpamplona.amethyst.ui.theme.RoyalBlue
|
import com.vitorpamplona.amethyst.ui.theme.RoyalBlue
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.Job
|
import kotlinx.coroutines.Job
|
||||||
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
|
import kotlinx.coroutines.flow.asStateFlow
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import java.math.BigDecimal
|
import java.math.BigDecimal
|
||||||
import java.time.Instant
|
import java.time.Instant
|
||||||
@@ -80,47 +83,50 @@ fun UserReactionsRow(
|
|||||||
}
|
}
|
||||||
|
|
||||||
Row(verticalAlignment = CenterVertically, modifier = Modifier.weight(1f)) {
|
Row(verticalAlignment = CenterVertically, modifier = Modifier.weight(1f)) {
|
||||||
UserReplyReaction(model.replies[model.today()])
|
val replies by model.replies.collectAsState()
|
||||||
|
UserReplyReaction(replies[model.today()])
|
||||||
}
|
}
|
||||||
|
|
||||||
Row(verticalAlignment = CenterVertically, modifier = Modifier.weight(1f)) {
|
Row(verticalAlignment = CenterVertically, modifier = Modifier.weight(1f)) {
|
||||||
UserBoostReaction(model.boosts[model.today()])
|
val boosts by model.boosts.collectAsState()
|
||||||
|
UserBoostReaction(boosts[model.today()])
|
||||||
}
|
}
|
||||||
|
|
||||||
Row(verticalAlignment = CenterVertically, modifier = Modifier.weight(1f)) {
|
Row(verticalAlignment = CenterVertically, modifier = Modifier.weight(1f)) {
|
||||||
UserLikeReaction(model.reactions[model.today()])
|
val reactions by model.reactions.collectAsState()
|
||||||
|
UserLikeReaction(reactions[model.today()])
|
||||||
}
|
}
|
||||||
|
|
||||||
Row(verticalAlignment = CenterVertically, modifier = Modifier.weight(1f)) {
|
Row(verticalAlignment = CenterVertically, modifier = Modifier.weight(1f)) {
|
||||||
UserZapReaction(model.zaps[model.today()])
|
val zaps by model.zaps.collectAsState()
|
||||||
|
UserZapReaction(zaps[model.today()])
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Stable
|
@Stable
|
||||||
class UserReactionsViewModel : ViewModel() {
|
class UserReactionsViewModel(val account: Account) : ViewModel() {
|
||||||
var user: User? = null
|
val user: User = account.userProfile()
|
||||||
|
|
||||||
var reactions by mutableStateOf<Map<String, Int>>(emptyMap())
|
private var _reactions = MutableStateFlow<Map<String, Int>>(emptyMap())
|
||||||
var boosts by mutableStateOf<Map<String, Int>>(emptyMap())
|
private var _boosts = MutableStateFlow<Map<String, Int>>(emptyMap())
|
||||||
var zaps by mutableStateOf<Map<String, BigDecimal>>(emptyMap())
|
private var _zaps = MutableStateFlow<Map<String, BigDecimal>>(emptyMap())
|
||||||
var replies by mutableStateOf<Map<String, Int>>(emptyMap())
|
private var _replies = MutableStateFlow<Map<String, Int>>(emptyMap())
|
||||||
|
|
||||||
var chartModel by mutableStateOf<ComposedChartEntryModel<ChartEntryModel>?>(null)
|
private var _chartModel = MutableStateFlow<ComposedChartEntryModel<ChartEntryModel>?>(null)
|
||||||
var axisLabels by mutableStateOf<List<String>>(emptyList())
|
private var _axisLabels = MutableStateFlow<List<String>>(emptyList())
|
||||||
|
|
||||||
|
val reactions = _reactions.asStateFlow()
|
||||||
|
val boosts = _boosts.asStateFlow()
|
||||||
|
val zaps = _zaps.asStateFlow()
|
||||||
|
val replies = _replies.asStateFlow()
|
||||||
|
|
||||||
|
val chartModel = _chartModel.asStateFlow()
|
||||||
|
val axisLabels = _axisLabels.asStateFlow()
|
||||||
|
|
||||||
private var takenIntoAccount = setOf<HexKey>()
|
private var takenIntoAccount = setOf<HexKey>()
|
||||||
private val sdf = DateTimeFormatter.ofPattern("yyyy-MM-dd") // SimpleDateFormat()
|
private val sdf = DateTimeFormatter.ofPattern("yyyy-MM-dd") // SimpleDateFormat()
|
||||||
|
|
||||||
fun load(baseUser: User) {
|
|
||||||
user = baseUser
|
|
||||||
reactions = emptyMap()
|
|
||||||
boosts = emptyMap()
|
|
||||||
zaps = emptyMap()
|
|
||||||
replies = emptyMap()
|
|
||||||
takenIntoAccount = emptySet()
|
|
||||||
}
|
|
||||||
|
|
||||||
fun formatDate(createAt: Long): String {
|
fun formatDate(createAt: Long): String {
|
||||||
return sdf.format(
|
return sdf.format(
|
||||||
Instant.ofEpochSecond(createAt)
|
Instant.ofEpochSecond(createAt)
|
||||||
@@ -131,8 +137,8 @@ class UserReactionsViewModel : ViewModel() {
|
|||||||
|
|
||||||
fun today() = sdf.format(LocalDateTime.now())
|
fun today() = sdf.format(LocalDateTime.now())
|
||||||
|
|
||||||
fun initializeSuspend() {
|
private suspend fun initializeSuspend() {
|
||||||
val currentUser = user?.pubkeyHex ?: return
|
val currentUser = user.pubkeyHex
|
||||||
|
|
||||||
val reactions = mutableMapOf<String, Int>()
|
val reactions = mutableMapOf<String, Int>()
|
||||||
val boosts = mutableMapOf<String, Int>()
|
val boosts = mutableMapOf<String, Int>()
|
||||||
@@ -172,21 +178,21 @@ class UserReactionsViewModel : ViewModel() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
this.takenIntoAccount = takenIntoAccount
|
this.takenIntoAccount = takenIntoAccount
|
||||||
this.reactions = reactions
|
this._reactions.emit(reactions)
|
||||||
this.replies = replies
|
this._replies.emit(replies)
|
||||||
this.zaps = zaps
|
this._zaps.emit(zaps)
|
||||||
this.boosts = boosts
|
this._boosts.emit(boosts)
|
||||||
|
|
||||||
refreshChartModel()
|
refreshChartModel()
|
||||||
}
|
}
|
||||||
|
|
||||||
fun addToStatsSuspend(newNotes: Set<Note>) {
|
suspend fun addToStatsSuspend(newNotes: Set<Note>) {
|
||||||
val currentUser = user?.pubkeyHex ?: return
|
val currentUser = user.pubkeyHex
|
||||||
|
|
||||||
val reactions = this.reactions.toMutableMap()
|
val reactions = this._reactions.value.toMutableMap()
|
||||||
val boosts = this.boosts.toMutableMap()
|
val boosts = this._boosts.value.toMutableMap()
|
||||||
val zaps = this.zaps.toMutableMap()
|
val zaps = this._zaps.value.toMutableMap()
|
||||||
val replies = this.replies.toMutableMap()
|
val replies = this._replies.value.toMutableMap()
|
||||||
val takenIntoAccount = this.takenIntoAccount.toMutableSet()
|
val takenIntoAccount = this.takenIntoAccount.toMutableSet()
|
||||||
var hasNewElements = false
|
var hasNewElements = false
|
||||||
|
|
||||||
@@ -227,16 +233,16 @@ class UserReactionsViewModel : ViewModel() {
|
|||||||
|
|
||||||
if (hasNewElements) {
|
if (hasNewElements) {
|
||||||
this.takenIntoAccount = takenIntoAccount
|
this.takenIntoAccount = takenIntoAccount
|
||||||
this.reactions = reactions
|
this._reactions.emit(reactions)
|
||||||
this.replies = replies
|
this._replies.emit(replies)
|
||||||
this.zaps = zaps
|
this._zaps.emit(zaps)
|
||||||
this.boosts = boosts
|
this._boosts.emit(boosts)
|
||||||
|
|
||||||
refreshChartModel()
|
refreshChartModel()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun refreshChartModel() {
|
private suspend fun refreshChartModel() {
|
||||||
val day = 24 * 60 * 60L
|
val day = 24 * 60 * 60L
|
||||||
val now = LocalDateTime.now()
|
val now = LocalDateTime.now()
|
||||||
val displayAxisFormatter = DateTimeFormatter.ofPattern("EEE")
|
val displayAxisFormatter = DateTimeFormatter.ofPattern("EEE")
|
||||||
@@ -244,36 +250,51 @@ class UserReactionsViewModel : ViewModel() {
|
|||||||
val dataAxisLabels = listOf(6, 5, 4, 3, 2, 1, 0).map { sdf.format(now.minusSeconds(day * it)) }
|
val dataAxisLabels = listOf(6, 5, 4, 3, 2, 1, 0).map { sdf.format(now.minusSeconds(day * it)) }
|
||||||
|
|
||||||
val listOfCountCurves = listOf(
|
val listOfCountCurves = listOf(
|
||||||
dataAxisLabels.mapIndexed { index, dateStr -> entryOf(index, replies[dateStr]?.toFloat() ?: 0f) },
|
dataAxisLabels.mapIndexed { index, dateStr -> entryOf(index, _replies.value[dateStr]?.toFloat() ?: 0f) },
|
||||||
dataAxisLabels.mapIndexed { index, dateStr -> entryOf(index, boosts[dateStr]?.toFloat() ?: 0f) },
|
dataAxisLabels.mapIndexed { index, dateStr -> entryOf(index, _boosts.value[dateStr]?.toFloat() ?: 0f) },
|
||||||
dataAxisLabels.mapIndexed { index, dateStr -> entryOf(index, reactions[dateStr]?.toFloat() ?: 0f) }
|
dataAxisLabels.mapIndexed { index, dateStr -> entryOf(index, _reactions.value[dateStr]?.toFloat() ?: 0f) }
|
||||||
)
|
)
|
||||||
|
|
||||||
val listOfValueCurves = listOf(
|
val listOfValueCurves = listOf(
|
||||||
dataAxisLabels.mapIndexed { index, dateStr -> entryOf(index, zaps[dateStr]?.toFloat() ?: 0f) }
|
dataAxisLabels.mapIndexed { index, dateStr -> entryOf(index, _zaps.value[dateStr]?.toFloat() ?: 0f) }
|
||||||
)
|
)
|
||||||
|
|
||||||
val chartEntryModelProducer1 = ChartEntryModelProducer(listOfCountCurves).getModel()
|
val chartEntryModelProducer1 = ChartEntryModelProducer(listOfCountCurves).getModel()
|
||||||
val chartEntryModelProducer2 = ChartEntryModelProducer(listOfValueCurves).getModel()
|
val chartEntryModelProducer2 = ChartEntryModelProducer(listOfValueCurves).getModel()
|
||||||
|
|
||||||
this.axisLabels = listOf(6, 5, 4, 3, 2, 1, 0).map { displayAxisFormatter.format(now.minusSeconds(day * it)) }
|
this._axisLabels.emit(listOf(6, 5, 4, 3, 2, 1, 0).map { displayAxisFormatter.format(now.minusSeconds(day * it)) })
|
||||||
this.chartModel = chartEntryModelProducer1.plus(chartEntryModelProducer2)
|
this._chartModel.emit(chartEntryModelProducer1.plus(chartEntryModelProducer2))
|
||||||
}
|
}
|
||||||
|
|
||||||
var collectorJob: Job? = null
|
var collectorJob: Job? = null
|
||||||
|
|
||||||
init {
|
init {
|
||||||
collectorJob = viewModelScope.launch(Dispatchers.IO) {
|
_reactions.value = emptyMap()
|
||||||
LocalCache.live.newEventBundles.collect { newNotes ->
|
_boosts.value = emptyMap()
|
||||||
addToStatsSuspend(newNotes)
|
_zaps.value = emptyMap()
|
||||||
|
_replies.value = emptyMap()
|
||||||
|
takenIntoAccount = emptySet()
|
||||||
|
|
||||||
|
viewModelScope.launch(Dispatchers.IO) {
|
||||||
|
initializeSuspend()
|
||||||
|
|
||||||
|
collectorJob = viewModelScope.launch(Dispatchers.IO) {
|
||||||
|
LocalCache.live.newEventBundles.collect { newNotes ->
|
||||||
|
addToStatsSuspend(newNotes)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onCleared() {
|
override fun onCleared() {
|
||||||
collectorJob?.cancel()
|
|
||||||
super.onCleared()
|
super.onCleared()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class Factory(val account: Account) : ViewModelProvider.Factory {
|
||||||
|
override fun <UserReactionsViewModel : ViewModel> create(modelClass: Class<UserReactionsViewModel>): UserReactionsViewModel {
|
||||||
|
return UserReactionsViewModel(account) as UserReactionsViewModel
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
|
|||||||
@@ -61,16 +61,22 @@ class ZapUserSetCard(val user: User, val zapEvents: ImmutableMap<Note, Note>) :
|
|||||||
|
|
||||||
@Immutable
|
@Immutable
|
||||||
class MultiSetCard(val note: Note, val boostEvents: ImmutableList<Note>, val likeEvents: ImmutableList<Note>, val zapEvents: ImmutableMap<Note, Note>) : Card() {
|
class MultiSetCard(val note: Note, val boostEvents: ImmutableList<Note>, val likeEvents: ImmutableList<Note>, val zapEvents: ImmutableMap<Note, Note>) : Card() {
|
||||||
val createdAt = maxOf(
|
val maxCreatedAt = maxOf(
|
||||||
zapEvents.maxOfOrNull { it.value.createdAt() ?: 0 } ?: 0,
|
zapEvents.maxOfOrNull { it.value.createdAt() ?: 0 } ?: 0,
|
||||||
likeEvents.maxOfOrNull { it.createdAt() ?: 0 } ?: 0,
|
likeEvents.maxOfOrNull { it.createdAt() ?: 0 } ?: 0,
|
||||||
boostEvents.maxOfOrNull { it.createdAt() ?: 0 } ?: 0
|
boostEvents.maxOfOrNull { it.createdAt() ?: 0 } ?: 0
|
||||||
)
|
)
|
||||||
|
|
||||||
|
val minCreatedAt = minOf(
|
||||||
|
zapEvents.minOfOrNull { it.value.createdAt() ?: Long.MAX_VALUE } ?: Long.MAX_VALUE,
|
||||||
|
likeEvents.minOfOrNull { it.createdAt() ?: Long.MAX_VALUE } ?: Long.MAX_VALUE,
|
||||||
|
boostEvents.minOfOrNull { it.createdAt() ?: Long.MAX_VALUE } ?: Long.MAX_VALUE
|
||||||
|
)
|
||||||
|
|
||||||
override fun createdAt(): Long {
|
override fun createdAt(): Long {
|
||||||
return createdAt
|
return maxCreatedAt
|
||||||
}
|
}
|
||||||
override fun id() = note.idHex + "X" + createdAt
|
override fun id() = note.idHex + "X" + maxCreatedAt + "X" + minCreatedAt
|
||||||
}
|
}
|
||||||
|
|
||||||
@Immutable
|
@Immutable
|
||||||
|
|||||||
@@ -39,72 +39,42 @@ import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
|||||||
|
|
||||||
@OptIn(ExperimentalMaterialApi::class)
|
@OptIn(ExperimentalMaterialApi::class)
|
||||||
@Composable
|
@Composable
|
||||||
fun CardFeedView(
|
fun RefresheableCardView(
|
||||||
viewModel: CardFeedViewModel,
|
viewModel: CardFeedViewModel,
|
||||||
accountViewModel: AccountViewModel,
|
accountViewModel: AccountViewModel,
|
||||||
nav: (String) -> Unit,
|
nav: (String) -> Unit,
|
||||||
routeForLastRead: String,
|
routeForLastRead: String,
|
||||||
scrollStateKey: String? = null,
|
scrollStateKey: String? = null,
|
||||||
scrollToTop: Boolean = false
|
enablePullRefresh: Boolean = true
|
||||||
) {
|
) {
|
||||||
val feedState by viewModel.feedContent.collectAsState()
|
|
||||||
|
|
||||||
var refreshing by remember { mutableStateOf(false) }
|
var refreshing by remember { mutableStateOf(false) }
|
||||||
val refresh = { refreshing = true; viewModel.invalidateData(); refreshing = false }
|
val refresh = { refreshing = true; viewModel.invalidateData(); refreshing = false }
|
||||||
val pullRefreshState = rememberPullRefreshState(refreshing, onRefresh = refresh)
|
val pullRefreshState = rememberPullRefreshState(refreshing, onRefresh = refresh)
|
||||||
|
|
||||||
Box(Modifier.fillMaxSize().pullRefresh(pullRefreshState)) {
|
val modifier = if (enablePullRefresh) {
|
||||||
Column(modifier = Modifier.fillMaxSize()) {
|
Modifier.pullRefresh(pullRefreshState)
|
||||||
Crossfade(
|
} else {
|
||||||
modifier = Modifier.fillMaxSize(),
|
Modifier
|
||||||
targetState = feedState,
|
}
|
||||||
animationSpec = tween(durationMillis = 100)
|
|
||||||
) { state ->
|
|
||||||
|
|
||||||
when (state) {
|
Box(modifier) {
|
||||||
is CardFeedState.Empty -> {
|
Column {
|
||||||
FeedEmpty {
|
SaveableCardFeedState(viewModel, accountViewModel, nav, routeForLastRead, scrollStateKey)
|
||||||
refreshing = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
is CardFeedState.FeedError -> {
|
|
||||||
FeedError(state.errorMessage) {
|
|
||||||
refreshing = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
is CardFeedState.Loaded -> {
|
|
||||||
if (refreshing) {
|
|
||||||
refreshing = false
|
|
||||||
}
|
|
||||||
|
|
||||||
FeedLoaded(
|
|
||||||
state = state,
|
|
||||||
accountViewModel = accountViewModel,
|
|
||||||
nav = nav,
|
|
||||||
routeForLastRead = routeForLastRead,
|
|
||||||
scrollStateKey = scrollStateKey,
|
|
||||||
scrollToTop = scrollToTop
|
|
||||||
)
|
|
||||||
}
|
|
||||||
CardFeedState.Loading -> {
|
|
||||||
LoadingFeed()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
PullRefreshIndicator(refreshing, pullRefreshState, Modifier.align(Alignment.TopCenter))
|
if (enablePullRefresh) {
|
||||||
|
PullRefreshIndicator(refreshing, pullRefreshState, Modifier.align(Alignment.TopCenter))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
private fun FeedLoaded(
|
private fun SaveableCardFeedState(
|
||||||
state: CardFeedState.Loaded,
|
viewModel: CardFeedViewModel,
|
||||||
accountViewModel: AccountViewModel,
|
accountViewModel: AccountViewModel,
|
||||||
nav: (String) -> Unit,
|
nav: (String) -> Unit,
|
||||||
routeForLastRead: String,
|
routeForLastRead: String,
|
||||||
scrollStateKey: String?,
|
scrollStateKey: String? = null
|
||||||
scrollToTop: Boolean = false
|
|
||||||
) {
|
) {
|
||||||
val listState = if (scrollStateKey != null) {
|
val listState = if (scrollStateKey != null) {
|
||||||
rememberForeverLazyListState(scrollStateKey)
|
rememberForeverLazyListState(scrollStateKey)
|
||||||
@@ -112,24 +82,76 @@ private fun FeedLoaded(
|
|||||||
rememberLazyListState()
|
rememberLazyListState()
|
||||||
}
|
}
|
||||||
|
|
||||||
if (scrollToTop) {
|
WatchScrollToTop(viewModel, listState)
|
||||||
LaunchedEffect(Unit) {
|
|
||||||
if (listState.firstVisibleItemIndex > 0 || listState.firstVisibleItemScrollOffset > 0) {
|
|
||||||
listState.scrollToItem(index = 0)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
LazyNotificationList(listState, state, routeForLastRead, accountViewModel, nav)
|
RenderCardFeed(viewModel, accountViewModel, listState, nav, routeForLastRead)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
private fun LazyNotificationList(
|
private fun WatchScrollToTop(
|
||||||
listState: LazyListState,
|
viewModel: CardFeedViewModel,
|
||||||
state: CardFeedState.Loaded,
|
listState: LazyListState
|
||||||
routeForLastRead: String,
|
) {
|
||||||
|
val scrollToTop by viewModel.scrollToTop.collectAsState()
|
||||||
|
|
||||||
|
LaunchedEffect(scrollToTop) {
|
||||||
|
if (scrollToTop > 0 && viewModel.scrolltoTopPending) {
|
||||||
|
listState.scrollToItem(index = 0)
|
||||||
|
viewModel.sentToTop()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun RenderCardFeed(
|
||||||
|
viewModel: CardFeedViewModel,
|
||||||
accountViewModel: AccountViewModel,
|
accountViewModel: AccountViewModel,
|
||||||
nav: (String) -> Unit
|
listState: LazyListState,
|
||||||
|
nav: (String) -> Unit,
|
||||||
|
routeForLastRead: String
|
||||||
|
) {
|
||||||
|
val feedState by viewModel.feedContent.collectAsState()
|
||||||
|
|
||||||
|
Crossfade(
|
||||||
|
modifier = Modifier.fillMaxSize(),
|
||||||
|
targetState = feedState,
|
||||||
|
animationSpec = tween(durationMillis = 100)
|
||||||
|
) { state ->
|
||||||
|
|
||||||
|
when (state) {
|
||||||
|
is CardFeedState.Empty -> {
|
||||||
|
FeedEmpty {
|
||||||
|
viewModel.invalidateData()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
is CardFeedState.FeedError -> {
|
||||||
|
FeedError(state.errorMessage) {
|
||||||
|
viewModel.invalidateData()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
is CardFeedState.Loaded -> {
|
||||||
|
FeedLoaded(
|
||||||
|
state = state,
|
||||||
|
listState = listState,
|
||||||
|
accountViewModel = accountViewModel,
|
||||||
|
nav = nav,
|
||||||
|
routeForLastRead = routeForLastRead
|
||||||
|
)
|
||||||
|
}
|
||||||
|
CardFeedState.Loading -> {
|
||||||
|
LoadingFeed()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun FeedLoaded(
|
||||||
|
state: CardFeedState.Loaded,
|
||||||
|
listState: LazyListState,
|
||||||
|
accountViewModel: AccountViewModel,
|
||||||
|
nav: (String) -> Unit,
|
||||||
|
routeForLastRead: String
|
||||||
) {
|
) {
|
||||||
LazyColumn(
|
LazyColumn(
|
||||||
modifier = remember { Modifier.fillMaxSize() },
|
modifier = remember { Modifier.fillMaxSize() },
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import android.util.Log
|
|||||||
import androidx.compose.runtime.Stable
|
import androidx.compose.runtime.Stable
|
||||||
import androidx.compose.runtime.mutableStateOf
|
import androidx.compose.runtime.mutableStateOf
|
||||||
import androidx.lifecycle.ViewModel
|
import androidx.lifecycle.ViewModel
|
||||||
|
import androidx.lifecycle.ViewModelProvider
|
||||||
import androidx.lifecycle.viewModelScope
|
import androidx.lifecycle.viewModelScope
|
||||||
import com.vitorpamplona.amethyst.model.Account
|
import com.vitorpamplona.amethyst.model.Account
|
||||||
import com.vitorpamplona.amethyst.model.LocalCache
|
import com.vitorpamplona.amethyst.model.LocalCache
|
||||||
@@ -35,12 +36,35 @@ import kotlin.time.ExperimentalTime
|
|||||||
import kotlin.time.measureTimedValue
|
import kotlin.time.measureTimedValue
|
||||||
|
|
||||||
@Stable
|
@Stable
|
||||||
class NotificationViewModel : CardFeedViewModel(NotificationFeedFilter)
|
class NotificationViewModel(val account: Account) : CardFeedViewModel(NotificationFeedFilter(account)) {
|
||||||
|
class Factory(val account: Account) : ViewModelProvider.Factory {
|
||||||
|
override fun <NotificationViewModel : ViewModel> create(modelClass: Class<NotificationViewModel>): NotificationViewModel {
|
||||||
|
return NotificationViewModel(account) as NotificationViewModel
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Stable
|
||||||
open class CardFeedViewModel(val localFilter: FeedFilter<Note>) : ViewModel() {
|
open class CardFeedViewModel(val localFilter: FeedFilter<Note>) : ViewModel() {
|
||||||
private val _feedContent = MutableStateFlow<CardFeedState>(CardFeedState.Loading)
|
private val _feedContent = MutableStateFlow<CardFeedState>(CardFeedState.Loading)
|
||||||
val feedContent = _feedContent.asStateFlow()
|
val feedContent = _feedContent.asStateFlow()
|
||||||
|
|
||||||
|
// Simple counter that changes when it needs to invalidate everything
|
||||||
|
private val _scrollToTop = MutableStateFlow<Int>(0)
|
||||||
|
val scrollToTop = _scrollToTop.asStateFlow()
|
||||||
|
var scrolltoTopPending = false
|
||||||
|
|
||||||
|
suspend fun sendToTop() {
|
||||||
|
if (scrolltoTopPending) return
|
||||||
|
|
||||||
|
scrolltoTopPending = true
|
||||||
|
_scrollToTop.emit(_scrollToTop.value + 1)
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun sentToTop() {
|
||||||
|
scrolltoTopPending = false
|
||||||
|
}
|
||||||
|
|
||||||
private var lastAccount: Account? = null
|
private var lastAccount: Account? = null
|
||||||
private var lastNotes: Set<Note>? = null
|
private var lastNotes: Set<Note>? = null
|
||||||
|
|
||||||
@@ -150,7 +174,10 @@ open class CardFeedViewModel(val localFilter: FeedFilter<Note>) : ViewModel() {
|
|||||||
val reactionsInCard = reactionsPerEvent[baseNote] ?: emptyList()
|
val reactionsInCard = reactionsPerEvent[baseNote] ?: emptyList()
|
||||||
val zapsInCard = zapsPerEvent[baseNote] ?: emptyMap()
|
val zapsInCard = zapsPerEvent[baseNote] ?: emptyMap()
|
||||||
|
|
||||||
val singleList = (boostsInCard + zapsInCard.values + reactionsInCard).sortedWith(compareBy({ it.createdAt() }, { it.idHex })).reversed()
|
val singleList = (boostsInCard + zapsInCard.values + reactionsInCard)
|
||||||
|
.sortedWith(compareBy({ it.createdAt() }, { it.idHex }))
|
||||||
|
.reversed()
|
||||||
|
|
||||||
singleList.chunked(50).map { chunk ->
|
singleList.chunked(50).map { chunk ->
|
||||||
MultiSetCard(
|
MultiSetCard(
|
||||||
baseNote,
|
baseNote,
|
||||||
@@ -234,19 +261,32 @@ open class CardFeedViewModel(val localFilter: FeedFilter<Note>) : ViewModel() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@OptIn(ExperimentalTime::class)
|
private val bundler = BundledUpdate(1000, Dispatchers.IO)
|
||||||
private val bundler = BundledUpdate(1000, Dispatchers.IO) {
|
|
||||||
// adds the time to perform the refresh into this delay
|
|
||||||
// holding off new updates in case of heavy refresh routines.
|
|
||||||
val (value, elapsed) = measureTimedValue {
|
|
||||||
refreshSuspended()
|
|
||||||
}
|
|
||||||
Log.d("Time", "${this.javaClass.simpleName} Card update $elapsed")
|
|
||||||
}
|
|
||||||
private val bundlerInsert = BundledInsert<Set<Note>>(1000, Dispatchers.IO)
|
private val bundlerInsert = BundledInsert<Set<Note>>(1000, Dispatchers.IO)
|
||||||
|
|
||||||
|
@OptIn(ExperimentalTime::class)
|
||||||
fun invalidateData(ignoreIfDoing: Boolean = false) {
|
fun invalidateData(ignoreIfDoing: Boolean = false) {
|
||||||
bundler.invalidate(ignoreIfDoing)
|
bundler.invalidate(ignoreIfDoing) {
|
||||||
|
// adds the time to perform the refresh into this delay
|
||||||
|
// holding off new updates in case of heavy refresh routines.
|
||||||
|
val (value, elapsed) = measureTimedValue {
|
||||||
|
refreshSuspended()
|
||||||
|
}
|
||||||
|
Log.d("Time", "${this.javaClass.simpleName} Card update $elapsed")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@OptIn(ExperimentalTime::class)
|
||||||
|
fun invalidateDataAndSendToTop(ignoreIfDoing: Boolean = false) {
|
||||||
|
bundler.invalidate(ignoreIfDoing) {
|
||||||
|
// adds the time to perform the refresh into this delay
|
||||||
|
// holding off new updates in case of heavy refresh routines.
|
||||||
|
val (value, elapsed) = measureTimedValue {
|
||||||
|
refreshSuspended()
|
||||||
|
sendToTop()
|
||||||
|
}
|
||||||
|
Log.d("Time", "${this.javaClass.simpleName} Card update $elapsed")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@OptIn(ExperimentalTime::class)
|
@OptIn(ExperimentalTime::class)
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import androidx.compose.foundation.layout.PaddingValues
|
|||||||
import androidx.compose.foundation.layout.fillMaxHeight
|
import androidx.compose.foundation.layout.fillMaxHeight
|
||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
import androidx.compose.foundation.lazy.LazyColumn
|
import androidx.compose.foundation.lazy.LazyColumn
|
||||||
|
import androidx.compose.foundation.lazy.LazyListState
|
||||||
import androidx.compose.foundation.lazy.itemsIndexed
|
import androidx.compose.foundation.lazy.itemsIndexed
|
||||||
import androidx.compose.foundation.lazy.rememberLazyListState
|
import androidx.compose.foundation.lazy.rememberLazyListState
|
||||||
import androidx.compose.material.Button
|
import androidx.compose.material.Button
|
||||||
@@ -35,17 +36,14 @@ import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
|||||||
|
|
||||||
@OptIn(ExperimentalMaterialApi::class)
|
@OptIn(ExperimentalMaterialApi::class)
|
||||||
@Composable
|
@Composable
|
||||||
fun FeedView(
|
fun RefresheableView(
|
||||||
viewModel: FeedViewModel,
|
viewModel: FeedViewModel,
|
||||||
accountViewModel: AccountViewModel,
|
accountViewModel: AccountViewModel,
|
||||||
nav: (String) -> Unit,
|
nav: (String) -> Unit,
|
||||||
routeForLastRead: String?,
|
routeForLastRead: String?,
|
||||||
scrollStateKey: String? = null,
|
scrollStateKey: String? = null,
|
||||||
scrollToTop: Boolean = false,
|
|
||||||
enablePullRefresh: Boolean = true
|
enablePullRefresh: Boolean = true
|
||||||
) {
|
) {
|
||||||
val feedState by viewModel.feedContent.collectAsState()
|
|
||||||
|
|
||||||
var refreshing by remember { mutableStateOf(false) }
|
var refreshing by remember { mutableStateOf(false) }
|
||||||
val refresh = { refreshing = true; viewModel.invalidateData(); refreshing = false }
|
val refresh = { refreshing = true; viewModel.invalidateData(); refreshing = false }
|
||||||
val pullRefreshState = rememberPullRefreshState(refreshing, onRefresh = refresh)
|
val pullRefreshState = rememberPullRefreshState(refreshing, onRefresh = refresh)
|
||||||
@@ -58,42 +56,7 @@ fun FeedView(
|
|||||||
|
|
||||||
Box(modifier) {
|
Box(modifier) {
|
||||||
Column {
|
Column {
|
||||||
Crossfade(
|
SaveableFeedState(viewModel, accountViewModel, nav, routeForLastRead, scrollStateKey)
|
||||||
targetState = feedState,
|
|
||||||
animationSpec = tween(durationMillis = 100)
|
|
||||||
) { state ->
|
|
||||||
when (state) {
|
|
||||||
is FeedState.Empty -> {
|
|
||||||
FeedEmpty {
|
|
||||||
refresh()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
is FeedState.FeedError -> {
|
|
||||||
FeedError(state.errorMessage) {
|
|
||||||
refresh()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
is FeedState.Loaded -> {
|
|
||||||
if (refreshing) {
|
|
||||||
refreshing = false
|
|
||||||
}
|
|
||||||
FeedLoaded(
|
|
||||||
state,
|
|
||||||
routeForLastRead,
|
|
||||||
accountViewModel,
|
|
||||||
nav,
|
|
||||||
scrollStateKey,
|
|
||||||
scrollToTop
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
is FeedState.Loading -> {
|
|
||||||
LoadingFeed()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (enablePullRefresh) {
|
if (enablePullRefresh) {
|
||||||
@@ -103,13 +66,12 @@ fun FeedView(
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
private fun FeedLoaded(
|
private fun SaveableFeedState(
|
||||||
state: FeedState.Loaded,
|
viewModel: FeedViewModel,
|
||||||
routeForLastRead: String?,
|
|
||||||
accountViewModel: AccountViewModel,
|
accountViewModel: AccountViewModel,
|
||||||
nav: (String) -> Unit,
|
nav: (String) -> Unit,
|
||||||
scrollStateKey: String?,
|
routeForLastRead: String?,
|
||||||
scrollToTop: Boolean = false
|
scrollStateKey: String? = null
|
||||||
) {
|
) {
|
||||||
val listState = if (scrollStateKey != null) {
|
val listState = if (scrollStateKey != null) {
|
||||||
rememberForeverLazyListState(scrollStateKey)
|
rememberForeverLazyListState(scrollStateKey)
|
||||||
@@ -117,14 +79,78 @@ private fun FeedLoaded(
|
|||||||
rememberLazyListState()
|
rememberLazyListState()
|
||||||
}
|
}
|
||||||
|
|
||||||
if (scrollToTop) {
|
WatchScrollToTop(viewModel, listState)
|
||||||
LaunchedEffect(Unit) {
|
|
||||||
if (listState.firstVisibleItemIndex > 0 || listState.firstVisibleItemScrollOffset > 0) {
|
RenderFeed(viewModel, accountViewModel, listState, nav, routeForLastRead)
|
||||||
listState.scrollToItem(index = 0)
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun RenderFeed(
|
||||||
|
viewModel: FeedViewModel,
|
||||||
|
accountViewModel: AccountViewModel,
|
||||||
|
listState: LazyListState,
|
||||||
|
nav: (String) -> Unit,
|
||||||
|
routeForLastRead: String?
|
||||||
|
) {
|
||||||
|
val feedState by viewModel.feedContent.collectAsState()
|
||||||
|
|
||||||
|
Crossfade(
|
||||||
|
targetState = feedState,
|
||||||
|
animationSpec = tween(durationMillis = 100)
|
||||||
|
) { state ->
|
||||||
|
when (state) {
|
||||||
|
is FeedState.Empty -> {
|
||||||
|
FeedEmpty {
|
||||||
|
viewModel.invalidateData()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
is FeedState.FeedError -> {
|
||||||
|
FeedError(state.errorMessage) {
|
||||||
|
viewModel.invalidateData()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
is FeedState.Loaded -> {
|
||||||
|
FeedLoaded(
|
||||||
|
state,
|
||||||
|
listState,
|
||||||
|
routeForLastRead,
|
||||||
|
accountViewModel,
|
||||||
|
nav
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
is FeedState.Loading -> {
|
||||||
|
LoadingFeed()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun WatchScrollToTop(
|
||||||
|
viewModel: FeedViewModel,
|
||||||
|
listState: LazyListState
|
||||||
|
) {
|
||||||
|
val scrollToTop by viewModel.scrollToTop.collectAsState()
|
||||||
|
|
||||||
|
LaunchedEffect(scrollToTop) {
|
||||||
|
if (scrollToTop > 0 && viewModel.scrolltoTopPending) {
|
||||||
|
listState.scrollToItem(index = 0)
|
||||||
|
viewModel.sentToTop()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun FeedLoaded(
|
||||||
|
state: FeedState.Loaded,
|
||||||
|
listState: LazyListState,
|
||||||
|
routeForLastRead: String?,
|
||||||
|
accountViewModel: AccountViewModel,
|
||||||
|
nav: (String) -> Unit
|
||||||
|
) {
|
||||||
val baseModifier = remember {
|
val baseModifier = remember {
|
||||||
Modifier
|
Modifier
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,8 +1,11 @@
|
|||||||
package com.vitorpamplona.amethyst.ui.screen
|
package com.vitorpamplona.amethyst.ui.screen
|
||||||
|
|
||||||
|
import androidx.compose.runtime.Stable
|
||||||
import androidx.compose.runtime.mutableStateOf
|
import androidx.compose.runtime.mutableStateOf
|
||||||
import androidx.lifecycle.ViewModel
|
import androidx.lifecycle.ViewModel
|
||||||
|
import androidx.lifecycle.ViewModelProvider
|
||||||
import androidx.lifecycle.viewModelScope
|
import androidx.lifecycle.viewModelScope
|
||||||
|
import com.vitorpamplona.amethyst.model.Account
|
||||||
import com.vitorpamplona.amethyst.model.LocalCache
|
import com.vitorpamplona.amethyst.model.LocalCache
|
||||||
import com.vitorpamplona.amethyst.model.Note
|
import com.vitorpamplona.amethyst.model.Note
|
||||||
import com.vitorpamplona.amethyst.ui.components.BundledInsert
|
import com.vitorpamplona.amethyst.ui.components.BundledInsert
|
||||||
@@ -37,26 +40,90 @@ import kotlinx.coroutines.launch
|
|||||||
|
|
||||||
class NostrChannelFeedViewModel : FeedViewModel(ChannelFeedFilter)
|
class NostrChannelFeedViewModel : FeedViewModel(ChannelFeedFilter)
|
||||||
class NostrChatRoomFeedViewModel : FeedViewModel(ChatroomFeedFilter)
|
class NostrChatRoomFeedViewModel : FeedViewModel(ChatroomFeedFilter)
|
||||||
class NostrGlobalFeedViewModel : FeedViewModel(GlobalFeedFilter)
|
class NostrGlobalFeedViewModel(val account: Account) : FeedViewModel(GlobalFeedFilter(account)) {
|
||||||
class NostrVideoFeedViewModel : FeedViewModel(VideoFeedFilter)
|
class Factory(val account: Account) : ViewModelProvider.Factory {
|
||||||
class NostrThreadFeedViewModel : FeedViewModel(ThreadFeedFilter)
|
override fun <NostrGlobalFeedViewModel : ViewModel> create(modelClass: Class<NostrGlobalFeedViewModel>): NostrGlobalFeedViewModel {
|
||||||
|
return NostrGlobalFeedViewModel(account) as NostrGlobalFeedViewModel
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
class NostrVideoFeedViewModel(val account: Account) : FeedViewModel(VideoFeedFilter(account)) {
|
||||||
|
class Factory(val account: Account) : ViewModelProvider.Factory {
|
||||||
|
override fun <NostrVideoFeedViewModel : ViewModel> create(modelClass: Class<NostrVideoFeedViewModel>): NostrVideoFeedViewModel {
|
||||||
|
return NostrVideoFeedViewModel(account) as NostrVideoFeedViewModel
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
class NostrThreadFeedViewModel(val noteId: String) : FeedViewModel(ThreadFeedFilter(noteId)) {
|
||||||
|
class Factory(val noteId: String) : ViewModelProvider.Factory {
|
||||||
|
override fun <NostrThreadFeedViewModel : ViewModel> create(modelClass: Class<NostrThreadFeedViewModel>): NostrThreadFeedViewModel {
|
||||||
|
return NostrThreadFeedViewModel(noteId) as NostrThreadFeedViewModel
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
class NostrHashtagFeedViewModel : FeedViewModel(HashtagFeedFilter)
|
class NostrHashtagFeedViewModel : FeedViewModel(HashtagFeedFilter)
|
||||||
class NostrUserProfileNewThreadsFeedViewModel : FeedViewModel(UserProfileNewThreadFeedFilter)
|
class NostrUserProfileNewThreadsFeedViewModel : FeedViewModel(UserProfileNewThreadFeedFilter)
|
||||||
class NostrUserProfileConversationsFeedViewModel : FeedViewModel(UserProfileConversationsFeedFilter)
|
class NostrUserProfileConversationsFeedViewModel : FeedViewModel(UserProfileConversationsFeedFilter)
|
||||||
class NostrUserProfileReportFeedViewModel : FeedViewModel(UserProfileReportsFeedFilter)
|
class NostrUserProfileReportFeedViewModel : FeedViewModel(UserProfileReportsFeedFilter)
|
||||||
class NostrUserProfileBookmarksFeedViewModel : FeedViewModel(UserProfileBookmarksFeedFilter)
|
class NostrUserProfileBookmarksFeedViewModel : FeedViewModel(UserProfileBookmarksFeedFilter)
|
||||||
class NostrChatroomListKnownFeedViewModel : FeedViewModel(ChatroomListKnownFeedFilter)
|
class NostrChatroomListKnownFeedViewModel(val account: Account) : FeedViewModel(ChatroomListKnownFeedFilter(account)) {
|
||||||
class NostrChatroomListNewFeedViewModel : FeedViewModel(ChatroomListNewFeedFilter)
|
class Factory(val account: Account) : ViewModelProvider.Factory {
|
||||||
class NostrHomeFeedViewModel : FeedViewModel(HomeNewThreadFeedFilter)
|
override fun <NostrChatroomListKnownFeedViewModel : ViewModel> create(modelClass: Class<NostrChatroomListKnownFeedViewModel>): NostrChatroomListKnownFeedViewModel {
|
||||||
class NostrHomeRepliesFeedViewModel : FeedViewModel(HomeConversationsFeedFilter)
|
return NostrChatroomListKnownFeedViewModel(account) as NostrChatroomListKnownFeedViewModel
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
class NostrChatroomListNewFeedViewModel(val account: Account) : FeedViewModel(ChatroomListNewFeedFilter(account)) {
|
||||||
|
class Factory(val account: Account) : ViewModelProvider.Factory {
|
||||||
|
override fun <NostrChatroomListNewFeedViewModel : ViewModel> create(modelClass: Class<NostrChatroomListNewFeedViewModel>): NostrChatroomListNewFeedViewModel {
|
||||||
|
return NostrChatroomListNewFeedViewModel(account) as NostrChatroomListNewFeedViewModel
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Stable
|
||||||
|
class NostrHomeFeedViewModel(val account: Account) : FeedViewModel(HomeNewThreadFeedFilter(account)) {
|
||||||
|
class Factory(val account: Account) : ViewModelProvider.Factory {
|
||||||
|
override fun <NostrHomeFeedViewModel : ViewModel> create(modelClass: Class<NostrHomeFeedViewModel>): NostrHomeFeedViewModel {
|
||||||
|
return NostrHomeFeedViewModel(account) as NostrHomeFeedViewModel
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Stable
|
||||||
|
class NostrHomeRepliesFeedViewModel(val account: Account) : FeedViewModel(HomeConversationsFeedFilter(account)) {
|
||||||
|
class Factory(val account: Account) : ViewModelProvider.Factory {
|
||||||
|
override fun <NostrHomeRepliesFeedViewModel : ViewModel> create(modelClass: Class<NostrHomeRepliesFeedViewModel>): NostrHomeRepliesFeedViewModel {
|
||||||
|
return NostrHomeRepliesFeedViewModel(account) as NostrHomeRepliesFeedViewModel
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
class NostrBookmarkPublicFeedViewModel : FeedViewModel(BookmarkPublicFeedFilter)
|
class NostrBookmarkPublicFeedViewModel : FeedViewModel(BookmarkPublicFeedFilter)
|
||||||
class NostrBookmarkPrivateFeedViewModel : FeedViewModel(BookmarkPrivateFeedFilter)
|
class NostrBookmarkPrivateFeedViewModel : FeedViewModel(BookmarkPrivateFeedFilter)
|
||||||
|
|
||||||
|
@Stable
|
||||||
abstract class FeedViewModel(val localFilter: FeedFilter<Note>) : ViewModel() {
|
abstract class FeedViewModel(val localFilter: FeedFilter<Note>) : ViewModel() {
|
||||||
private val _feedContent = MutableStateFlow<FeedState>(FeedState.Loading)
|
private val _feedContent = MutableStateFlow<FeedState>(FeedState.Loading)
|
||||||
val feedContent = _feedContent.asStateFlow()
|
val feedContent = _feedContent.asStateFlow()
|
||||||
|
|
||||||
|
// Simple counter that changes when it needs to invalidate everything
|
||||||
|
private val _scrollToTop = MutableStateFlow<Int>(0)
|
||||||
|
val scrollToTop = _scrollToTop.asStateFlow()
|
||||||
|
var scrolltoTopPending = false
|
||||||
|
|
||||||
|
suspend fun sendToTop() {
|
||||||
|
if (scrolltoTopPending) return
|
||||||
|
|
||||||
|
scrolltoTopPending = true
|
||||||
|
_scrollToTop.emit(_scrollToTop.value + 1)
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun sentToTop() {
|
||||||
|
scrolltoTopPending = false
|
||||||
|
}
|
||||||
|
|
||||||
fun newListFromDataSource(): ImmutableList<Note> {
|
fun newListFromDataSource(): ImmutableList<Note> {
|
||||||
return localFilter.loadTop().toImmutableList()
|
return localFilter.loadTop().toImmutableList()
|
||||||
}
|
}
|
||||||
@@ -119,15 +186,24 @@ abstract class FeedViewModel(val localFilter: FeedFilter<Note>) : ViewModel() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private val bundler = BundledUpdate(250, Dispatchers.IO) {
|
private val bundler = BundledUpdate(250, Dispatchers.IO)
|
||||||
// adds the time to perform the refresh into this delay
|
|
||||||
// holding off new updates in case of heavy refresh routines.
|
|
||||||
refreshSuspended()
|
|
||||||
}
|
|
||||||
private val bundlerInsert = BundledInsert<Set<Note>>(250, Dispatchers.IO)
|
private val bundlerInsert = BundledInsert<Set<Note>>(250, Dispatchers.IO)
|
||||||
|
|
||||||
fun invalidateData(ignoreIfDoing: Boolean = false) {
|
fun invalidateData(ignoreIfDoing: Boolean = false) {
|
||||||
bundler.invalidate(ignoreIfDoing)
|
bundler.invalidate(ignoreIfDoing) {
|
||||||
|
// adds the time to perform the refresh into this delay
|
||||||
|
// holding off new updates in case of heavy refresh routines.
|
||||||
|
refreshSuspended()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun invalidateDataAndSendToTop(ignoreIfDoing: Boolean = false) {
|
||||||
|
bundler.invalidate(ignoreIfDoing) {
|
||||||
|
// adds the time to perform the refresh into this delay
|
||||||
|
// holding off new updates in case of heavy refresh routines.
|
||||||
|
refreshSuspended()
|
||||||
|
sendToTop()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun invalidateInsertData(newItems: Set<Note>) {
|
fun invalidateInsertData(newItems: Set<Note>) {
|
||||||
@@ -136,7 +212,7 @@ abstract class FeedViewModel(val localFilter: FeedFilter<Note>) : ViewModel() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
var collectorJob: Job? = null
|
private var collectorJob: Job? = null
|
||||||
|
|
||||||
init {
|
init {
|
||||||
collectorJob = viewModelScope.launch(Dispatchers.IO) {
|
collectorJob = viewModelScope.launch(Dispatchers.IO) {
|
||||||
|
|||||||
@@ -58,14 +58,14 @@ open class LnZapFeedViewModel(val dataSource: FeedFilter<Pair<Note, Note>>) : Vi
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private val bundler = BundledUpdate(250, Dispatchers.IO) {
|
private val bundler = BundledUpdate(250, Dispatchers.IO)
|
||||||
// adds the time to perform the refresh into this delay
|
|
||||||
// holding off new updates in case of heavy refresh routines.
|
|
||||||
refreshSuspended()
|
|
||||||
}
|
|
||||||
|
|
||||||
fun invalidateData() {
|
fun invalidateData() {
|
||||||
bundler.invalidate()
|
bundler.invalidate() {
|
||||||
|
// adds the time to perform the refresh into this delay
|
||||||
|
// holding off new updates in case of heavy refresh routines.
|
||||||
|
refreshSuspended()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
var collectorJob: Job? = null
|
var collectorJob: Job? = null
|
||||||
|
|||||||
@@ -13,7 +13,6 @@ import androidx.compose.material.pullrefresh.rememberPullRefreshState
|
|||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.collectAsState
|
import androidx.compose.runtime.collectAsState
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.runtime.livedata.observeAsState
|
|
||||||
import androidx.compose.runtime.mutableStateOf
|
import androidx.compose.runtime.mutableStateOf
|
||||||
import androidx.compose.runtime.remember
|
import androidx.compose.runtime.remember
|
||||||
import androidx.compose.runtime.setValue
|
import androidx.compose.runtime.setValue
|
||||||
@@ -83,14 +82,14 @@ class RelayFeedViewModel : ViewModel() {
|
|||||||
currentUser = null
|
currentUser = null
|
||||||
}
|
}
|
||||||
|
|
||||||
private val bundler = BundledUpdate(250, Dispatchers.IO) {
|
private val bundler = BundledUpdate(250, Dispatchers.IO)
|
||||||
// adds the time to perform the refresh into this delay
|
|
||||||
// holding off new updates in case of heavy refresh routines.
|
|
||||||
refreshSuspended()
|
|
||||||
}
|
|
||||||
|
|
||||||
fun invalidateData() {
|
fun invalidateData() {
|
||||||
bundler.invalidate()
|
bundler.invalidate() {
|
||||||
|
// adds the time to perform the refresh into this delay
|
||||||
|
// holding off new updates in case of heavy refresh routines.
|
||||||
|
refreshSuspended()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -101,9 +100,6 @@ fun RelayFeedView(
|
|||||||
accountViewModel: AccountViewModel,
|
accountViewModel: AccountViewModel,
|
||||||
enablePullRefresh: Boolean = true
|
enablePullRefresh: Boolean = true
|
||||||
) {
|
) {
|
||||||
val accountState by accountViewModel.accountLiveData.observeAsState()
|
|
||||||
val account = accountState?.account ?: return
|
|
||||||
|
|
||||||
val feedState by viewModel.feedContent.collectAsState()
|
val feedState by viewModel.feedContent.collectAsState()
|
||||||
|
|
||||||
var wantsToAddRelay by remember {
|
var wantsToAddRelay by remember {
|
||||||
@@ -111,7 +107,7 @@ fun RelayFeedView(
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (wantsToAddRelay.isNotEmpty()) {
|
if (wantsToAddRelay.isNotEmpty()) {
|
||||||
NewRelayListView({ wantsToAddRelay = "" }, account, wantsToAddRelay)
|
NewRelayListView({ wantsToAddRelay = "" }, accountViewModel, wantsToAddRelay)
|
||||||
}
|
}
|
||||||
|
|
||||||
var refreshing by remember { mutableStateOf(false) }
|
var refreshing by remember { mutableStateOf(false) }
|
||||||
|
|||||||
@@ -21,6 +21,10 @@ object ScrollStateKeys {
|
|||||||
val HOME_REPLIES = Route.Home.base + "FollowsReplies"
|
val HOME_REPLIES = Route.Home.base + "FollowsReplies"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
object PagerStateKeys {
|
||||||
|
const val HOME_SCREEN = "Home"
|
||||||
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun rememberForeverLazyListState(
|
fun rememberForeverLazyListState(
|
||||||
key: String,
|
key: String,
|
||||||
@@ -62,7 +66,7 @@ fun rememberForeverPagerState(
|
|||||||
savedOffset
|
savedOffset
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
DisposableEffect(Unit) {
|
DisposableEffect(scrollState) {
|
||||||
onDispose {
|
onDispose {
|
||||||
val lastIndex = scrollState.currentPage
|
val lastIndex = scrollState.currentPage
|
||||||
val lastOffset = scrollState.currentPageOffsetFraction
|
val lastOffset = scrollState.currentPageOffsetFraction
|
||||||
|
|||||||
@@ -62,14 +62,14 @@ open class UserFeedViewModel(val dataSource: FeedFilter<User>) : ViewModel() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private val bundler = BundledUpdate(250, Dispatchers.IO) {
|
private val bundler = BundledUpdate(250, Dispatchers.IO)
|
||||||
// adds the time to perform the refresh into this delay
|
|
||||||
// holding off new updates in case of heavy refresh routines.
|
|
||||||
refreshSuspended()
|
|
||||||
}
|
|
||||||
|
|
||||||
fun invalidateData() {
|
fun invalidateData() {
|
||||||
bundler.invalidate()
|
bundler.invalidate() {
|
||||||
|
// adds the time to perform the refresh into this delay
|
||||||
|
// holding off new updates in case of heavy refresh routines.
|
||||||
|
refreshSuspended()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
var collectorJob: Job? = null
|
var collectorJob: Job? = null
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ package com.vitorpamplona.amethyst.ui.screen.loggedIn
|
|||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import androidx.compose.runtime.Immutable
|
import androidx.compose.runtime.Stable
|
||||||
import androidx.core.content.ContextCompat
|
import androidx.core.content.ContextCompat
|
||||||
import androidx.lifecycle.LiveData
|
import androidx.lifecycle.LiveData
|
||||||
import androidx.lifecycle.ViewModel
|
import androidx.lifecycle.ViewModel
|
||||||
@@ -26,8 +26,8 @@ import kotlinx.coroutines.launch
|
|||||||
import java.math.BigDecimal
|
import java.math.BigDecimal
|
||||||
import java.util.Locale
|
import java.util.Locale
|
||||||
|
|
||||||
@Immutable
|
@Stable
|
||||||
class AccountViewModel(private val account: Account) : ViewModel() {
|
class AccountViewModel(val account: Account) : ViewModel() {
|
||||||
val accountLiveData: LiveData<AccountState> = account.live.map { it }
|
val accountLiveData: LiveData<AccountState> = account.live.map { it }
|
||||||
val accountLanguagesLiveData: LiveData<AccountState> = account.liveLanguages.map { it }
|
val accountLanguagesLiveData: LiveData<AccountState> = account.liveLanguages.map { it }
|
||||||
|
|
||||||
@@ -249,6 +249,10 @@ class AccountViewModel(private val account: Account) : ViewModel() {
|
|||||||
account.updateShowSensitiveContent(null)
|
account.updateShowSensitiveContent(null)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun defaultZapType(): LnZapEvent.ZapType {
|
||||||
|
return account.defaultZapType
|
||||||
|
}
|
||||||
|
|
||||||
class Factory(val account: Account) : ViewModelProvider.Factory {
|
class Factory(val account: Account) : ViewModelProvider.Factory {
|
||||||
override fun <AccountViewModel : ViewModel> create(modelClass: Class<AccountViewModel>): AccountViewModel {
|
override fun <AccountViewModel : ViewModel> create(modelClass: Class<AccountViewModel>): AccountViewModel {
|
||||||
return AccountViewModel(account) as AccountViewModel
|
return AccountViewModel(account) as AccountViewModel
|
||||||
|
|||||||
+3
-3
@@ -22,9 +22,9 @@ import androidx.lifecycle.viewmodel.compose.viewModel
|
|||||||
import com.vitorpamplona.amethyst.R
|
import com.vitorpamplona.amethyst.R
|
||||||
import com.vitorpamplona.amethyst.ui.dal.BookmarkPrivateFeedFilter
|
import com.vitorpamplona.amethyst.ui.dal.BookmarkPrivateFeedFilter
|
||||||
import com.vitorpamplona.amethyst.ui.dal.BookmarkPublicFeedFilter
|
import com.vitorpamplona.amethyst.ui.dal.BookmarkPublicFeedFilter
|
||||||
import com.vitorpamplona.amethyst.ui.screen.FeedView
|
|
||||||
import com.vitorpamplona.amethyst.ui.screen.NostrBookmarkPrivateFeedViewModel
|
import com.vitorpamplona.amethyst.ui.screen.NostrBookmarkPrivateFeedViewModel
|
||||||
import com.vitorpamplona.amethyst.ui.screen.NostrBookmarkPublicFeedViewModel
|
import com.vitorpamplona.amethyst.ui.screen.NostrBookmarkPublicFeedViewModel
|
||||||
|
import com.vitorpamplona.amethyst.ui.screen.RefresheableView
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
|
|
||||||
@OptIn(ExperimentalFoundationApi::class)
|
@OptIn(ExperimentalFoundationApi::class)
|
||||||
@@ -73,8 +73,8 @@ fun BookmarkListScreen(accountViewModel: AccountViewModel, nav: (String) -> Unit
|
|||||||
}
|
}
|
||||||
HorizontalPager(pageCount = 2, state = pagerState) { page ->
|
HorizontalPager(pageCount = 2, state = pagerState) { page ->
|
||||||
when (page) {
|
when (page) {
|
||||||
0 -> FeedView(privateFeedViewModel, accountViewModel, nav, null)
|
0 -> RefresheableView(privateFeedViewModel, accountViewModel, nav, null)
|
||||||
1 -> FeedView(publicFeedViewModel, accountViewModel, nav, null)
|
1 -> RefresheableView(publicFeedViewModel, accountViewModel, nav, null)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+21
-23
@@ -22,11 +22,11 @@ import androidx.compose.material.icons.Icons
|
|||||||
import androidx.compose.material.icons.filled.MoreVert
|
import androidx.compose.material.icons.filled.MoreVert
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.DisposableEffect
|
import androidx.compose.runtime.DisposableEffect
|
||||||
|
import androidx.compose.runtime.Immutable
|
||||||
import androidx.compose.runtime.LaunchedEffect
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
import androidx.compose.runtime.MutableState
|
import androidx.compose.runtime.MutableState
|
||||||
import androidx.compose.runtime.derivedStateOf
|
import androidx.compose.runtime.derivedStateOf
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.runtime.livedata.observeAsState
|
|
||||||
import androidx.compose.runtime.mutableStateOf
|
import androidx.compose.runtime.mutableStateOf
|
||||||
import androidx.compose.runtime.remember
|
import androidx.compose.runtime.remember
|
||||||
import androidx.compose.runtime.rememberCoroutineScope
|
import androidx.compose.runtime.rememberCoroutineScope
|
||||||
@@ -41,8 +41,6 @@ import androidx.lifecycle.LifecycleEventObserver
|
|||||||
import androidx.lifecycle.viewmodel.compose.viewModel
|
import androidx.lifecycle.viewmodel.compose.viewModel
|
||||||
import com.vitorpamplona.amethyst.R
|
import com.vitorpamplona.amethyst.R
|
||||||
import com.vitorpamplona.amethyst.service.NostrChatroomListDataSource
|
import com.vitorpamplona.amethyst.service.NostrChatroomListDataSource
|
||||||
import com.vitorpamplona.amethyst.ui.dal.ChatroomListKnownFeedFilter
|
|
||||||
import com.vitorpamplona.amethyst.ui.dal.ChatroomListNewFeedFilter
|
|
||||||
import com.vitorpamplona.amethyst.ui.screen.ChatroomListFeedView
|
import com.vitorpamplona.amethyst.ui.screen.ChatroomListFeedView
|
||||||
import com.vitorpamplona.amethyst.ui.screen.FeedViewModel
|
import com.vitorpamplona.amethyst.ui.screen.FeedViewModel
|
||||||
import com.vitorpamplona.amethyst.ui.screen.NostrChatroomListKnownFeedViewModel
|
import com.vitorpamplona.amethyst.ui.screen.NostrChatroomListKnownFeedViewModel
|
||||||
@@ -51,7 +49,12 @@ import kotlinx.coroutines.launch
|
|||||||
|
|
||||||
@OptIn(ExperimentalFoundationApi::class)
|
@OptIn(ExperimentalFoundationApi::class)
|
||||||
@Composable
|
@Composable
|
||||||
fun ChatroomListScreen(accountViewModel: AccountViewModel, nav: (String) -> Unit) {
|
fun ChatroomListScreen(
|
||||||
|
knownFeedViewModel: NostrChatroomListKnownFeedViewModel,
|
||||||
|
newFeedViewModel: NostrChatroomListNewFeedViewModel,
|
||||||
|
accountViewModel: AccountViewModel,
|
||||||
|
nav: (String) -> Unit
|
||||||
|
) {
|
||||||
val pagerState = rememberPagerState()
|
val pagerState = rememberPagerState()
|
||||||
val coroutineScope = rememberCoroutineScope()
|
val coroutineScope = rememberCoroutineScope()
|
||||||
|
|
||||||
@@ -59,30 +62,15 @@ fun ChatroomListScreen(accountViewModel: AccountViewModel, nav: (String) -> Unit
|
|||||||
val markKnownAsRead = remember { mutableStateOf(false) }
|
val markKnownAsRead = remember { mutableStateOf(false) }
|
||||||
val markNewAsRead = remember { mutableStateOf(false) }
|
val markNewAsRead = remember { mutableStateOf(false) }
|
||||||
|
|
||||||
val accountState by accountViewModel.accountLiveData.observeAsState()
|
WatchAccountForListScreen(knownFeedViewModel, newFeedViewModel, accountViewModel)
|
||||||
val account = accountState?.account ?: return
|
|
||||||
|
|
||||||
ChatroomListKnownFeedFilter.account = account
|
|
||||||
val knownFeedViewModel: NostrChatroomListKnownFeedViewModel = viewModel()
|
|
||||||
|
|
||||||
ChatroomListNewFeedFilter.account = account
|
|
||||||
val newFeedViewModel: NostrChatroomListNewFeedViewModel = viewModel()
|
|
||||||
|
|
||||||
LaunchedEffect(accountViewModel) {
|
|
||||||
NostrChatroomListDataSource.account = account
|
|
||||||
NostrChatroomListDataSource.start()
|
|
||||||
knownFeedViewModel.invalidateData()
|
|
||||||
newFeedViewModel.invalidateData()
|
|
||||||
}
|
|
||||||
|
|
||||||
val lifeCycleOwner = LocalLifecycleOwner.current
|
val lifeCycleOwner = LocalLifecycleOwner.current
|
||||||
DisposableEffect(accountViewModel) {
|
DisposableEffect(accountViewModel) {
|
||||||
val observer = LifecycleEventObserver { _, event ->
|
val observer = LifecycleEventObserver { _, event ->
|
||||||
if (event == Lifecycle.Event.ON_RESUME) {
|
if (event == Lifecycle.Event.ON_RESUME) {
|
||||||
NostrChatroomListDataSource.account = account
|
|
||||||
NostrChatroomListDataSource.start()
|
NostrChatroomListDataSource.start()
|
||||||
knownFeedViewModel.invalidateData()
|
knownFeedViewModel.invalidateData(true)
|
||||||
newFeedViewModel.invalidateData()
|
newFeedViewModel.invalidateData(true)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -92,7 +80,7 @@ fun ChatroomListScreen(accountViewModel: AccountViewModel, nav: (String) -> Unit
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
val tabs by remember {
|
val tabs by remember(knownFeedViewModel, markKnownAsRead) {
|
||||||
derivedStateOf {
|
derivedStateOf {
|
||||||
listOf(
|
listOf(
|
||||||
ChatroomListTabItem(R.string.known, knownFeedViewModel, markKnownAsRead),
|
ChatroomListTabItem(R.string.known, knownFeedViewModel, markKnownAsRead),
|
||||||
@@ -159,6 +147,16 @@ fun ChatroomListScreen(accountViewModel: AccountViewModel, nav: (String) -> Unit
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun WatchAccountForListScreen(knownFeedViewModel: NostrChatroomListKnownFeedViewModel, newFeedViewModel: NostrChatroomListNewFeedViewModel, accountViewModel: AccountViewModel) {
|
||||||
|
LaunchedEffect(accountViewModel) {
|
||||||
|
NostrChatroomListDataSource.start()
|
||||||
|
knownFeedViewModel.invalidateData(true)
|
||||||
|
newFeedViewModel.invalidateData(true)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Immutable
|
||||||
class ChatroomListTabItem(val resource: Int, val viewModel: FeedViewModel, val markAsRead: MutableState<Boolean>)
|
class ChatroomListTabItem(val resource: Int, val viewModel: FeedViewModel, val markAsRead: MutableState<Boolean>)
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
|
|||||||
@@ -25,8 +25,8 @@ import androidx.lifecycle.viewmodel.compose.viewModel
|
|||||||
import com.vitorpamplona.amethyst.model.Account
|
import com.vitorpamplona.amethyst.model.Account
|
||||||
import com.vitorpamplona.amethyst.service.NostrHashtagDataSource
|
import com.vitorpamplona.amethyst.service.NostrHashtagDataSource
|
||||||
import com.vitorpamplona.amethyst.ui.dal.HashtagFeedFilter
|
import com.vitorpamplona.amethyst.ui.dal.HashtagFeedFilter
|
||||||
import com.vitorpamplona.amethyst.ui.screen.FeedView
|
|
||||||
import com.vitorpamplona.amethyst.ui.screen.NostrHashtagFeedViewModel
|
import com.vitorpamplona.amethyst.ui.screen.NostrHashtagFeedViewModel
|
||||||
|
import com.vitorpamplona.amethyst.ui.screen.RefresheableView
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
|
|
||||||
@@ -75,7 +75,7 @@ fun HashtagScreen(tag: String?, accountViewModel: AccountViewModel, nav: (String
|
|||||||
modifier = Modifier.padding(vertical = 0.dp)
|
modifier = Modifier.padding(vertical = 0.dp)
|
||||||
) {
|
) {
|
||||||
HashtagHeader(tag, account)
|
HashtagHeader(tag, account)
|
||||||
FeedView(feedViewModel, accountViewModel, nav, null)
|
RefresheableView(feedViewModel, accountViewModel, nav, null)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ import androidx.compose.material.TabRow
|
|||||||
import androidx.compose.material.Text
|
import androidx.compose.material.Text
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.DisposableEffect
|
import androidx.compose.runtime.DisposableEffect
|
||||||
|
import androidx.compose.runtime.Immutable
|
||||||
import androidx.compose.runtime.LaunchedEffect
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.runtime.livedata.observeAsState
|
import androidx.compose.runtime.livedata.observeAsState
|
||||||
@@ -27,15 +28,17 @@ import androidx.lifecycle.Lifecycle
|
|||||||
import androidx.lifecycle.LifecycleEventObserver
|
import androidx.lifecycle.LifecycleEventObserver
|
||||||
import com.vitorpamplona.amethyst.R
|
import com.vitorpamplona.amethyst.R
|
||||||
import com.vitorpamplona.amethyst.service.NostrHomeDataSource
|
import com.vitorpamplona.amethyst.service.NostrHomeDataSource
|
||||||
import com.vitorpamplona.amethyst.ui.dal.HomeConversationsFeedFilter
|
|
||||||
import com.vitorpamplona.amethyst.ui.dal.HomeNewThreadFeedFilter
|
|
||||||
import com.vitorpamplona.amethyst.ui.navigation.Route
|
import com.vitorpamplona.amethyst.ui.navigation.Route
|
||||||
import com.vitorpamplona.amethyst.ui.note.UpdateZapAmountDialog
|
import com.vitorpamplona.amethyst.ui.note.UpdateZapAmountDialog
|
||||||
import com.vitorpamplona.amethyst.ui.screen.FeedView
|
|
||||||
import com.vitorpamplona.amethyst.ui.screen.FeedViewModel
|
import com.vitorpamplona.amethyst.ui.screen.FeedViewModel
|
||||||
import com.vitorpamplona.amethyst.ui.screen.NostrHomeFeedViewModel
|
import com.vitorpamplona.amethyst.ui.screen.NostrHomeFeedViewModel
|
||||||
import com.vitorpamplona.amethyst.ui.screen.NostrHomeRepliesFeedViewModel
|
import com.vitorpamplona.amethyst.ui.screen.NostrHomeRepliesFeedViewModel
|
||||||
|
import com.vitorpamplona.amethyst.ui.screen.PagerStateKeys
|
||||||
|
import com.vitorpamplona.amethyst.ui.screen.RefresheableView
|
||||||
import com.vitorpamplona.amethyst.ui.screen.ScrollStateKeys
|
import com.vitorpamplona.amethyst.ui.screen.ScrollStateKeys
|
||||||
|
import com.vitorpamplona.amethyst.ui.screen.rememberForeverPagerState
|
||||||
|
import kotlinx.collections.immutable.ImmutableList
|
||||||
|
import kotlinx.collections.immutable.toImmutableList
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
|
|
||||||
@@ -46,34 +49,22 @@ fun HomeScreen(
|
|||||||
repliesFeedViewModel: NostrHomeRepliesFeedViewModel,
|
repliesFeedViewModel: NostrHomeRepliesFeedViewModel,
|
||||||
accountViewModel: AccountViewModel,
|
accountViewModel: AccountViewModel,
|
||||||
nav: (String) -> Unit,
|
nav: (String) -> Unit,
|
||||||
pagerState: PagerState,
|
|
||||||
scrollToTop: Boolean = false,
|
|
||||||
nip47: String? = null
|
nip47: String? = null
|
||||||
) {
|
) {
|
||||||
val coroutineScope = rememberCoroutineScope()
|
|
||||||
var wantsToAddNip47 by remember { mutableStateOf(nip47) }
|
var wantsToAddNip47 by remember { mutableStateOf(nip47) }
|
||||||
|
|
||||||
val accountState by accountViewModel.accountLiveData.observeAsState()
|
val pagerState = rememberForeverPagerState(key = PagerStateKeys.HOME_SCREEN)
|
||||||
val account = accountState?.account ?: return
|
|
||||||
|
|
||||||
LaunchedEffect(accountViewModel, account.defaultHomeFollowList) {
|
WatchAccountForHomeScreen(homeFeedViewModel, repliesFeedViewModel, accountViewModel)
|
||||||
HomeNewThreadFeedFilter.account = account
|
|
||||||
HomeConversationsFeedFilter.account = account
|
|
||||||
NostrHomeDataSource.invalidateFilters()
|
|
||||||
homeFeedViewModel.invalidateData(true)
|
|
||||||
repliesFeedViewModel.invalidateData(true)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (wantsToAddNip47 != null) {
|
if (wantsToAddNip47 != null) {
|
||||||
UpdateZapAmountDialog({ wantsToAddNip47 = null }, account = account, wantsToAddNip47)
|
UpdateZapAmountDialog({ wantsToAddNip47 = null }, wantsToAddNip47, accountViewModel)
|
||||||
}
|
}
|
||||||
|
|
||||||
val lifeCycleOwner = LocalLifecycleOwner.current
|
val lifeCycleOwner = LocalLifecycleOwner.current
|
||||||
DisposableEffect(accountViewModel) {
|
DisposableEffect(accountViewModel) {
|
||||||
val observer = LifecycleEventObserver { _, event ->
|
val observer = LifecycleEventObserver { _, event ->
|
||||||
if (event == Lifecycle.Event.ON_RESUME) {
|
if (event == Lifecycle.Event.ON_RESUME) {
|
||||||
HomeNewThreadFeedFilter.account = account
|
|
||||||
HomeConversationsFeedFilter.account = account
|
|
||||||
NostrHomeDataSource.invalidateFilters()
|
NostrHomeDataSource.invalidateFilters()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -84,23 +75,12 @@ fun HomeScreen(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (scrollToTop) {
|
|
||||||
val scope = rememberCoroutineScope()
|
|
||||||
LaunchedEffect(key1 = Unit) {
|
|
||||||
scope.launch(Dispatchers.IO) {
|
|
||||||
NostrHomeDataSource.invalidateFilters()
|
|
||||||
homeFeedViewModel.invalidateData(true)
|
|
||||||
repliesFeedViewModel.invalidateData(true)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
val tabs by remember(homeFeedViewModel, repliesFeedViewModel) {
|
val tabs by remember(homeFeedViewModel, repliesFeedViewModel) {
|
||||||
mutableStateOf(
|
mutableStateOf(
|
||||||
listOf(
|
listOf(
|
||||||
TabItem(R.string.new_threads, homeFeedViewModel, Route.Home.base + "Follows", ScrollStateKeys.HOME_FOLLOWS),
|
TabItem(R.string.new_threads, homeFeedViewModel, Route.Home.base + "Follows", ScrollStateKeys.HOME_FOLLOWS),
|
||||||
TabItem(R.string.conversations, repliesFeedViewModel, Route.Home.base + "FollowsReplies", ScrollStateKeys.HOME_REPLIES)
|
TabItem(R.string.conversations, repliesFeedViewModel, Route.Home.base + "FollowsReplies", ScrollStateKeys.HOME_REPLIES)
|
||||||
)
|
).toImmutableList()
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -108,35 +88,67 @@ fun HomeScreen(
|
|||||||
Column(
|
Column(
|
||||||
modifier = Modifier.padding(vertical = 0.dp)
|
modifier = Modifier.padding(vertical = 0.dp)
|
||||||
) {
|
) {
|
||||||
TabRow(
|
HomePages(pagerState, tabs, accountViewModel, nav)
|
||||||
backgroundColor = MaterialTheme.colors.background,
|
|
||||||
selectedTabIndex = pagerState.currentPage
|
|
||||||
) {
|
|
||||||
tabs.forEachIndexed { index, tab ->
|
|
||||||
Tab(
|
|
||||||
selected = pagerState.currentPage == index,
|
|
||||||
text = {
|
|
||||||
Text(text = stringResource(tab.resource))
|
|
||||||
},
|
|
||||||
onClick = {
|
|
||||||
coroutineScope.launch { pagerState.animateScrollToPage(index) }
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
HorizontalPager(pageCount = 2, state = pagerState) { page ->
|
|
||||||
FeedView(
|
|
||||||
viewModel = tabs[page].viewModel,
|
|
||||||
accountViewModel = accountViewModel,
|
|
||||||
nav = nav,
|
|
||||||
routeForLastRead = tabs[page].routeForLastRead,
|
|
||||||
scrollStateKey = tabs[page].scrollStateKey,
|
|
||||||
scrollToTop = scrollToTop
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
@OptIn(ExperimentalFoundationApi::class)
|
||||||
|
private fun HomePages(
|
||||||
|
pagerState: PagerState,
|
||||||
|
tabs: ImmutableList<TabItem>,
|
||||||
|
accountViewModel: AccountViewModel,
|
||||||
|
nav: (String) -> Unit
|
||||||
|
) {
|
||||||
|
TabRow(
|
||||||
|
backgroundColor = MaterialTheme.colors.background,
|
||||||
|
selectedTabIndex = pagerState.currentPage
|
||||||
|
) {
|
||||||
|
val coroutineScope = rememberCoroutineScope()
|
||||||
|
|
||||||
|
tabs.forEachIndexed { index, tab ->
|
||||||
|
Tab(
|
||||||
|
selected = pagerState.currentPage == index,
|
||||||
|
text = {
|
||||||
|
Text(text = stringResource(tab.resource))
|
||||||
|
},
|
||||||
|
onClick = {
|
||||||
|
coroutineScope.launch { pagerState.animateScrollToPage(index) }
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
HorizontalPager(pageCount = 2, state = pagerState) { page ->
|
||||||
|
RefresheableView(
|
||||||
|
viewModel = tabs[page].viewModel,
|
||||||
|
accountViewModel = accountViewModel,
|
||||||
|
nav = nav,
|
||||||
|
routeForLastRead = tabs[page].routeForLastRead,
|
||||||
|
scrollStateKey = tabs[page].scrollStateKey
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun WatchAccountForHomeScreen(
|
||||||
|
homeFeedViewModel: NostrHomeFeedViewModel,
|
||||||
|
repliesFeedViewModel: NostrHomeRepliesFeedViewModel,
|
||||||
|
accountViewModel: AccountViewModel
|
||||||
|
) {
|
||||||
|
val accountState by accountViewModel.accountLiveData.observeAsState()
|
||||||
|
val account = remember(accountState) { accountState?.account } ?: return
|
||||||
|
val scope = rememberCoroutineScope()
|
||||||
|
|
||||||
|
LaunchedEffect(accountViewModel, account.defaultHomeFollowList) {
|
||||||
|
scope.launch(Dispatchers.IO) {
|
||||||
|
NostrHomeDataSource.invalidateFilters()
|
||||||
|
homeFeedViewModel.invalidateDataAndSendToTop(true)
|
||||||
|
repliesFeedViewModel.invalidateDataAndSendToTop(true)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Immutable
|
||||||
class TabItem(val resource: Int, val viewModel: FeedViewModel, val routeForLastRead: String, val scrollStateKey: String)
|
class TabItem(val resource: Int, val viewModel: FeedViewModel, val routeForLastRead: String, val scrollStateKey: String)
|
||||||
|
|||||||
@@ -20,7 +20,6 @@ import androidx.compose.material.rememberScaffoldState
|
|||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.collectAsState
|
import androidx.compose.runtime.collectAsState
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.runtime.livedata.observeAsState
|
|
||||||
import androidx.compose.runtime.remember
|
import androidx.compose.runtime.remember
|
||||||
import androidx.compose.runtime.rememberCoroutineScope
|
import androidx.compose.runtime.rememberCoroutineScope
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
@@ -37,8 +36,16 @@ import com.vitorpamplona.amethyst.ui.navigation.AppTopBar
|
|||||||
import com.vitorpamplona.amethyst.ui.navigation.DrawerContent
|
import com.vitorpamplona.amethyst.ui.navigation.DrawerContent
|
||||||
import com.vitorpamplona.amethyst.ui.navigation.Route
|
import com.vitorpamplona.amethyst.ui.navigation.Route
|
||||||
import com.vitorpamplona.amethyst.ui.navigation.currentRoute
|
import com.vitorpamplona.amethyst.ui.navigation.currentRoute
|
||||||
|
import com.vitorpamplona.amethyst.ui.note.UserReactionsViewModel
|
||||||
import com.vitorpamplona.amethyst.ui.screen.AccountState
|
import com.vitorpamplona.amethyst.ui.screen.AccountState
|
||||||
import com.vitorpamplona.amethyst.ui.screen.AccountStateViewModel
|
import com.vitorpamplona.amethyst.ui.screen.AccountStateViewModel
|
||||||
|
import com.vitorpamplona.amethyst.ui.screen.NostrChatroomListKnownFeedViewModel
|
||||||
|
import com.vitorpamplona.amethyst.ui.screen.NostrChatroomListNewFeedViewModel
|
||||||
|
import com.vitorpamplona.amethyst.ui.screen.NostrGlobalFeedViewModel
|
||||||
|
import com.vitorpamplona.amethyst.ui.screen.NostrHomeFeedViewModel
|
||||||
|
import com.vitorpamplona.amethyst.ui.screen.NostrHomeRepliesFeedViewModel
|
||||||
|
import com.vitorpamplona.amethyst.ui.screen.NostrVideoFeedViewModel
|
||||||
|
import com.vitorpamplona.amethyst.ui.screen.NotificationViewModel
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
|
|
||||||
@OptIn(ExperimentalMaterialApi::class)
|
@OptIn(ExperimentalMaterialApi::class)
|
||||||
@@ -61,11 +68,51 @@ fun MainScreen(accountViewModel: AccountViewModel, accountStateViewModel: Accoun
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
val accountState by accountViewModel.accountLiveData.observeAsState()
|
val followLists: FollowListViewModel = viewModel(
|
||||||
val account = remember(accountState) { accountState?.account }
|
key = accountViewModel.userProfile().pubkeyHex + "FollowListViewModel",
|
||||||
|
factory = FollowListViewModel.Factory(accountViewModel.account)
|
||||||
|
)
|
||||||
|
|
||||||
val followLists: FollowListViewModel = viewModel()
|
// Avoids creating ViewModels for performance reasons (up to 1 second delays)
|
||||||
followLists.load(account)
|
val homeFeedViewModel: NostrHomeFeedViewModel = viewModel(
|
||||||
|
key = accountViewModel.userProfile().pubkeyHex + "NostrHomeFeedViewModel",
|
||||||
|
factory = NostrHomeFeedViewModel.Factory(accountViewModel.account)
|
||||||
|
)
|
||||||
|
|
||||||
|
val repliesFeedViewModel: NostrHomeRepliesFeedViewModel = viewModel(
|
||||||
|
key = accountViewModel.userProfile().pubkeyHex + "NostrHomeRepliesFeedViewModel",
|
||||||
|
factory = NostrHomeRepliesFeedViewModel.Factory(accountViewModel.account)
|
||||||
|
)
|
||||||
|
|
||||||
|
val searchFeedViewModel: NostrGlobalFeedViewModel = viewModel(
|
||||||
|
key = accountViewModel.userProfile().pubkeyHex + "NostrGlobalFeedViewModel",
|
||||||
|
factory = NostrGlobalFeedViewModel.Factory(accountViewModel.account)
|
||||||
|
)
|
||||||
|
|
||||||
|
val videoFeedViewModel: NostrVideoFeedViewModel = viewModel(
|
||||||
|
key = accountViewModel.userProfile().pubkeyHex + "NostrVideoFeedViewModel",
|
||||||
|
factory = NostrVideoFeedViewModel.Factory(accountViewModel.account)
|
||||||
|
)
|
||||||
|
|
||||||
|
val notifFeedViewModel: NotificationViewModel = viewModel(
|
||||||
|
key = accountViewModel.userProfile().pubkeyHex + "NotificationViewModel",
|
||||||
|
factory = NotificationViewModel.Factory(accountViewModel.account)
|
||||||
|
)
|
||||||
|
|
||||||
|
val userReactionsStatsModel: UserReactionsViewModel = viewModel(
|
||||||
|
key = accountViewModel.userProfile().pubkeyHex + "UserReactionsViewModel",
|
||||||
|
factory = UserReactionsViewModel.Factory(accountViewModel.account)
|
||||||
|
)
|
||||||
|
|
||||||
|
val knownFeedViewModel: NostrChatroomListKnownFeedViewModel = viewModel(
|
||||||
|
key = accountViewModel.userProfile().pubkeyHex + "NostrChatroomListKnownFeedViewModel",
|
||||||
|
factory = NostrChatroomListKnownFeedViewModel.Factory(accountViewModel.account)
|
||||||
|
)
|
||||||
|
|
||||||
|
val newFeedViewModel: NostrChatroomListNewFeedViewModel = viewModel(
|
||||||
|
key = accountViewModel.userProfile().pubkeyHex + "NostrChatroomListNewFeedViewModel",
|
||||||
|
factory = NostrChatroomListNewFeedViewModel.Factory(accountViewModel.account)
|
||||||
|
)
|
||||||
|
|
||||||
ModalBottomSheetLayout(
|
ModalBottomSheetLayout(
|
||||||
sheetState = sheetState,
|
sheetState = sheetState,
|
||||||
@@ -95,7 +142,19 @@ fun MainScreen(accountViewModel: AccountViewModel, accountStateViewModel: Accoun
|
|||||||
scaffoldState = scaffoldState
|
scaffoldState = scaffoldState
|
||||||
) {
|
) {
|
||||||
Column(modifier = Modifier.padding(bottom = it.calculateBottomPadding())) {
|
Column(modifier = Modifier.padding(bottom = it.calculateBottomPadding())) {
|
||||||
AppNavigation(navController, accountViewModel, startingPage)
|
AppNavigation(
|
||||||
|
homeFeedViewModel,
|
||||||
|
repliesFeedViewModel,
|
||||||
|
knownFeedViewModel,
|
||||||
|
newFeedViewModel,
|
||||||
|
searchFeedViewModel,
|
||||||
|
videoFeedViewModel,
|
||||||
|
notifFeedViewModel,
|
||||||
|
userReactionsStatsModel,
|
||||||
|
navController,
|
||||||
|
accountViewModel,
|
||||||
|
startingPage
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+28
-33
@@ -9,11 +9,11 @@ import androidx.compose.material.Divider
|
|||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.DisposableEffect
|
import androidx.compose.runtime.DisposableEffect
|
||||||
import androidx.compose.runtime.LaunchedEffect
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
|
import androidx.compose.runtime.collectAsState
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.runtime.livedata.observeAsState
|
import androidx.compose.runtime.livedata.observeAsState
|
||||||
import androidx.compose.runtime.mutableStateOf
|
import androidx.compose.runtime.mutableStateOf
|
||||||
import androidx.compose.runtime.remember
|
import androidx.compose.runtime.remember
|
||||||
import androidx.compose.runtime.rememberCoroutineScope
|
|
||||||
import androidx.compose.runtime.setValue
|
import androidx.compose.runtime.setValue
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.graphics.Brush
|
import androidx.compose.ui.graphics.Brush
|
||||||
@@ -23,7 +23,6 @@ import androidx.compose.ui.platform.LocalLifecycleOwner
|
|||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.lifecycle.Lifecycle
|
import androidx.lifecycle.Lifecycle
|
||||||
import androidx.lifecycle.LifecycleEventObserver
|
import androidx.lifecycle.LifecycleEventObserver
|
||||||
import androidx.lifecycle.viewmodel.compose.viewModel
|
|
||||||
import com.patrykandpatrick.vico.compose.axis.horizontal.bottomAxis
|
import com.patrykandpatrick.vico.compose.axis.horizontal.bottomAxis
|
||||||
import com.patrykandpatrick.vico.compose.axis.vertical.endAxis
|
import com.patrykandpatrick.vico.compose.axis.vertical.endAxis
|
||||||
import com.patrykandpatrick.vico.compose.axis.vertical.startAxis
|
import com.patrykandpatrick.vico.compose.axis.vertical.startAxis
|
||||||
@@ -38,9 +37,7 @@ import com.patrykandpatrick.vico.core.chart.composed.plus
|
|||||||
import com.patrykandpatrick.vico.core.chart.line.LineChart
|
import com.patrykandpatrick.vico.core.chart.line.LineChart
|
||||||
import com.patrykandpatrick.vico.core.chart.values.ChartValues
|
import com.patrykandpatrick.vico.core.chart.values.ChartValues
|
||||||
import com.patrykandpatrick.vico.core.component.shape.shader.DynamicShaders
|
import com.patrykandpatrick.vico.core.component.shape.shader.DynamicShaders
|
||||||
import com.patrykandpatrick.vico.core.entry.composed.plus
|
|
||||||
import com.vitorpamplona.amethyst.service.NostrAccountDataSource
|
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.navigation.Route
|
||||||
import com.vitorpamplona.amethyst.ui.note.OneGiga
|
import com.vitorpamplona.amethyst.ui.note.OneGiga
|
||||||
import com.vitorpamplona.amethyst.ui.note.OneKilo
|
import com.vitorpamplona.amethyst.ui.note.OneKilo
|
||||||
@@ -48,13 +45,11 @@ import com.vitorpamplona.amethyst.ui.note.OneMega
|
|||||||
import com.vitorpamplona.amethyst.ui.note.UserReactionsRow
|
import com.vitorpamplona.amethyst.ui.note.UserReactionsRow
|
||||||
import com.vitorpamplona.amethyst.ui.note.UserReactionsViewModel
|
import com.vitorpamplona.amethyst.ui.note.UserReactionsViewModel
|
||||||
import com.vitorpamplona.amethyst.ui.note.showCount
|
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.NotificationViewModel
|
||||||
|
import com.vitorpamplona.amethyst.ui.screen.RefresheableCardView
|
||||||
import com.vitorpamplona.amethyst.ui.screen.ScrollStateKeys
|
import com.vitorpamplona.amethyst.ui.screen.ScrollStateKeys
|
||||||
import com.vitorpamplona.amethyst.ui.theme.BitcoinOrange
|
import com.vitorpamplona.amethyst.ui.theme.BitcoinOrange
|
||||||
import com.vitorpamplona.amethyst.ui.theme.RoyalBlue
|
import com.vitorpamplona.amethyst.ui.theme.RoyalBlue
|
||||||
import kotlinx.coroutines.Dispatchers
|
|
||||||
import kotlinx.coroutines.launch
|
|
||||||
import java.math.BigDecimal
|
import java.math.BigDecimal
|
||||||
import java.math.RoundingMode
|
import java.math.RoundingMode
|
||||||
import kotlin.math.roundToInt
|
import kotlin.math.roundToInt
|
||||||
@@ -64,24 +59,15 @@ fun NotificationScreen(
|
|||||||
notifFeedViewModel: NotificationViewModel,
|
notifFeedViewModel: NotificationViewModel,
|
||||||
userReactionsStatsModel: UserReactionsViewModel,
|
userReactionsStatsModel: UserReactionsViewModel,
|
||||||
accountViewModel: AccountViewModel,
|
accountViewModel: AccountViewModel,
|
||||||
nav: (String) -> Unit,
|
nav: (String) -> Unit
|
||||||
scrollToTop: Boolean = false
|
|
||||||
) {
|
) {
|
||||||
val accountState by accountViewModel.accountLiveData.observeAsState()
|
WatchAccountForNotifications(notifFeedViewModel, accountViewModel)
|
||||||
val account = remember(accountState) { accountState?.account } ?: return
|
|
||||||
|
|
||||||
LaunchedEffect(accountViewModel, account.defaultNotificationFollowList) {
|
|
||||||
NostrAccountDataSource.invalidateFilters()
|
|
||||||
NotificationFeedFilter.account = account
|
|
||||||
notifFeedViewModel.invalidateData(true)
|
|
||||||
}
|
|
||||||
|
|
||||||
val lifeCycleOwner = LocalLifecycleOwner.current
|
val lifeCycleOwner = LocalLifecycleOwner.current
|
||||||
DisposableEffect(accountViewModel) {
|
DisposableEffect(accountViewModel) {
|
||||||
val observer = LifecycleEventObserver { _, event ->
|
val observer = LifecycleEventObserver { _, event ->
|
||||||
if (event == Lifecycle.Event.ON_RESUME) {
|
if (event == Lifecycle.Event.ON_RESUME) {
|
||||||
NostrAccountDataSource.invalidateFilters()
|
NostrAccountDataSource.invalidateFilters()
|
||||||
NotificationFeedFilter.account = account
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -91,16 +77,6 @@ fun NotificationScreen(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (scrollToTop) {
|
|
||||||
val scope = rememberCoroutineScope()
|
|
||||||
LaunchedEffect(key1 = Unit) {
|
|
||||||
scope.launch(Dispatchers.IO) {
|
|
||||||
notifFeedViewModel.clear()
|
|
||||||
notifFeedViewModel.invalidateData(true)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Column(Modifier.fillMaxHeight()) {
|
Column(Modifier.fillMaxHeight()) {
|
||||||
Column(
|
Column(
|
||||||
modifier = Modifier.padding(vertical = 0.dp)
|
modifier = Modifier.padding(vertical = 0.dp)
|
||||||
@@ -108,18 +84,35 @@ fun NotificationScreen(
|
|||||||
SummaryBar(
|
SummaryBar(
|
||||||
model = userReactionsStatsModel
|
model = userReactionsStatsModel
|
||||||
)
|
)
|
||||||
CardFeedView(
|
RefresheableCardView(
|
||||||
viewModel = notifFeedViewModel,
|
viewModel = notifFeedViewModel,
|
||||||
accountViewModel = accountViewModel,
|
accountViewModel = accountViewModel,
|
||||||
nav = nav,
|
nav = nav,
|
||||||
routeForLastRead = Route.Notification.base,
|
routeForLastRead = Route.Notification.base,
|
||||||
scrollStateKey = ScrollStateKeys.NOTIFICATION_SCREEN,
|
scrollStateKey = ScrollStateKeys.NOTIFICATION_SCREEN
|
||||||
scrollToTop = scrollToTop
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun WatchAccountForNotifications(
|
||||||
|
notifFeedViewModel: NotificationViewModel,
|
||||||
|
accountViewModel: AccountViewModel
|
||||||
|
) {
|
||||||
|
val accountState by accountViewModel.accountLiveData.observeAsState()
|
||||||
|
val account = remember(accountState) { accountState?.account } ?: return
|
||||||
|
|
||||||
|
LaunchedEffect(accountViewModel, account.defaultNotificationFollowList) {
|
||||||
|
NostrAccountDataSource.invalidateFilters()
|
||||||
|
if (notifFeedViewModel.scrollToTop.value > 0) {
|
||||||
|
notifFeedViewModel.clear()
|
||||||
|
}
|
||||||
|
|
||||||
|
notifFeedViewModel.invalidateDataAndSendToTop(true)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun SummaryBar(model: UserReactionsViewModel) {
|
fun SummaryBar(model: UserReactionsViewModel) {
|
||||||
var showChart by remember {
|
var showChart by remember {
|
||||||
@@ -167,7 +160,9 @@ fun SummaryBar(model: UserReactionsViewModel) {
|
|||||||
)
|
)
|
||||||
|
|
||||||
if (showChart) {
|
if (showChart) {
|
||||||
model.chartModel?.let {
|
val axisModel by model.axisLabels.collectAsState()
|
||||||
|
val chartModel by model.chartModel.collectAsState()
|
||||||
|
chartModel?.let {
|
||||||
Row(
|
Row(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.padding(vertical = 10.dp, horizontal = 20.dp)
|
.padding(vertical = 10.dp, horizontal = 20.dp)
|
||||||
@@ -186,7 +181,7 @@ fun SummaryBar(model: UserReactionsViewModel) {
|
|||||||
valueFormatter = AmountAxisValueFormatter()
|
valueFormatter = AmountAxisValueFormatter()
|
||||||
),
|
),
|
||||||
bottomAxis = bottomAxis(
|
bottomAxis = bottomAxis(
|
||||||
valueFormatter = LabelValueFormatter(model.axisLabels)
|
valueFormatter = LabelValueFormatter(axisModel)
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -82,7 +82,6 @@ import com.vitorpamplona.amethyst.ui.dal.UserProfileZapsFeedFilter
|
|||||||
import com.vitorpamplona.amethyst.ui.navigation.ShowQRDialog
|
import com.vitorpamplona.amethyst.ui.navigation.ShowQRDialog
|
||||||
import com.vitorpamplona.amethyst.ui.note.UserPicture
|
import com.vitorpamplona.amethyst.ui.note.UserPicture
|
||||||
import com.vitorpamplona.amethyst.ui.note.showAmount
|
import com.vitorpamplona.amethyst.ui.note.showAmount
|
||||||
import com.vitorpamplona.amethyst.ui.screen.FeedView
|
|
||||||
import com.vitorpamplona.amethyst.ui.screen.LnZapFeedView
|
import com.vitorpamplona.amethyst.ui.screen.LnZapFeedView
|
||||||
import com.vitorpamplona.amethyst.ui.screen.NostrUserProfileBookmarksFeedViewModel
|
import com.vitorpamplona.amethyst.ui.screen.NostrUserProfileBookmarksFeedViewModel
|
||||||
import com.vitorpamplona.amethyst.ui.screen.NostrUserProfileConversationsFeedViewModel
|
import com.vitorpamplona.amethyst.ui.screen.NostrUserProfileConversationsFeedViewModel
|
||||||
@@ -91,6 +90,7 @@ import com.vitorpamplona.amethyst.ui.screen.NostrUserProfileFollowsUserFeedViewM
|
|||||||
import com.vitorpamplona.amethyst.ui.screen.NostrUserProfileNewThreadsFeedViewModel
|
import com.vitorpamplona.amethyst.ui.screen.NostrUserProfileNewThreadsFeedViewModel
|
||||||
import com.vitorpamplona.amethyst.ui.screen.NostrUserProfileReportFeedViewModel
|
import com.vitorpamplona.amethyst.ui.screen.NostrUserProfileReportFeedViewModel
|
||||||
import com.vitorpamplona.amethyst.ui.screen.NostrUserProfileZapsFeedViewModel
|
import com.vitorpamplona.amethyst.ui.screen.NostrUserProfileZapsFeedViewModel
|
||||||
|
import com.vitorpamplona.amethyst.ui.screen.RefresheableView
|
||||||
import com.vitorpamplona.amethyst.ui.screen.RelayFeedView
|
import com.vitorpamplona.amethyst.ui.screen.RelayFeedView
|
||||||
import com.vitorpamplona.amethyst.ui.screen.RelayFeedViewModel
|
import com.vitorpamplona.amethyst.ui.screen.RelayFeedViewModel
|
||||||
import com.vitorpamplona.amethyst.ui.screen.UserFeedView
|
import com.vitorpamplona.amethyst.ui.screen.UserFeedView
|
||||||
@@ -115,7 +115,7 @@ fun ProfileScreen(userId: String?, accountViewModel: AccountViewModel, nav: (Str
|
|||||||
|
|
||||||
userBase?.let {
|
userBase?.let {
|
||||||
ProfileScreen(
|
ProfileScreen(
|
||||||
user = it,
|
baseUser = it,
|
||||||
accountViewModel = accountViewModel,
|
accountViewModel = accountViewModel,
|
||||||
nav = nav
|
nav = nav
|
||||||
)
|
)
|
||||||
@@ -124,19 +124,16 @@ fun ProfileScreen(userId: String?, accountViewModel: AccountViewModel, nav: (Str
|
|||||||
|
|
||||||
@OptIn(ExperimentalFoundationApi::class)
|
@OptIn(ExperimentalFoundationApi::class)
|
||||||
@Composable
|
@Composable
|
||||||
fun ProfileScreen(user: User, accountViewModel: AccountViewModel, nav: (String) -> Unit) {
|
fun ProfileScreen(baseUser: User, accountViewModel: AccountViewModel, nav: (String) -> Unit) {
|
||||||
val accountState by accountViewModel.accountLiveData.observeAsState()
|
UserProfileNewThreadFeedFilter.loadUserProfile(accountViewModel.account, baseUser)
|
||||||
val account = remember(accountState) { accountState?.account } ?: return
|
UserProfileConversationsFeedFilter.loadUserProfile(accountViewModel.account, baseUser)
|
||||||
|
UserProfileFollowersFeedFilter.loadUserProfile(accountViewModel.account, baseUser)
|
||||||
|
UserProfileFollowsFeedFilter.loadUserProfile(accountViewModel.account, baseUser)
|
||||||
|
UserProfileZapsFeedFilter.loadUserProfile(baseUser)
|
||||||
|
UserProfileReportsFeedFilter.loadUserProfile(baseUser)
|
||||||
|
UserProfileBookmarksFeedFilter.loadUserProfile(accountViewModel.account, baseUser)
|
||||||
|
|
||||||
UserProfileNewThreadFeedFilter.loadUserProfile(account, user)
|
NostrUserProfileDataSource.loadUserProfile(baseUser)
|
||||||
UserProfileConversationsFeedFilter.loadUserProfile(account, user)
|
|
||||||
UserProfileFollowersFeedFilter.loadUserProfile(account, user)
|
|
||||||
UserProfileFollowsFeedFilter.loadUserProfile(account, user)
|
|
||||||
UserProfileZapsFeedFilter.loadUserProfile(user)
|
|
||||||
UserProfileReportsFeedFilter.loadUserProfile(user)
|
|
||||||
UserProfileBookmarksFeedFilter.loadUserProfile(account, user)
|
|
||||||
|
|
||||||
NostrUserProfileDataSource.loadUserProfile(user)
|
|
||||||
|
|
||||||
val lifeCycleOwner = LocalLifecycleOwner.current
|
val lifeCycleOwner = LocalLifecycleOwner.current
|
||||||
|
|
||||||
@@ -148,7 +145,7 @@ fun ProfileScreen(user: User, accountViewModel: AccountViewModel, nav: (String)
|
|||||||
val observer = LifecycleEventObserver { _, event ->
|
val observer = LifecycleEventObserver { _, event ->
|
||||||
if (event == Lifecycle.Event.ON_RESUME) {
|
if (event == Lifecycle.Event.ON_RESUME) {
|
||||||
println("Profidle Start")
|
println("Profidle Start")
|
||||||
NostrUserProfileDataSource.loadUserProfile(user)
|
NostrUserProfileDataSource.loadUserProfile(baseUser)
|
||||||
NostrUserProfileDataSource.start()
|
NostrUserProfileDataSource.start()
|
||||||
}
|
}
|
||||||
if (event == Lifecycle.Event.ON_PAUSE) {
|
if (event == Lifecycle.Event.ON_PAUSE) {
|
||||||
@@ -167,8 +164,6 @@ fun ProfileScreen(user: User, accountViewModel: AccountViewModel, nav: (String)
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
val baseUser = NostrUserProfileDataSource.user ?: return
|
|
||||||
|
|
||||||
var columnSize by remember { mutableStateOf(IntSize.Zero) }
|
var columnSize by remember { mutableStateOf(IntSize.Zero) }
|
||||||
var tabsSize by remember { mutableStateOf(IntSize.Zero) }
|
var tabsSize by remember { mutableStateOf(IntSize.Zero) }
|
||||||
|
|
||||||
@@ -209,7 +204,7 @@ fun ProfileScreen(user: User, accountViewModel: AccountViewModel, nav: (String)
|
|||||||
.fillMaxHeight()
|
.fillMaxHeight()
|
||||||
) {
|
) {
|
||||||
Column(modifier = Modifier.padding()) {
|
Column(modifier = Modifier.padding()) {
|
||||||
ProfileHeader(baseUser, nav, account, accountViewModel)
|
ProfileHeader(baseUser, nav, accountViewModel)
|
||||||
ScrollableTabRow(
|
ScrollableTabRow(
|
||||||
backgroundColor = MaterialTheme.colors.background,
|
backgroundColor = MaterialTheme.colors.background,
|
||||||
selectedTabIndex = pagerState.currentPage,
|
selectedTabIndex = pagerState.currentPage,
|
||||||
@@ -342,7 +337,6 @@ private fun FollowTabHeader(baseUser: User) {
|
|||||||
private fun ProfileHeader(
|
private fun ProfileHeader(
|
||||||
baseUser: User,
|
baseUser: User,
|
||||||
nav: (String) -> Unit,
|
nav: (String) -> Unit,
|
||||||
account: Account,
|
|
||||||
accountViewModel: AccountViewModel
|
accountViewModel: AccountViewModel
|
||||||
) {
|
) {
|
||||||
var popupExpanded by remember { mutableStateOf(false) }
|
var popupExpanded by remember { mutableStateOf(false) }
|
||||||
@@ -394,7 +388,7 @@ private fun ProfileHeader(
|
|||||||
) {
|
) {
|
||||||
UserPicture(
|
UserPicture(
|
||||||
baseUser = baseUser,
|
baseUser = baseUser,
|
||||||
baseUserAccount = account.userProfile(),
|
baseUserAccount = accountViewModel.userProfile(),
|
||||||
size = 100.dp,
|
size = 100.dp,
|
||||||
modifier = Modifier.border(
|
modifier = Modifier.border(
|
||||||
3.dp,
|
3.dp,
|
||||||
@@ -427,11 +421,11 @@ private fun ProfileHeader(
|
|||||||
// No need for this button anymore
|
// No need for this button anymore
|
||||||
// NPubCopyButton(baseUser)
|
// NPubCopyButton(baseUser)
|
||||||
|
|
||||||
ProfileActions(baseUser, account, coroutineScope)
|
ProfileActions(baseUser, accountViewModel, coroutineScope)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
DrawAdditionalInfo(baseUser, account, accountViewModel, nav)
|
DrawAdditionalInfo(baseUser, accountViewModel, nav)
|
||||||
|
|
||||||
Divider(modifier = Modifier.padding(top = 6.dp))
|
Divider(modifier = Modifier.padding(top = 6.dp))
|
||||||
}
|
}
|
||||||
@@ -446,11 +440,14 @@ private fun ProfileHeader(
|
|||||||
@Composable
|
@Composable
|
||||||
private fun ProfileActions(
|
private fun ProfileActions(
|
||||||
baseUser: User,
|
baseUser: User,
|
||||||
account: Account,
|
accountViewModel: AccountViewModel,
|
||||||
coroutineScope: CoroutineScope
|
coroutineScope: CoroutineScope
|
||||||
) {
|
) {
|
||||||
val accountUserState by account.userProfile().live().follows.observeAsState()
|
val accountLocalUserState by accountViewModel.accountLiveData.observeAsState()
|
||||||
val accountLocalUserState by account.live.observeAsState()
|
val account = remember(accountLocalUserState) { accountLocalUserState?.account } ?: return
|
||||||
|
|
||||||
|
val accountUserState by accountViewModel.account.userProfile().live().follows.observeAsState()
|
||||||
|
|
||||||
val accountUser = remember(accountUserState) { accountUserState?.user } ?: return
|
val accountUser = remember(accountUserState) { accountUserState?.user } ?: return
|
||||||
|
|
||||||
val isHidden by remember(accountUserState, accountLocalUserState) {
|
val isHidden by remember(accountUserState, accountLocalUserState) {
|
||||||
@@ -497,7 +494,7 @@ private fun ProfileActions(
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
private fun DrawAdditionalInfo(baseUser: User, account: Account, accountViewModel: AccountViewModel, nav: (String) -> Unit) {
|
private fun DrawAdditionalInfo(baseUser: User, accountViewModel: AccountViewModel, nav: (String) -> Unit) {
|
||||||
val userState by baseUser.live().metadata.observeAsState()
|
val userState by baseUser.live().metadata.observeAsState()
|
||||||
val user = remember(userState) { userState?.user } ?: return
|
val user = remember(userState) { userState?.user } ?: return
|
||||||
val tags = remember(userState) { userState?.user?.info?.latestMetadata?.tags }
|
val tags = remember(userState) { userState?.user?.info?.latestMetadata?.tags }
|
||||||
@@ -607,7 +604,7 @@ private fun DrawAdditionalInfo(baseUser: User, account: Account, accountViewMode
|
|||||||
|
|
||||||
val lud16 = remember(userState) { user.info?.lud16?.trim() ?: user.info?.lud06?.trim() }
|
val lud16 = remember(userState) { user.info?.lud16?.trim() ?: user.info?.lud06?.trim() }
|
||||||
val pubkeyHex = remember { baseUser.pubkeyHex }
|
val pubkeyHex = remember { baseUser.pubkeyHex }
|
||||||
DisplayLNAddress(lud16, pubkeyHex, account, scope, context)
|
DisplayLNAddress(lud16, pubkeyHex, accountViewModel.account, scope, context)
|
||||||
|
|
||||||
val identities = user.info?.latestMetadata?.identityClaims()
|
val identities = user.info?.latestMetadata?.identityClaims()
|
||||||
if (!identities.isNullOrEmpty()) {
|
if (!identities.isNullOrEmpty()) {
|
||||||
@@ -733,25 +730,54 @@ private fun DisplayBadges(
|
|||||||
nav: (String) -> Unit
|
nav: (String) -> Unit
|
||||||
) {
|
) {
|
||||||
val userBadgeState by baseUser.live().badges.observeAsState()
|
val userBadgeState by baseUser.live().badges.observeAsState()
|
||||||
val userBadge = remember(userBadgeState) { userBadgeState?.user } ?: return
|
val badgeList by remember(userBadgeState) {
|
||||||
|
derivedStateOf {
|
||||||
userBadge.acceptedBadges?.let { note ->
|
val list = (userBadgeState?.user?.acceptedBadges?.event as? BadgeProfilesEvent)?.badgeAwardEvents()
|
||||||
(note.event as? BadgeProfilesEvent)?.let { event ->
|
if (list.isNullOrEmpty()) {
|
||||||
FlowRow(verticalAlignment = Alignment.CenterVertically, modifier = Modifier.padding(vertical = 5.dp)) {
|
null
|
||||||
event.badgeAwardEvents().forEach { badgeAwardEvent ->
|
} else {
|
||||||
val baseNote = LocalCache.notes[badgeAwardEvent]
|
list
|
||||||
if (baseNote != null) {
|
|
||||||
val badgeAwardState by baseNote.live().metadata.observeAsState()
|
|
||||||
val baseBadgeDefinition = badgeAwardState?.note?.replyTo?.firstOrNull()
|
|
||||||
|
|
||||||
if (baseBadgeDefinition != null) {
|
|
||||||
BadgeThumb(baseBadgeDefinition, nav, 35.dp)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
badgeList?.let { list ->
|
||||||
|
FlowRow(
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
modifier = Modifier.padding(vertical = 5.dp)
|
||||||
|
) {
|
||||||
|
list.forEach { badgeAwardEvent ->
|
||||||
|
LoadAndRenderBadge(badgeAwardEvent, nav)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun LoadAndRenderBadge(badgeAwardEventHex: String, nav: (String) -> Unit) {
|
||||||
|
var baseNote by remember {
|
||||||
|
mutableStateOf<Note?>(null)
|
||||||
|
}
|
||||||
|
|
||||||
|
val scope = rememberCoroutineScope()
|
||||||
|
LaunchedEffect(key1 = badgeAwardEventHex) {
|
||||||
|
scope.launch(Dispatchers.IO) {
|
||||||
|
baseNote = LocalCache.getOrCreateNote(badgeAwardEventHex)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
baseNote?.let {
|
||||||
|
val badgeAwardState by it.live().metadata.observeAsState()
|
||||||
|
val baseBadgeDefinition by remember(badgeAwardState) {
|
||||||
|
derivedStateOf {
|
||||||
|
badgeAwardState?.note?.replyTo?.firstOrNull()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
baseBadgeDefinition?.let {
|
||||||
|
BadgeThumb(it, nav, 35.dp)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
@@ -762,7 +788,7 @@ fun BadgeThumb(
|
|||||||
pictureModifier: Modifier = Modifier
|
pictureModifier: Modifier = Modifier
|
||||||
) {
|
) {
|
||||||
BadgeThumb(note, size, pictureModifier) {
|
BadgeThumb(note, size, pictureModifier) {
|
||||||
nav("Note/${it.idHex}")
|
nav("Note/$it")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -771,19 +797,19 @@ fun BadgeThumb(
|
|||||||
baseNote: Note,
|
baseNote: Note,
|
||||||
size: Dp,
|
size: Dp,
|
||||||
pictureModifier: Modifier = Modifier,
|
pictureModifier: Modifier = Modifier,
|
||||||
onClick: ((Note) -> Unit)? = null
|
onClick: ((String) -> Unit)? = null
|
||||||
) {
|
) {
|
||||||
val noteState by baseNote.live().metadata.observeAsState()
|
|
||||||
val note = noteState?.note ?: return
|
|
||||||
|
|
||||||
val event = (note.event as? BadgeDefinitionEvent)
|
|
||||||
val image = event?.thumb() ?: event?.image()
|
|
||||||
|
|
||||||
Box(
|
Box(
|
||||||
Modifier
|
remember {
|
||||||
.width(size)
|
Modifier
|
||||||
.height(size)
|
.width(size)
|
||||||
|
.height(size)
|
||||||
|
}
|
||||||
) {
|
) {
|
||||||
|
val noteState by baseNote.live().metadata.observeAsState()
|
||||||
|
val event = remember(noteState) { noteState?.note?.event as? BadgeDefinitionEvent } ?: return
|
||||||
|
val image = remember(noteState) { event.thumb() ?: event.image() }
|
||||||
|
|
||||||
if (image == null) {
|
if (image == null) {
|
||||||
RobohashAsyncImage(
|
RobohashAsyncImage(
|
||||||
robot = "authornotfound",
|
robot = "authornotfound",
|
||||||
@@ -796,7 +822,7 @@ fun BadgeThumb(
|
|||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
RobohashFallbackAsyncImage(
|
RobohashFallbackAsyncImage(
|
||||||
robot = note.idHex,
|
robot = event.id,
|
||||||
robotSize = size,
|
robotSize = size,
|
||||||
model = image,
|
model = image,
|
||||||
contentDescription = stringResource(id = R.string.profile_image),
|
contentDescription = stringResource(id = R.string.profile_image),
|
||||||
@@ -807,7 +833,7 @@ fun BadgeThumb(
|
|||||||
.background(MaterialTheme.colors.background)
|
.background(MaterialTheme.colors.background)
|
||||||
.run {
|
.run {
|
||||||
if (onClick != null) {
|
if (onClick != null) {
|
||||||
this.clickable(onClick = { onClick(note) })
|
this.clickable(onClick = { onClick(event.id) })
|
||||||
} else {
|
} else {
|
||||||
this
|
this
|
||||||
}
|
}
|
||||||
@@ -871,7 +897,7 @@ fun TabNotesNewThreads(accountViewModel: AccountViewModel, nav: (String) -> Unit
|
|||||||
Column(
|
Column(
|
||||||
modifier = Modifier.padding(vertical = 0.dp)
|
modifier = Modifier.padding(vertical = 0.dp)
|
||||||
) {
|
) {
|
||||||
FeedView(feedViewModel, accountViewModel, nav, null, enablePullRefresh = false)
|
RefresheableView(feedViewModel, accountViewModel, nav, null, enablePullRefresh = false)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -888,7 +914,7 @@ fun TabNotesConversations(accountViewModel: AccountViewModel, nav: (String) -> U
|
|||||||
Column(
|
Column(
|
||||||
modifier = Modifier.padding(vertical = 0.dp)
|
modifier = Modifier.padding(vertical = 0.dp)
|
||||||
) {
|
) {
|
||||||
FeedView(feedViewModel, accountViewModel, nav, null, enablePullRefresh = false)
|
RefresheableView(feedViewModel, accountViewModel, nav, null, enablePullRefresh = false)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -905,7 +931,7 @@ fun TabBookmarks(baseUser: User, accountViewModel: AccountViewModel, nav: (Strin
|
|||||||
Column(
|
Column(
|
||||||
modifier = Modifier.padding(vertical = 0.dp)
|
modifier = Modifier.padding(vertical = 0.dp)
|
||||||
) {
|
) {
|
||||||
FeedView(feedViewModel, accountViewModel, nav, null, enablePullRefresh = false)
|
RefresheableView(feedViewModel, accountViewModel, nav, null, enablePullRefresh = false)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -981,7 +1007,7 @@ fun TabReports(baseUser: User, accountViewModel: AccountViewModel, nav: (String)
|
|||||||
Column(
|
Column(
|
||||||
modifier = Modifier.padding(vertical = 0.dp)
|
modifier = Modifier.padding(vertical = 0.dp)
|
||||||
) {
|
) {
|
||||||
FeedView(feedViewModel, accountViewModel, nav, null, enablePullRefresh = false)
|
RefresheableView(feedViewModel, accountViewModel, nav, null, enablePullRefresh = false)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -55,15 +55,14 @@ import com.vitorpamplona.amethyst.model.User
|
|||||||
import com.vitorpamplona.amethyst.service.NostrGlobalDataSource
|
import com.vitorpamplona.amethyst.service.NostrGlobalDataSource
|
||||||
import com.vitorpamplona.amethyst.service.NostrSearchEventOrUserDataSource
|
import com.vitorpamplona.amethyst.service.NostrSearchEventOrUserDataSource
|
||||||
import com.vitorpamplona.amethyst.ui.components.BundledUpdate
|
import com.vitorpamplona.amethyst.ui.components.BundledUpdate
|
||||||
import com.vitorpamplona.amethyst.ui.dal.GlobalFeedFilter
|
|
||||||
import com.vitorpamplona.amethyst.ui.note.AboutDisplay
|
import com.vitorpamplona.amethyst.ui.note.AboutDisplay
|
||||||
import com.vitorpamplona.amethyst.ui.note.ChannelName
|
import com.vitorpamplona.amethyst.ui.note.ChannelName
|
||||||
import com.vitorpamplona.amethyst.ui.note.NoteCompose
|
import com.vitorpamplona.amethyst.ui.note.NoteCompose
|
||||||
import com.vitorpamplona.amethyst.ui.note.UserCompose
|
import com.vitorpamplona.amethyst.ui.note.UserCompose
|
||||||
import com.vitorpamplona.amethyst.ui.note.UserPicture
|
import com.vitorpamplona.amethyst.ui.note.UserPicture
|
||||||
import com.vitorpamplona.amethyst.ui.note.UsernameDisplay
|
import com.vitorpamplona.amethyst.ui.note.UsernameDisplay
|
||||||
import com.vitorpamplona.amethyst.ui.screen.FeedView
|
|
||||||
import com.vitorpamplona.amethyst.ui.screen.NostrGlobalFeedViewModel
|
import com.vitorpamplona.amethyst.ui.screen.NostrGlobalFeedViewModel
|
||||||
|
import com.vitorpamplona.amethyst.ui.screen.RefresheableView
|
||||||
import com.vitorpamplona.amethyst.ui.screen.ScrollStateKeys
|
import com.vitorpamplona.amethyst.ui.screen.ScrollStateKeys
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.FlowPreview
|
import kotlinx.coroutines.FlowPreview
|
||||||
@@ -81,25 +80,16 @@ import kotlinx.coroutines.channels.Channel as CoroutineChannel
|
|||||||
fun SearchScreen(
|
fun SearchScreen(
|
||||||
searchFeedViewModel: NostrGlobalFeedViewModel,
|
searchFeedViewModel: NostrGlobalFeedViewModel,
|
||||||
accountViewModel: AccountViewModel,
|
accountViewModel: AccountViewModel,
|
||||||
nav: (String) -> Unit,
|
nav: (String) -> Unit
|
||||||
scrollToTop: Boolean = false
|
|
||||||
) {
|
) {
|
||||||
val lifeCycleOwner = LocalLifecycleOwner.current
|
val lifeCycleOwner = LocalLifecycleOwner.current
|
||||||
val account = accountViewModel.accountLiveData.value?.account ?: return
|
|
||||||
|
|
||||||
GlobalFeedFilter.account = account
|
WatchAccountForSearchScreen(searchFeedViewModel, accountViewModel)
|
||||||
|
|
||||||
LaunchedEffect(accountViewModel) {
|
|
||||||
GlobalFeedFilter.account = account
|
|
||||||
NostrGlobalDataSource.resetFilters()
|
|
||||||
searchFeedViewModel.invalidateData()
|
|
||||||
}
|
|
||||||
|
|
||||||
DisposableEffect(accountViewModel) {
|
DisposableEffect(accountViewModel) {
|
||||||
val observer = LifecycleEventObserver { _, event ->
|
val observer = LifecycleEventObserver { _, event ->
|
||||||
if (event == Lifecycle.Event.ON_RESUME) {
|
if (event == Lifecycle.Event.ON_RESUME) {
|
||||||
println("Global Start")
|
println("Global Start")
|
||||||
GlobalFeedFilter.account = account
|
|
||||||
NostrGlobalDataSource.start()
|
NostrGlobalDataSource.start()
|
||||||
NostrSearchEventOrUserDataSource.start()
|
NostrSearchEventOrUserDataSource.start()
|
||||||
searchFeedViewModel.invalidateData()
|
searchFeedViewModel.invalidateData()
|
||||||
@@ -123,11 +113,19 @@ fun SearchScreen(
|
|||||||
modifier = Modifier.padding(vertical = 0.dp)
|
modifier = Modifier.padding(vertical = 0.dp)
|
||||||
) {
|
) {
|
||||||
SearchBar(accountViewModel, nav)
|
SearchBar(accountViewModel, nav)
|
||||||
FeedView(searchFeedViewModel, accountViewModel, nav, null, ScrollStateKeys.GLOBAL_SCREEN, scrollToTop)
|
RefresheableView(searchFeedViewModel, accountViewModel, nav, null, ScrollStateKeys.GLOBAL_SCREEN)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun WatchAccountForSearchScreen(searchFeedViewModel: NostrGlobalFeedViewModel, accountViewModel: AccountViewModel) {
|
||||||
|
LaunchedEffect(accountViewModel) {
|
||||||
|
NostrGlobalDataSource.resetFilters()
|
||||||
|
searchFeedViewModel.invalidateData(true)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
class SearchBarViewModel : ViewModel() {
|
class SearchBarViewModel : ViewModel() {
|
||||||
var account: Account? = null
|
var account: Account? = null
|
||||||
|
|
||||||
@@ -168,14 +166,14 @@ class SearchBarViewModel : ViewModel() {
|
|||||||
searchResultsNotes.value = emptyList()
|
searchResultsNotes.value = emptyList()
|
||||||
}
|
}
|
||||||
|
|
||||||
private val bundler = BundledUpdate(250, Dispatchers.IO) {
|
private val bundler = BundledUpdate(250, Dispatchers.IO)
|
||||||
// adds the time to perform the refresh into this delay
|
|
||||||
// holding off new updates in case of heavy refresh routines.
|
|
||||||
runSearch()
|
|
||||||
}
|
|
||||||
|
|
||||||
fun invalidateData() {
|
fun invalidateData() {
|
||||||
bundler.invalidate()
|
bundler.invalidate() {
|
||||||
|
// adds the time to perform the refresh into this delay
|
||||||
|
// holding off new updates in case of heavy refresh routines.
|
||||||
|
runSearch()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun isSearching() = searchValue.isNotBlank()
|
fun isSearching() = searchValue.isNotBlank()
|
||||||
|
|||||||
@@ -7,7 +7,6 @@ import androidx.compose.runtime.Composable
|
|||||||
import androidx.compose.runtime.DisposableEffect
|
import androidx.compose.runtime.DisposableEffect
|
||||||
import androidx.compose.runtime.LaunchedEffect
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.runtime.livedata.observeAsState
|
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.platform.LocalLifecycleOwner
|
import androidx.compose.ui.platform.LocalLifecycleOwner
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
@@ -15,55 +14,51 @@ import androidx.lifecycle.Lifecycle
|
|||||||
import androidx.lifecycle.LifecycleEventObserver
|
import androidx.lifecycle.LifecycleEventObserver
|
||||||
import androidx.lifecycle.viewmodel.compose.viewModel
|
import androidx.lifecycle.viewmodel.compose.viewModel
|
||||||
import com.vitorpamplona.amethyst.service.NostrThreadDataSource
|
import com.vitorpamplona.amethyst.service.NostrThreadDataSource
|
||||||
import com.vitorpamplona.amethyst.ui.dal.ThreadFeedFilter
|
|
||||||
import com.vitorpamplona.amethyst.ui.screen.NostrThreadFeedViewModel
|
import com.vitorpamplona.amethyst.ui.screen.NostrThreadFeedViewModel
|
||||||
import com.vitorpamplona.amethyst.ui.screen.ThreadFeedView
|
import com.vitorpamplona.amethyst.ui.screen.ThreadFeedView
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun ThreadScreen(noteId: String?, accountViewModel: AccountViewModel, nav: (String) -> Unit) {
|
fun ThreadScreen(noteId: String?, accountViewModel: AccountViewModel, nav: (String) -> Unit) {
|
||||||
val account by accountViewModel.accountLiveData.observeAsState()
|
if (noteId == null) return
|
||||||
|
|
||||||
val lifeCycleOwner = LocalLifecycleOwner.current
|
val lifeCycleOwner = LocalLifecycleOwner.current
|
||||||
|
|
||||||
if (account != null && noteId != null) {
|
val feedViewModel: NostrThreadFeedViewModel = viewModel(
|
||||||
ThreadFeedFilter.loadThread(noteId)
|
key = noteId + "NostrThreadFeedViewModel",
|
||||||
val feedViewModel: NostrThreadFeedViewModel = viewModel()
|
factory = NostrThreadFeedViewModel.Factory(noteId)
|
||||||
|
)
|
||||||
|
|
||||||
LaunchedEffect(noteId) {
|
LaunchedEffect(noteId) {
|
||||||
NostrThreadDataSource.loadThread(noteId)
|
NostrThreadDataSource.loadThread(noteId)
|
||||||
ThreadFeedFilter.loadThread(noteId)
|
feedViewModel.invalidateData()
|
||||||
feedViewModel.invalidateData()
|
}
|
||||||
}
|
|
||||||
|
|
||||||
DisposableEffect(accountViewModel) {
|
DisposableEffect(accountViewModel) {
|
||||||
val observer = LifecycleEventObserver { _, event ->
|
val observer = LifecycleEventObserver { _, event ->
|
||||||
if (event == Lifecycle.Event.ON_RESUME) {
|
if (event == Lifecycle.Event.ON_RESUME) {
|
||||||
println("Thread Start")
|
println("Thread Start")
|
||||||
NostrThreadDataSource.loadThread(noteId)
|
NostrThreadDataSource.loadThread(noteId)
|
||||||
NostrThreadDataSource.start()
|
NostrThreadDataSource.start()
|
||||||
ThreadFeedFilter.loadThread(noteId)
|
feedViewModel.invalidateData()
|
||||||
feedViewModel.invalidateData()
|
|
||||||
}
|
|
||||||
if (event == Lifecycle.Event.ON_PAUSE) {
|
|
||||||
println("Thread Stop")
|
|
||||||
ThreadFeedFilter.loadThread(null)
|
|
||||||
NostrThreadDataSource.loadThread(null)
|
|
||||||
NostrThreadDataSource.stop()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
if (event == Lifecycle.Event.ON_PAUSE) {
|
||||||
lifeCycleOwner.lifecycle.addObserver(observer)
|
println("Thread Stop")
|
||||||
onDispose {
|
NostrThreadDataSource.loadThread(null)
|
||||||
lifeCycleOwner.lifecycle.removeObserver(observer)
|
NostrThreadDataSource.stop()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Column(Modifier.fillMaxHeight()) {
|
lifeCycleOwner.lifecycle.addObserver(observer)
|
||||||
Column(
|
onDispose {
|
||||||
modifier = Modifier.padding(vertical = 0.dp)
|
lifeCycleOwner.lifecycle.removeObserver(observer)
|
||||||
) {
|
}
|
||||||
ThreadFeedView(noteId, feedViewModel, accountViewModel, nav)
|
}
|
||||||
}
|
|
||||||
|
Column(Modifier.fillMaxHeight()) {
|
||||||
|
Column(
|
||||||
|
modifier = Modifier.padding(vertical = 0.dp)
|
||||||
|
) {
|
||||||
|
ThreadFeedView(noteId, feedViewModel, accountViewModel, nav)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -68,7 +68,6 @@ import com.vitorpamplona.amethyst.ui.actions.NewMediaModel
|
|||||||
import com.vitorpamplona.amethyst.ui.actions.NewMediaView
|
import com.vitorpamplona.amethyst.ui.actions.NewMediaView
|
||||||
import com.vitorpamplona.amethyst.ui.actions.NewPostView
|
import com.vitorpamplona.amethyst.ui.actions.NewPostView
|
||||||
import com.vitorpamplona.amethyst.ui.components.ObserveDisplayNip05Status
|
import com.vitorpamplona.amethyst.ui.components.ObserveDisplayNip05Status
|
||||||
import com.vitorpamplona.amethyst.ui.dal.VideoFeedFilter
|
|
||||||
import com.vitorpamplona.amethyst.ui.navigation.Route
|
import com.vitorpamplona.amethyst.ui.navigation.Route
|
||||||
import com.vitorpamplona.amethyst.ui.note.FileHeaderDisplay
|
import com.vitorpamplona.amethyst.ui.note.FileHeaderDisplay
|
||||||
import com.vitorpamplona.amethyst.ui.note.FileStorageHeaderDisplay
|
import com.vitorpamplona.amethyst.ui.note.FileStorageHeaderDisplay
|
||||||
@@ -82,12 +81,12 @@ import com.vitorpamplona.amethyst.ui.note.ZapReaction
|
|||||||
import com.vitorpamplona.amethyst.ui.screen.FeedEmpty
|
import com.vitorpamplona.amethyst.ui.screen.FeedEmpty
|
||||||
import com.vitorpamplona.amethyst.ui.screen.FeedError
|
import com.vitorpamplona.amethyst.ui.screen.FeedError
|
||||||
import com.vitorpamplona.amethyst.ui.screen.FeedState
|
import com.vitorpamplona.amethyst.ui.screen.FeedState
|
||||||
|
import com.vitorpamplona.amethyst.ui.screen.FeedViewModel
|
||||||
import com.vitorpamplona.amethyst.ui.screen.LoadingFeed
|
import com.vitorpamplona.amethyst.ui.screen.LoadingFeed
|
||||||
import com.vitorpamplona.amethyst.ui.screen.NostrVideoFeedViewModel
|
import com.vitorpamplona.amethyst.ui.screen.NostrVideoFeedViewModel
|
||||||
import com.vitorpamplona.amethyst.ui.screen.ScrollStateKeys
|
import com.vitorpamplona.amethyst.ui.screen.ScrollStateKeys
|
||||||
import com.vitorpamplona.amethyst.ui.screen.rememberForeverPagerState
|
import com.vitorpamplona.amethyst.ui.screen.rememberForeverPagerState
|
||||||
import kotlinx.collections.immutable.ImmutableList
|
import kotlinx.collections.immutable.ImmutableList
|
||||||
import kotlinx.coroutines.Dispatchers
|
|
||||||
import kotlinx.coroutines.delay
|
import kotlinx.coroutines.delay
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
|
|
||||||
@@ -95,30 +94,16 @@ import kotlinx.coroutines.launch
|
|||||||
fun VideoScreen(
|
fun VideoScreen(
|
||||||
videoFeedView: NostrVideoFeedViewModel,
|
videoFeedView: NostrVideoFeedViewModel,
|
||||||
accountViewModel: AccountViewModel,
|
accountViewModel: AccountViewModel,
|
||||||
nav: (String) -> Unit,
|
nav: (String) -> Unit
|
||||||
scrollToTop: Boolean = false
|
|
||||||
) {
|
) {
|
||||||
val lifeCycleOwner = LocalLifecycleOwner.current
|
val lifeCycleOwner = LocalLifecycleOwner.current
|
||||||
val account = accountViewModel.accountLiveData.value?.account ?: return
|
|
||||||
|
|
||||||
val accountState = account.live.observeAsState()
|
WatchAccountForVideoScreen(videoFeedView = videoFeedView, accountViewModel = accountViewModel)
|
||||||
|
|
||||||
NostrVideoDataSource.account = account
|
|
||||||
VideoFeedFilter.account = account
|
|
||||||
|
|
||||||
LaunchedEffect(accountViewModel, accountState.value?.account?.defaultStoriesFollowList) {
|
|
||||||
VideoFeedFilter.account = account
|
|
||||||
NostrVideoDataSource.account = account
|
|
||||||
NostrVideoDataSource.resetFilters()
|
|
||||||
videoFeedView.invalidateData()
|
|
||||||
}
|
|
||||||
|
|
||||||
DisposableEffect(accountViewModel) {
|
DisposableEffect(accountViewModel) {
|
||||||
val observer = LifecycleEventObserver { _, event ->
|
val observer = LifecycleEventObserver { _, event ->
|
||||||
if (event == Lifecycle.Event.ON_RESUME) {
|
if (event == Lifecycle.Event.ON_RESUME) {
|
||||||
println("Video Start")
|
println("Video Start")
|
||||||
VideoFeedFilter.account = account
|
|
||||||
NostrVideoDataSource.account = account
|
|
||||||
NostrVideoDataSource.start()
|
NostrVideoDataSource.start()
|
||||||
}
|
}
|
||||||
if (event == Lifecycle.Event.ON_PAUSE) {
|
if (event == Lifecycle.Event.ON_PAUSE) {
|
||||||
@@ -133,32 +118,69 @@ fun VideoScreen(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (scrollToTop) {
|
|
||||||
val scope = rememberCoroutineScope()
|
|
||||||
LaunchedEffect(key1 = Unit) {
|
|
||||||
scope.launch(Dispatchers.IO) {
|
|
||||||
NostrVideoDataSource.resetFilters()
|
|
||||||
videoFeedView.invalidateData()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Column(Modifier.fillMaxHeight()) {
|
Column(Modifier.fillMaxHeight()) {
|
||||||
Column(
|
Column(
|
||||||
modifier = Modifier.padding(vertical = 0.dp)
|
modifier = Modifier.padding(vertical = 0.dp)
|
||||||
) {
|
) {
|
||||||
FeedView(videoFeedView, accountViewModel, nav, ScrollStateKeys.VIDEO_SCREEN, scrollToTop)
|
SaveableFeedState(videoFeedView, accountViewModel, nav, ScrollStateKeys.VIDEO_SCREEN)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun FeedView(
|
fun WatchAccountForVideoScreen(videoFeedView: NostrVideoFeedViewModel, accountViewModel: AccountViewModel) {
|
||||||
|
val accountState by accountViewModel.accountLiveData.observeAsState()
|
||||||
|
val account = remember(accountState) { accountState?.account } ?: return
|
||||||
|
|
||||||
|
LaunchedEffect(accountViewModel, account.defaultStoriesFollowList) {
|
||||||
|
NostrVideoDataSource.resetFilters()
|
||||||
|
videoFeedView.invalidateDataAndSendToTop(true)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@OptIn(ExperimentalFoundationApi::class)
|
||||||
|
@Composable
|
||||||
|
private fun SaveableFeedState(
|
||||||
videoFeedView: NostrVideoFeedViewModel,
|
videoFeedView: NostrVideoFeedViewModel,
|
||||||
accountViewModel: AccountViewModel,
|
accountViewModel: AccountViewModel,
|
||||||
nav: (String) -> Unit,
|
nav: (String) -> Unit,
|
||||||
scrollStateKey: String? = null,
|
routeForLastRead: String?,
|
||||||
scrollToTop: Boolean = false
|
scrollStateKey: String? = null
|
||||||
|
) {
|
||||||
|
val pagerState = if (scrollStateKey != null) {
|
||||||
|
rememberForeverPagerState(scrollStateKey)
|
||||||
|
} else {
|
||||||
|
remember { PagerState() }
|
||||||
|
}
|
||||||
|
|
||||||
|
WatchScrollToTop(videoFeedView, pagerState)
|
||||||
|
|
||||||
|
RenderPage(videoFeedView, accountViewModel, pagerState, nav)
|
||||||
|
}
|
||||||
|
|
||||||
|
@OptIn(ExperimentalFoundationApi::class)
|
||||||
|
@Composable
|
||||||
|
public fun WatchScrollToTop(
|
||||||
|
viewModel: FeedViewModel,
|
||||||
|
pagerState: PagerState
|
||||||
|
) {
|
||||||
|
val scrollToTop by viewModel.scrollToTop.collectAsState()
|
||||||
|
|
||||||
|
LaunchedEffect(scrollToTop) {
|
||||||
|
if (scrollToTop > 0 && viewModel.scrolltoTopPending) {
|
||||||
|
pagerState.scrollToPage(page = 0)
|
||||||
|
viewModel.sentToTop()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@OptIn(ExperimentalFoundationApi::class)
|
||||||
|
@Composable
|
||||||
|
fun RenderPage(
|
||||||
|
videoFeedView: NostrVideoFeedViewModel,
|
||||||
|
accountViewModel: AccountViewModel,
|
||||||
|
pagerState: PagerState,
|
||||||
|
nav: (String) -> Unit
|
||||||
) {
|
) {
|
||||||
val feedState by videoFeedView.feedContent.collectAsState()
|
val feedState by videoFeedView.feedContent.collectAsState()
|
||||||
|
|
||||||
@@ -180,10 +202,9 @@ fun FeedView(
|
|||||||
is FeedState.Loaded -> {
|
is FeedState.Loaded -> {
|
||||||
SlidingCarousel(
|
SlidingCarousel(
|
||||||
state.feed,
|
state.feed,
|
||||||
|
pagerState,
|
||||||
accountViewModel,
|
accountViewModel,
|
||||||
nav,
|
nav
|
||||||
scrollStateKey,
|
|
||||||
scrollToTop
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -200,23 +221,10 @@ fun FeedView(
|
|||||||
@Composable
|
@Composable
|
||||||
fun SlidingCarousel(
|
fun SlidingCarousel(
|
||||||
feed: MutableState<ImmutableList<Note>>,
|
feed: MutableState<ImmutableList<Note>>,
|
||||||
|
pagerState: PagerState,
|
||||||
accountViewModel: AccountViewModel,
|
accountViewModel: AccountViewModel,
|
||||||
nav: (String) -> Unit,
|
nav: (String) -> Unit
|
||||||
scrollStateKey: String? = null,
|
|
||||||
scrollToTop: Boolean = false
|
|
||||||
) {
|
) {
|
||||||
val pagerState = if (scrollStateKey != null) {
|
|
||||||
rememberForeverPagerState(scrollStateKey)
|
|
||||||
} else {
|
|
||||||
remember { PagerState() }
|
|
||||||
}
|
|
||||||
|
|
||||||
if (scrollToTop) {
|
|
||||||
LaunchedEffect(Unit) {
|
|
||||||
pagerState.scrollToPage(page = 0)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
VerticalPager(
|
VerticalPager(
|
||||||
pageCount = feed.value.size,
|
pageCount = feed.value.size,
|
||||||
state = pagerState,
|
state = pagerState,
|
||||||
|
|||||||
+153
-140
@@ -55,26 +55,16 @@ fun TranslatableRichTextViewer(
|
|||||||
var showOriginal by remember { mutableStateOf(false) }
|
var showOriginal by remember { mutableStateOf(false) }
|
||||||
var langSettingsPopupExpanded by remember { mutableStateOf(false) }
|
var langSettingsPopupExpanded by remember { mutableStateOf(false) }
|
||||||
|
|
||||||
val accountState by accountViewModel.accountLanguagesLiveData.observeAsState()
|
WatchLanguageChanges(content, accountViewModel) { result, newShowOriginal ->
|
||||||
val account = remember(accountState) { accountState?.account } ?: return
|
if (translatedTextState.result != result.result ||
|
||||||
|
translatedTextState.sourceLang != result.sourceLang ||
|
||||||
|
translatedTextState.targetLang != result.targetLang
|
||||||
|
) {
|
||||||
|
translatedTextState = result
|
||||||
|
}
|
||||||
|
|
||||||
val scope = rememberCoroutineScope()
|
if (showOriginal != newShowOriginal) {
|
||||||
|
showOriginal = newShowOriginal
|
||||||
LaunchedEffect(accountState) {
|
|
||||||
scope.launch(Dispatchers.IO) {
|
|
||||||
LanguageTranslatorService.autoTranslate(
|
|
||||||
content,
|
|
||||||
account.dontTranslateFrom,
|
|
||||||
account.translateTo
|
|
||||||
).addOnCompleteListener { task ->
|
|
||||||
if (task.isSuccessful && content != task.result.result) {
|
|
||||||
if (task.result.sourceLang != null && task.result.targetLang != null) {
|
|
||||||
val preference = account.preferenceBetween(task.result.sourceLang!!, task.result.targetLang!!)
|
|
||||||
showOriginal = preference == task.result.sourceLang
|
|
||||||
}
|
|
||||||
translatedTextState = task.result
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -98,137 +88,135 @@ fun TranslatableRichTextViewer(
|
|||||||
val target = translatedTextState.targetLang
|
val target = translatedTextState.targetLang
|
||||||
val source = translatedTextState.sourceLang
|
val source = translatedTextState.sourceLang
|
||||||
|
|
||||||
if (source != null && target != null) {
|
if (source != null && target != null && source != target) {
|
||||||
if (source != target) {
|
Row(
|
||||||
Row(
|
modifier = Modifier
|
||||||
modifier = Modifier
|
.fillMaxWidth()
|
||||||
.fillMaxWidth()
|
.padding(top = 5.dp)
|
||||||
.padding(top = 5.dp)
|
) {
|
||||||
|
val clickableTextStyle =
|
||||||
|
SpanStyle(color = MaterialTheme.colors.primary.copy(alpha = 0.52f))
|
||||||
|
|
||||||
|
val annotatedTranslationString = buildAnnotatedString {
|
||||||
|
withStyle(clickableTextStyle) {
|
||||||
|
pushStringAnnotation("langSettings", true.toString())
|
||||||
|
append(stringResource(R.string.translations_auto))
|
||||||
|
}
|
||||||
|
|
||||||
|
append("-${stringResource(R.string.translations_translated_from)} ")
|
||||||
|
|
||||||
|
withStyle(clickableTextStyle) {
|
||||||
|
pushStringAnnotation("showOriginal", true.toString())
|
||||||
|
append(Locale(source).displayName)
|
||||||
|
}
|
||||||
|
|
||||||
|
append(" ${stringResource(R.string.translations_to)} ")
|
||||||
|
|
||||||
|
withStyle(clickableTextStyle) {
|
||||||
|
pushStringAnnotation("showOriginal", false.toString())
|
||||||
|
append(Locale(target).displayName)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ClickableText(
|
||||||
|
text = annotatedTranslationString,
|
||||||
|
style = LocalTextStyle.current.copy(color = MaterialTheme.colors.onSurface.copy(alpha = 0.32f)),
|
||||||
|
overflow = TextOverflow.Visible,
|
||||||
|
maxLines = 3
|
||||||
|
) { spanOffset ->
|
||||||
|
annotatedTranslationString.getStringAnnotations(spanOffset, spanOffset)
|
||||||
|
.firstOrNull()
|
||||||
|
?.also { span ->
|
||||||
|
if (span.tag == "showOriginal") {
|
||||||
|
showOriginal = span.item.toBoolean()
|
||||||
|
} else {
|
||||||
|
langSettingsPopupExpanded = !langSettingsPopupExpanded
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
DropdownMenu(
|
||||||
|
expanded = langSettingsPopupExpanded,
|
||||||
|
onDismissRequest = { langSettingsPopupExpanded = false }
|
||||||
) {
|
) {
|
||||||
val clickableTextStyle =
|
DropdownMenuItem(onClick = {
|
||||||
SpanStyle(color = MaterialTheme.colors.primary.copy(alpha = 0.52f))
|
accountViewModel.dontTranslateFrom(source)
|
||||||
|
langSettingsPopupExpanded = false
|
||||||
val annotatedTranslationString = buildAnnotatedString {
|
}) {
|
||||||
withStyle(clickableTextStyle) {
|
if (source in accountViewModel.account.dontTranslateFrom) {
|
||||||
pushStringAnnotation("langSettings", true.toString())
|
Icon(
|
||||||
append(stringResource(R.string.translations_auto))
|
imageVector = Icons.Default.Check,
|
||||||
|
contentDescription = null,
|
||||||
|
modifier = Modifier.size(24.dp)
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
Spacer(modifier = Modifier.size(24.dp))
|
||||||
}
|
}
|
||||||
|
|
||||||
append("-${stringResource(R.string.translations_translated_from)} ")
|
Spacer(modifier = Modifier.size(10.dp))
|
||||||
|
|
||||||
withStyle(clickableTextStyle) {
|
Text(stringResource(R.string.translations_never_translate_from_lang, Locale(source).displayName))
|
||||||
pushStringAnnotation("showOriginal", true.toString())
|
|
||||||
append(Locale(source).displayName)
|
|
||||||
}
|
|
||||||
|
|
||||||
append(" ${stringResource(R.string.translations_to)} ")
|
|
||||||
|
|
||||||
withStyle(clickableTextStyle) {
|
|
||||||
pushStringAnnotation("showOriginal", false.toString())
|
|
||||||
append(Locale(target).displayName)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
Divider()
|
||||||
|
DropdownMenuItem(onClick = {
|
||||||
|
accountViewModel.prefer(source, target, source)
|
||||||
|
langSettingsPopupExpanded = false
|
||||||
|
}) {
|
||||||
|
if (accountViewModel.account.preferenceBetween(source, target) == source) {
|
||||||
|
Icon(
|
||||||
|
imageVector = Icons.Default.Check,
|
||||||
|
contentDescription = null,
|
||||||
|
modifier = Modifier.size(24.dp)
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
Spacer(modifier = Modifier.size(24.dp))
|
||||||
|
}
|
||||||
|
|
||||||
ClickableText(
|
Spacer(modifier = Modifier.size(10.dp))
|
||||||
text = annotatedTranslationString,
|
|
||||||
style = LocalTextStyle.current.copy(color = MaterialTheme.colors.onSurface.copy(alpha = 0.32f)),
|
Text(stringResource(R.string.translations_show_in_lang_first, Locale(source).displayName))
|
||||||
overflow = TextOverflow.Visible,
|
}
|
||||||
maxLines = 3
|
DropdownMenuItem(onClick = {
|
||||||
) { spanOffset ->
|
accountViewModel.prefer(source, target, target)
|
||||||
annotatedTranslationString.getStringAnnotations(spanOffset, spanOffset)
|
langSettingsPopupExpanded = false
|
||||||
.firstOrNull()
|
}) {
|
||||||
?.also { span ->
|
if (accountViewModel.account.preferenceBetween(source, target) == target) {
|
||||||
if (span.tag == "showOriginal") {
|
Icon(
|
||||||
showOriginal = span.item.toBoolean()
|
imageVector = Icons.Default.Check,
|
||||||
|
contentDescription = null,
|
||||||
|
modifier = Modifier.size(24.dp)
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
Spacer(modifier = Modifier.size(24.dp))
|
||||||
|
}
|
||||||
|
|
||||||
|
Spacer(modifier = Modifier.size(10.dp))
|
||||||
|
|
||||||
|
Text(stringResource(R.string.translations_show_in_lang_first, Locale(target).displayName))
|
||||||
|
}
|
||||||
|
Divider()
|
||||||
|
|
||||||
|
val languageList =
|
||||||
|
ConfigurationCompat.getLocales(Resources.getSystem().configuration)
|
||||||
|
for (i in 0 until languageList.size()) {
|
||||||
|
languageList.get(i)?.let { lang ->
|
||||||
|
DropdownMenuItem(onClick = {
|
||||||
|
accountViewModel.translateTo(lang)
|
||||||
|
langSettingsPopupExpanded = false
|
||||||
|
}) {
|
||||||
|
if (lang.language in accountViewModel.account.translateTo) {
|
||||||
|
Icon(
|
||||||
|
imageVector = Icons.Default.Check,
|
||||||
|
contentDescription = null,
|
||||||
|
modifier = Modifier.size(24.dp)
|
||||||
|
)
|
||||||
} else {
|
} else {
|
||||||
langSettingsPopupExpanded = !langSettingsPopupExpanded
|
Spacer(modifier = Modifier.size(24.dp))
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
DropdownMenu(
|
Spacer(modifier = Modifier.size(10.dp))
|
||||||
expanded = langSettingsPopupExpanded,
|
|
||||||
onDismissRequest = { langSettingsPopupExpanded = false }
|
|
||||||
) {
|
|
||||||
DropdownMenuItem(onClick = {
|
|
||||||
accountViewModel.dontTranslateFrom(source)
|
|
||||||
langSettingsPopupExpanded = false
|
|
||||||
}) {
|
|
||||||
if (source in account.dontTranslateFrom) {
|
|
||||||
Icon(
|
|
||||||
imageVector = Icons.Default.Check,
|
|
||||||
contentDescription = null,
|
|
||||||
modifier = Modifier.size(24.dp)
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
Spacer(modifier = Modifier.size(24.dp))
|
|
||||||
}
|
|
||||||
|
|
||||||
Spacer(modifier = Modifier.size(10.dp))
|
Text(stringResource(R.string.translations_always_translate_to_lang, lang.displayName))
|
||||||
|
|
||||||
Text(stringResource(R.string.translations_never_translate_from_lang, Locale(source).displayName))
|
|
||||||
}
|
|
||||||
Divider()
|
|
||||||
DropdownMenuItem(onClick = {
|
|
||||||
accountViewModel.prefer(source, target, source)
|
|
||||||
langSettingsPopupExpanded = false
|
|
||||||
}) {
|
|
||||||
if (account.preferenceBetween(source, target) == source) {
|
|
||||||
Icon(
|
|
||||||
imageVector = Icons.Default.Check,
|
|
||||||
contentDescription = null,
|
|
||||||
modifier = Modifier.size(24.dp)
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
Spacer(modifier = Modifier.size(24.dp))
|
|
||||||
}
|
|
||||||
|
|
||||||
Spacer(modifier = Modifier.size(10.dp))
|
|
||||||
|
|
||||||
Text(stringResource(R.string.translations_show_in_lang_first, Locale(source).displayName))
|
|
||||||
}
|
|
||||||
DropdownMenuItem(onClick = {
|
|
||||||
accountViewModel.prefer(source, target, target)
|
|
||||||
langSettingsPopupExpanded = false
|
|
||||||
}) {
|
|
||||||
if (account.preferenceBetween(source, target) == target) {
|
|
||||||
Icon(
|
|
||||||
imageVector = Icons.Default.Check,
|
|
||||||
contentDescription = null,
|
|
||||||
modifier = Modifier.size(24.dp)
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
Spacer(modifier = Modifier.size(24.dp))
|
|
||||||
}
|
|
||||||
|
|
||||||
Spacer(modifier = Modifier.size(10.dp))
|
|
||||||
|
|
||||||
Text(stringResource(R.string.translations_show_in_lang_first, Locale(target).displayName))
|
|
||||||
}
|
|
||||||
Divider()
|
|
||||||
|
|
||||||
val languageList =
|
|
||||||
ConfigurationCompat.getLocales(Resources.getSystem().configuration)
|
|
||||||
for (i in 0 until languageList.size()) {
|
|
||||||
languageList.get(i)?.let { lang ->
|
|
||||||
DropdownMenuItem(onClick = {
|
|
||||||
accountViewModel.translateTo(lang)
|
|
||||||
langSettingsPopupExpanded = false
|
|
||||||
}) {
|
|
||||||
if (lang.language in account.translateTo) {
|
|
||||||
Icon(
|
|
||||||
imageVector = Icons.Default.Check,
|
|
||||||
contentDescription = null,
|
|
||||||
modifier = Modifier.size(24.dp)
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
Spacer(modifier = Modifier.size(24.dp))
|
|
||||||
}
|
|
||||||
|
|
||||||
Spacer(modifier = Modifier.size(10.dp))
|
|
||||||
|
|
||||||
Text(stringResource(R.string.translations_always_translate_to_lang, lang.displayName))
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -237,3 +225,28 @@ fun TranslatableRichTextViewer(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun WatchLanguageChanges(content: String, accountViewModel: AccountViewModel, onTranslated: (ResultOrError, Boolean) -> Unit) {
|
||||||
|
val accountState by accountViewModel.accountLanguagesLiveData.observeAsState()
|
||||||
|
val account = remember(accountState) { accountState?.account } ?: return
|
||||||
|
|
||||||
|
val scope = rememberCoroutineScope()
|
||||||
|
|
||||||
|
LaunchedEffect(accountState) {
|
||||||
|
scope.launch(Dispatchers.IO) {
|
||||||
|
LanguageTranslatorService.autoTranslate(
|
||||||
|
content,
|
||||||
|
account.dontTranslateFrom,
|
||||||
|
account.translateTo
|
||||||
|
).addOnCompleteListener { task ->
|
||||||
|
if (task.isSuccessful && content != task.result.result) {
|
||||||
|
if (task.result.sourceLang != null && task.result.targetLang != null) {
|
||||||
|
val preference = account.preferenceBetween(task.result.sourceLang!!, task.result.targetLang!!)
|
||||||
|
onTranslated(task.result, preference == task.result.sourceLang)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user