Restructuring ViewModels
This commit is contained in:
@@ -39,14 +39,14 @@ object NotificationCache {
|
||||
|
||||
class NotificationLiveData(val cache: NotificationCache) : LiveData<NotificationState>(NotificationState(cache)) {
|
||||
// Refreshes observers in batches.
|
||||
private val bundler = BundledUpdate(300, Dispatchers.IO) {
|
||||
if (hasActiveObservers()) {
|
||||
postValue(NotificationState(cache))
|
||||
}
|
||||
}
|
||||
private val bundler = BundledUpdate(300, Dispatchers.IO)
|
||||
|
||||
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)) {
|
||||
// Refreshes observers in batches.
|
||||
private val bundler = BundledUpdate(300, Dispatchers.Default) {
|
||||
if (hasActiveObservers()) {
|
||||
refresh()
|
||||
}
|
||||
}
|
||||
private val bundler = BundledUpdate(300, Dispatchers.Default)
|
||||
|
||||
fun invalidateData() {
|
||||
bundler.invalidate()
|
||||
bundler.invalidate() {
|
||||
if (hasActiveObservers()) {
|
||||
refresh()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun refresh() {
|
||||
|
||||
@@ -55,14 +55,14 @@ class AntiSpamFilter {
|
||||
class AntiSpamLiveData(val cache: AntiSpamFilter) : LiveData<AntiSpamState>(AntiSpamState(cache)) {
|
||||
|
||||
// Refreshes observers in batches.
|
||||
private val bundler = BundledUpdate(300, Dispatchers.IO) {
|
||||
if (hasActiveObservers()) {
|
||||
postValue(AntiSpamState(cache))
|
||||
}
|
||||
}
|
||||
private val bundler = BundledUpdate(300, Dispatchers.IO)
|
||||
|
||||
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)) {
|
||||
// Refreshes observers in batches.
|
||||
private val bundler = BundledUpdate(300, Dispatchers.IO) {
|
||||
if (hasActiveObservers()) {
|
||||
postValue(ChannelState(channel))
|
||||
}
|
||||
}
|
||||
private val bundler = BundledUpdate(300, Dispatchers.IO)
|
||||
|
||||
fun invalidateData() {
|
||||
bundler.invalidate()
|
||||
bundler.invalidate() {
|
||||
if (hasActiveObservers()) {
|
||||
postValue(ChannelState(channel))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onActive() {
|
||||
|
||||
@@ -414,14 +414,14 @@ class NoteLiveSet(u: Note) {
|
||||
|
||||
class NoteLiveData(val note: Note) : LiveData<NoteState>(NoteState(note)) {
|
||||
// Refreshes observers in batches.
|
||||
private val bundler = BundledUpdate(300, Dispatchers.IO) {
|
||||
// if (hasObservers()) {
|
||||
postValue(NoteState(note))
|
||||
// }
|
||||
}
|
||||
private val bundler = BundledUpdate(300, Dispatchers.IO)
|
||||
|
||||
fun invalidateData() {
|
||||
bundler.invalidate()
|
||||
bundler.invalidate() {
|
||||
// if (hasObservers()) {
|
||||
postValue(NoteState(note))
|
||||
// }
|
||||
}
|
||||
}
|
||||
|
||||
override fun onActive() {
|
||||
|
||||
@@ -400,14 +400,14 @@ class UserMetadata {
|
||||
|
||||
class UserLiveData(val user: User) : LiveData<UserState>(UserState(user)) {
|
||||
// Refreshes observers in batches.
|
||||
private val bundler = BundledUpdate(300, Dispatchers.IO) {
|
||||
if (hasActiveObservers()) {
|
||||
postValue(UserState(user))
|
||||
}
|
||||
}
|
||||
private val bundler = BundledUpdate(300, Dispatchers.IO)
|
||||
|
||||
fun invalidateData() {
|
||||
bundler.invalidate()
|
||||
bundler.invalidate() {
|
||||
if (hasActiveObservers()) {
|
||||
postValue(UserState(user))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onActive() {
|
||||
|
||||
@@ -103,16 +103,16 @@ abstract class NostrDataSource(val debugName: String) {
|
||||
}
|
||||
|
||||
// Refreshes observers in batches.
|
||||
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()
|
||||
}
|
||||
private val bundler = BundledUpdate(300, Dispatchers.IO)
|
||||
|
||||
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() {
|
||||
|
||||
@@ -49,18 +49,18 @@ import androidx.compose.ui.window.Dialog
|
||||
import androidx.compose.ui.window.DialogProperties
|
||||
import androidx.lifecycle.viewmodel.compose.viewModel
|
||||
import com.vitorpamplona.amethyst.R
|
||||
import com.vitorpamplona.amethyst.model.Account
|
||||
import com.vitorpamplona.amethyst.model.RelaySetupInfo
|
||||
import com.vitorpamplona.amethyst.service.relays.FeedType
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
||||
import java.lang.Math.round
|
||||
|
||||
@Composable
|
||||
fun NewRelayListView(onClose: () -> Unit, account: Account, relayToAdd: String = "") {
|
||||
fun NewRelayListView(onClose: () -> Unit, accountViewModel: AccountViewModel, relayToAdd: String = "") {
|
||||
val postViewModel: NewRelayListViewModel = viewModel()
|
||||
val feedState by postViewModel.relays.collectAsState()
|
||||
|
||||
LaunchedEffect(Unit) {
|
||||
postViewModel.load(account)
|
||||
postViewModel.load(accountViewModel.account)
|
||||
}
|
||||
|
||||
Dialog(
|
||||
|
||||
@@ -16,13 +16,12 @@ import java.util.concurrent.atomic.AtomicBoolean
|
||||
*/
|
||||
class BundledUpdate(
|
||||
val delay: Long,
|
||||
val dispatcher: CoroutineDispatcher = Dispatchers.Default,
|
||||
val onUpdate: () -> Unit
|
||||
val dispatcher: CoroutineDispatcher = Dispatchers.Default
|
||||
) {
|
||||
private var onlyOneInBlock = AtomicBoolean()
|
||||
private var invalidatesAgain = false
|
||||
|
||||
fun invalidate(ignoreIfDoing: Boolean = false) {
|
||||
fun invalidate(ignoreIfDoing: Boolean = false, onUpdate: suspend () -> Unit) {
|
||||
if (onlyOneInBlock.getAndSet(true)) {
|
||||
if (!ignoreIfDoing) {
|
||||
invalidatesAgain = true
|
||||
|
||||
@@ -157,145 +157,159 @@ private fun RenderRegular(
|
||||
// FlowRow doesn't work well with paragraphs. So we need to split them
|
||||
content.split('\n').forEach { paragraph ->
|
||||
FlowRow() {
|
||||
val s = if (isArabic(paragraph)) {
|
||||
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)
|
||||
)
|
||||
}
|
||||
val s = remember(paragraph) {
|
||||
if (isArabic(paragraph)) {
|
||||
paragraph.trim().split(' ').reversed()
|
||||
} 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)
|
||||
)
|
||||
}
|
||||
paragraph.trim().split(' ')
|
||||
}
|
||||
}
|
||||
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
|
||||
|
||||
import android.content.Context
|
||||
import android.graphics.drawable.Drawable
|
||||
import android.net.Uri
|
||||
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 exoPlayer = remember(videoUri) {
|
||||
var exoPlayer by remember { mutableStateOf<ExoPlayer?>(null) }
|
||||
|
||||
LaunchedEffect(key1 = videoUri) {
|
||||
val mediaBuilder = MediaItem.Builder().setUri(videoUri)
|
||||
|
||||
description?.let {
|
||||
@@ -135,7 +138,7 @@ fun VideoView(videoUri: Uri, description: String? = null, thumb: Drawable? = nul
|
||||
|
||||
val media = mediaBuilder.build()
|
||||
|
||||
ExoPlayer.Builder(context).build().apply {
|
||||
exoPlayer = ExoPlayer.Builder(context).build().apply {
|
||||
repeatMode = Player.REPEAT_MODE_ALL
|
||||
videoScalingMode = C.VIDEO_SCALING_MODE_SCALE_TO_FIT
|
||||
volume = if (mutedInstance.value) 0f else 1f
|
||||
@@ -152,53 +155,15 @@ fun VideoView(videoUri: Uri, description: String? = null, thumb: Drawable? = nul
|
||||
}
|
||||
}
|
||||
|
||||
DisposableEffect(
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
exoPlayer?.let {
|
||||
RenderVideoPlayer(it, context, thumb, onDialog, mutedInstance)
|
||||
}
|
||||
|
||||
MuteButton(mutedInstance) {
|
||||
mutedInstance.value = !mutedInstance.value
|
||||
DefaultMutedSetting.value = mutedInstance.value
|
||||
|
||||
exoPlayer.volume = if (mutedInstance.value) 0f else 1f
|
||||
}
|
||||
}
|
||||
) {
|
||||
DisposableEffect(Unit) {
|
||||
val observer = LifecycleEventObserver { _, event ->
|
||||
when (event) {
|
||||
Lifecycle.Event.ON_PAUSE -> {
|
||||
exoPlayer.pause()
|
||||
exoPlayer?.pause()
|
||||
}
|
||||
else -> {}
|
||||
}
|
||||
@@ -207,12 +172,64 @@ fun VideoView(videoUri: Uri, description: String? = null, thumb: Drawable? = nul
|
||||
lifecycle.addObserver(observer)
|
||||
|
||||
onDispose {
|
||||
exoPlayer.release()
|
||||
exoPlayer?.release()
|
||||
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 {
|
||||
val view = LocalView.current
|
||||
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.measureTimedValue
|
||||
|
||||
object ChatroomListKnownFeedFilter : AdditiveFeedFilter<Note>() {
|
||||
lateinit var account: Account
|
||||
class ChatroomListKnownFeedFilter(val account: Account) : AdditiveFeedFilter<Note>() {
|
||||
|
||||
// returns the last Note of each user.
|
||||
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.Note
|
||||
|
||||
object ChatroomListNewFeedFilter : FeedFilter<Note>() {
|
||||
lateinit var account: Account
|
||||
class ChatroomListNewFeedFilter(val account: Account) : FeedFilter<Note>() {
|
||||
|
||||
// returns the last Note of each user.
|
||||
override fun feed(): List<Note> {
|
||||
val me = account.userProfile()
|
||||
val followingKeySet = ChatroomListKnownFeedFilter.account.followingKeySet()
|
||||
val followingKeySet = account.followingKeySet()
|
||||
|
||||
val privateChatrooms = account.userProfile().privateChatrooms
|
||||
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.service.model.*
|
||||
|
||||
object GlobalFeedFilter : AdditiveFeedFilter<Note>() {
|
||||
lateinit var account: Account
|
||||
class GlobalFeedFilter(val account: Account) : AdditiveFeedFilter<Note>() {
|
||||
|
||||
override fun feed(): List<Note> {
|
||||
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.TextNoteEvent
|
||||
|
||||
object HomeConversationsFeedFilter : AdditiveFeedFilter<Note>() {
|
||||
lateinit var account: Account
|
||||
|
||||
class HomeConversationsFeedFilter(val account: Account) : AdditiveFeedFilter<Note>() {
|
||||
override fun feed(): List<Note> {
|
||||
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.TextNoteEvent
|
||||
|
||||
object HomeNewThreadFeedFilter : AdditiveFeedFilter<Note>() {
|
||||
lateinit var account: Account
|
||||
|
||||
class HomeNewThreadFeedFilter(val account: Account) : AdditiveFeedFilter<Note>() {
|
||||
override fun feed(): List<Note> {
|
||||
val notes = innerApplyFilter(LocalCache.notes.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.service.model.*
|
||||
|
||||
object NotificationFeedFilter : AdditiveFeedFilter<Note>() {
|
||||
lateinit var account: Account
|
||||
|
||||
class NotificationFeedFilter(val account: Account) : AdditiveFeedFilter<Note>() {
|
||||
override fun feed(): List<Note> {
|
||||
return sort(innerApplyFilter(LocalCache.notes.values))
|
||||
}
|
||||
|
||||
@@ -1,21 +1,18 @@
|
||||
package com.vitorpamplona.amethyst.ui.dal
|
||||
|
||||
import com.google.errorprone.annotations.Immutable
|
||||
import com.vitorpamplona.amethyst.model.Note
|
||||
import com.vitorpamplona.amethyst.model.ThreadAssembler
|
||||
|
||||
object ThreadFeedFilter : FeedFilter<Note>() {
|
||||
var noteId: String? = null
|
||||
@Immutable
|
||||
class ThreadFeedFilter(val noteId: String) : FeedFilter<Note>() {
|
||||
|
||||
override fun feed(): List<Note> {
|
||||
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
|
||||
val order = compareByDescending<Note> { it.replyLevelSignature(cachedSignatures) }
|
||||
|
||||
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.service.model.*
|
||||
|
||||
object VideoFeedFilter : AdditiveFeedFilter<Note>() {
|
||||
lateinit var account: Account
|
||||
|
||||
class VideoFeedFilter(val account: Account) : AdditiveFeedFilter<Note>() {
|
||||
override fun feed(): List<Note> {
|
||||
val notes = innerApplyFilter(LocalCache.notes.values)
|
||||
|
||||
|
||||
@@ -151,21 +151,14 @@ private fun RowScope.HasNewItemsIcon(
|
||||
scope.launch {
|
||||
if (!selected) {
|
||||
navController.navigate(route.base) {
|
||||
navController.graph.startDestinationRoute?.let { start ->
|
||||
popUpTo(start)
|
||||
restoreState = true
|
||||
}
|
||||
popUpTo(Route.Home.route)
|
||||
launchSingleTop = true
|
||||
restoreState = true
|
||||
}
|
||||
} else {
|
||||
val newRoute = route.route.replace("{scrollToTop}", "true")
|
||||
navController.navigate(newRoute) {
|
||||
navController.graph.startDestinationRoute?.let { start ->
|
||||
popUpTo(start) { inclusive = route.route == Route.Home.route }
|
||||
restoreState = true
|
||||
}
|
||||
|
||||
popUpTo(Route.Home.route)
|
||||
launchSingleTop = true
|
||||
restoreState = true
|
||||
}
|
||||
@@ -209,27 +202,30 @@ private fun RowScope.BottomIcon(
|
||||
|
||||
@Composable
|
||||
private fun NotifiableIcon(icon: Int, size: Dp, iconSize: Dp, selected: Boolean, hasNewItems: Boolean) {
|
||||
Box(Modifier.size(size)) {
|
||||
Box(remember { Modifier.size(size) }) {
|
||||
Icon(
|
||||
painter = painterResource(id = icon),
|
||||
contentDescription = null,
|
||||
modifier = Modifier.size(iconSize),
|
||||
modifier = remember { Modifier.size(iconSize) },
|
||||
tint = if (selected) MaterialTheme.colors.primary else Color.Unspecified
|
||||
)
|
||||
|
||||
if (hasNewItems) {
|
||||
Box(
|
||||
Modifier
|
||||
.width(10.dp)
|
||||
.height(10.dp)
|
||||
.align(Alignment.TopEnd)
|
||||
) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
remember {
|
||||
Modifier
|
||||
.width(10.dp)
|
||||
.height(10.dp)
|
||||
.clip(shape = CircleShape)
|
||||
.background(MaterialTheme.colors.primary),
|
||||
.align(Alignment.TopEnd)
|
||||
}
|
||||
) {
|
||||
Box(
|
||||
modifier = remember {
|
||||
Modifier
|
||||
.width(10.dp)
|
||||
.height(10.dp)
|
||||
.clip(shape = CircleShape)
|
||||
}.background(MaterialTheme.colors.primary),
|
||||
contentAlignment = Alignment.TopEnd
|
||||
) {
|
||||
Text(
|
||||
@@ -237,9 +233,11 @@ private fun NotifiableIcon(icon: Int, size: Dp, iconSize: Dp, selected: Boolean,
|
||||
color = Color.White,
|
||||
textAlign = TextAlign.Center,
|
||||
fontSize = 12.sp,
|
||||
modifier = Modifier
|
||||
.wrapContentHeight()
|
||||
.align(Alignment.TopEnd)
|
||||
modifier = remember {
|
||||
Modifier
|
||||
.wrapContentHeight()
|
||||
.align(Alignment.TopEnd)
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,25 +1,18 @@
|
||||
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.LaunchedEffect
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.livedata.observeAsState
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.rememberCoroutineScope
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.lifecycle.viewmodel.compose.viewModel
|
||||
import androidx.navigation.NavHostController
|
||||
import androidx.navigation.compose.NavHost
|
||||
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.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
|
||||
@@ -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.ThreadScreen
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.VideoScreen
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
@OptIn(ExperimentalFoundationApi::class)
|
||||
@Composable
|
||||
fun AppNavigation(
|
||||
homeFeedViewModel: NostrHomeFeedViewModel,
|
||||
repliesFeedViewModel: NostrHomeRepliesFeedViewModel,
|
||||
knownFeedViewModel: NostrChatroomListKnownFeedViewModel,
|
||||
newFeedViewModel: NostrChatroomListNewFeedViewModel,
|
||||
searchFeedViewModel: NostrGlobalFeedViewModel,
|
||||
videoFeedViewModel: NostrVideoFeedViewModel,
|
||||
notifFeedViewModel: NotificationViewModel,
|
||||
userReactionsStatsModel: UserReactionsViewModel,
|
||||
|
||||
navController: NavHostController,
|
||||
accountViewModel: AccountViewModel,
|
||||
nextPage: String? = null
|
||||
) {
|
||||
val homePagerState = rememberPagerState()
|
||||
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()
|
||||
|
||||
LaunchedEffect(accountHex) {
|
||||
scope.launch(Dispatchers.IO) {
|
||||
userReactionsStatsModel.load(account.userProfile())
|
||||
userReactionsStatsModel.initializeSuspend()
|
||||
}
|
||||
}
|
||||
|
||||
val nav = remember {
|
||||
{ route: String ->
|
||||
if (getRouteWithArguments(navController) != route) {
|
||||
@@ -96,23 +66,20 @@ fun AppNavigation(
|
||||
composable(route.route, route.arguments, content = {
|
||||
val scrollToTop = it.arguments?.getBoolean("scrollToTop") ?: false
|
||||
|
||||
VideoScreen(
|
||||
videoFeedView = videoFeedViewModel,
|
||||
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) {
|
||||
LaunchedEffect(key1 = it) {
|
||||
if (scrollToTop) {
|
||||
scope.launch {
|
||||
delay(1000)
|
||||
videoFeedViewModel.sendToTop()
|
||||
it.arguments?.remove("scrollToTop")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
VideoScreen(
|
||||
videoFeedView = videoFeedViewModel,
|
||||
accountViewModel = accountViewModel,
|
||||
nav = nav
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
@@ -120,23 +87,20 @@ fun AppNavigation(
|
||||
composable(route.route, route.arguments, content = {
|
||||
val scrollToTop = it.arguments?.getBoolean("scrollToTop") ?: false
|
||||
|
||||
SearchScreen(
|
||||
searchFeedViewModel = searchFeedViewModel,
|
||||
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) {
|
||||
LaunchedEffect(key1 = it) {
|
||||
if (scrollToTop) {
|
||||
scope.launch {
|
||||
delay(1000)
|
||||
searchFeedViewModel.sendToTop()
|
||||
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 nip47 = it.arguments?.getString("nip47")
|
||||
|
||||
LaunchedEffect(key1 = it) {
|
||||
if (scrollToTop) {
|
||||
scope.launch {
|
||||
homeFeedViewModel.sendToTop()
|
||||
repliesFeedViewModel.sendToTop()
|
||||
it.arguments?.remove("scrollToTop")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
HomeScreen(
|
||||
homeFeedViewModel = homeFeedViewModel,
|
||||
repliesFeedViewModel = repliesFeedViewModel,
|
||||
accountViewModel = accountViewModel,
|
||||
nav = nav,
|
||||
pagerState = homePagerState,
|
||||
scrollToTop = scrollToTop,
|
||||
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) {
|
||||
LaunchedEffect(key1 = Unit) {
|
||||
scope.launch {
|
||||
@@ -179,28 +142,37 @@ fun AppNavigation(
|
||||
composable(route.route, route.arguments, content = {
|
||||
val scrollToTop = it.arguments?.getBoolean("scrollToTop") ?: false
|
||||
|
||||
NotificationScreen(
|
||||
notifFeedViewModel = notifFeedViewModel,
|
||||
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) {
|
||||
LaunchedEffect(key1 = it) {
|
||||
if (scrollToTop) {
|
||||
scope.launch {
|
||||
delay(1000)
|
||||
notifFeedViewModel.clear()
|
||||
notifFeedViewModel.sendToTop()
|
||||
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.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.ExpandMore
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.Stable
|
||||
import androidx.compose.runtime.collectAsState
|
||||
import androidx.compose.runtime.derivedStateOf
|
||||
import androidx.compose.runtime.getValue
|
||||
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.unit.dp
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.ViewModelProvider
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import androidx.lifecycle.viewmodel.compose.viewModel
|
||||
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.RobohashAsyncImageProxy
|
||||
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.SpinnerSelectionDialog
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
import kotlinx.collections.immutable.toImmutableList
|
||||
import kotlinx.collections.immutable.toPersistentList
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.asStateFlow
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
|
||||
@Composable
|
||||
fun AppTopBar(followLists: FollowListViewModel, navController: NavHostController, scaffoldState: ScaffoldState, accountViewModel: AccountViewModel) {
|
||||
@@ -93,27 +101,51 @@ fun AppTopBar(followLists: FollowListViewModel, navController: NavHostController
|
||||
|
||||
@Composable
|
||||
fun StoriesTopBar(followLists: FollowListViewModel, scaffoldState: ScaffoldState, accountViewModel: AccountViewModel) {
|
||||
GenericTopBar(scaffoldState, accountViewModel) { account ->
|
||||
FollowList(followLists, account.defaultStoriesFollowList, account.userProfile(), true) { listName ->
|
||||
account.changeDefaultStoriesFollowList(listName)
|
||||
GenericTopBar(scaffoldState, accountViewModel) { accountViewModel ->
|
||||
val accountState by accountViewModel.accountLiveData.observeAsState()
|
||||
accountState?.account?.let { account ->
|
||||
FollowList(
|
||||
followLists,
|
||||
account.defaultStoriesFollowList,
|
||||
account.userProfile(),
|
||||
true
|
||||
) { listName ->
|
||||
account.changeDefaultStoriesFollowList(listName)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun HomeTopBar(followLists: FollowListViewModel, scaffoldState: ScaffoldState, accountViewModel: AccountViewModel) {
|
||||
GenericTopBar(scaffoldState, accountViewModel) { account ->
|
||||
FollowList(followLists, account.defaultHomeFollowList, account.userProfile(), false) { listName ->
|
||||
account.changeDefaultHomeFollowList(listName)
|
||||
GenericTopBar(scaffoldState, accountViewModel) { accountViewModel ->
|
||||
val accountState by accountViewModel.accountLiveData.observeAsState()
|
||||
accountState?.account?.let { account ->
|
||||
FollowList(
|
||||
followLists,
|
||||
account.defaultHomeFollowList,
|
||||
account.userProfile(),
|
||||
false
|
||||
) { listName ->
|
||||
account.changeDefaultHomeFollowList(listName)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun NotificationTopBar(followLists: FollowListViewModel, scaffoldState: ScaffoldState, accountViewModel: AccountViewModel) {
|
||||
GenericTopBar(scaffoldState, accountViewModel) { account ->
|
||||
FollowList(followLists, account.defaultNotificationFollowList, account.userProfile(), true) { listName ->
|
||||
account.changeDefaultNotificationFollowList(listName)
|
||||
GenericTopBar(scaffoldState, accountViewModel) { accountViewModel ->
|
||||
val accountState by accountViewModel.accountLiveData.observeAsState()
|
||||
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)
|
||||
@Composable
|
||||
fun GenericTopBar(scaffoldState: ScaffoldState, accountViewModel: AccountViewModel, content: @Composable (Account) -> Unit) {
|
||||
val accountState by accountViewModel.accountLiveData.observeAsState()
|
||||
val account = accountState?.account ?: return
|
||||
|
||||
fun GenericTopBar(scaffoldState: ScaffoldState, accountViewModel: AccountViewModel, content: @Composable (AccountViewModel) -> Unit) {
|
||||
val relayViewModel: RelayPoolViewModel = viewModel { RelayPoolViewModel() }
|
||||
val connectedRelaysLiveData by relayViewModel.connectedRelaysLiveData.observeAsState()
|
||||
val availableRelaysLiveData by relayViewModel.availableRelaysLiveData.observeAsState()
|
||||
@@ -142,7 +171,7 @@ fun GenericTopBar(scaffoldState: ScaffoldState, accountViewModel: AccountViewMod
|
||||
}
|
||||
|
||||
if (wantsToEditRelays) {
|
||||
NewRelayListView({ wantsToEditRelays = false }, account)
|
||||
NewRelayListView({ wantsToEditRelays = false }, accountViewModel)
|
||||
}
|
||||
|
||||
Column() {
|
||||
@@ -163,7 +192,7 @@ fun GenericTopBar(scaffoldState: ScaffoldState, accountViewModel: AccountViewMod
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
verticalArrangement = Arrangement.Center
|
||||
) {
|
||||
content(account)
|
||||
content(accountViewModel)
|
||||
}
|
||||
|
||||
Column(
|
||||
@@ -193,7 +222,7 @@ fun GenericTopBar(scaffoldState: ScaffoldState, accountViewModel: AccountViewMod
|
||||
}
|
||||
},
|
||||
navigationIcon = {
|
||||
LoggedInUserPictureDrawer(account) {
|
||||
LoggedInUserPictureDrawer(accountViewModel) {
|
||||
coroutineScope.launch {
|
||||
scaffoldState.drawerState.open()
|
||||
}
|
||||
@@ -219,10 +248,10 @@ fun GenericTopBar(scaffoldState: ScaffoldState, accountViewModel: AccountViewMod
|
||||
|
||||
@Composable
|
||||
private fun LoggedInUserPictureDrawer(
|
||||
account: Account,
|
||||
accountViewModel: AccountViewModel,
|
||||
onClick: () -> Unit
|
||||
) {
|
||||
val accountUserState by account.userProfile().live().metadata.observeAsState()
|
||||
val accountUserState by accountViewModel.account.userProfile().live().metadata.observeAsState()
|
||||
val accountUser = accountUserState?.user ?: return
|
||||
|
||||
IconButton(
|
||||
@@ -230,8 +259,8 @@ private fun LoggedInUserPictureDrawer(
|
||||
modifier = Modifier
|
||||
) {
|
||||
RobohashAsyncImageProxy(
|
||||
robot = accountUser.pubkeyHex,
|
||||
model = ResizeImage(accountUser.profilePicture(), 34.dp),
|
||||
robot = remember { accountUser.pubkeyHex },
|
||||
model = remember(accountUser) { ResizeImage(accountUser.profilePicture(), 34.dp) },
|
||||
contentDescription = stringResource(id = R.string.profile_image),
|
||||
modifier = Modifier
|
||||
.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 followLists = remember(followListsModel.followLists) {
|
||||
(defaultOptions + followListsModel.followLists)
|
||||
val followLists by followListsModel.followLists.collectAsState()
|
||||
|
||||
val allLists = remember(followLists) {
|
||||
(defaultOptions + followLists)
|
||||
}
|
||||
|
||||
val followNames = remember(followLists) {
|
||||
derivedStateOf {
|
||||
followLists.map { it.second }
|
||||
allLists.map { it.second }
|
||||
}
|
||||
}
|
||||
|
||||
SimpleTextSpinner(
|
||||
placeholder = followLists.firstOrNull { it.first == listName }?.second ?: "Select an Option",
|
||||
placeholder = allLists.firstOrNull { it.first == listName }?.second ?: "Select an Option",
|
||||
options = followNames.value,
|
||||
onSelect = {
|
||||
onChange(followLists.getOrNull(it)?.first ?: KIND3_FOLLOWS)
|
||||
onChange(allLists.getOrNull(it)?.first ?: KIND3_FOLLOWS)
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
class FollowListViewModel : ViewModel() {
|
||||
var followLists by mutableStateOf<List<Pair<String, String>>>(emptyList())
|
||||
var account: Account? = null
|
||||
|
||||
fun load(account: Account?) {
|
||||
this.account = account
|
||||
refresh()
|
||||
}
|
||||
@Stable
|
||||
class FollowListViewModel(val account: Account) : ViewModel() {
|
||||
private var _followLists = MutableStateFlow<ImmutableList<Pair<String, String>>>(emptyList<Pair<String, String>>().toPersistentList())
|
||||
val followLists = _followLists.asStateFlow()
|
||||
|
||||
fun refresh() {
|
||||
val scope = CoroutineScope(Job() + Dispatchers.Default)
|
||||
@@ -284,28 +311,25 @@ class FollowListViewModel : ViewModel() {
|
||||
}
|
||||
|
||||
private suspend fun refreshFollows() {
|
||||
val myAccount = account ?: return
|
||||
|
||||
val newFollowLists = LocalCache.addressables.mapNotNull {
|
||||
val event = (it.value.event as? PeopleListEvent)
|
||||
// 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())
|
||||
} else {
|
||||
null
|
||||
}
|
||||
}.sortedBy { it.second }
|
||||
}.sortedBy { it.second }.toImmutableList()
|
||||
|
||||
withContext(Dispatchers.Main) {
|
||||
if (followLists != newFollowLists) {
|
||||
followLists = newFollowLists
|
||||
}
|
||||
if (!equalImmutableLists(_followLists.value, newFollowLists)) {
|
||||
_followLists.emit(newFollowLists)
|
||||
}
|
||||
}
|
||||
|
||||
var collectorJob: Job? = null
|
||||
|
||||
init {
|
||||
refresh()
|
||||
collectorJob = viewModelScope.launch(Dispatchers.IO) {
|
||||
LocalCache.live.newEventBundles.collect { newNotes ->
|
||||
newNotes.forEach {
|
||||
@@ -321,6 +345,12 @@ class FollowListViewModel : ViewModel() {
|
||||
collectorJob?.cancel()
|
||||
super.onCleared()
|
||||
}
|
||||
|
||||
class Factory(val account: Account) : ViewModelProvider.Factory {
|
||||
override fun <FollowListViewModel : ViewModel> create(modelClass: Class<FollowListViewModel>): FollowListViewModel {
|
||||
return FollowListViewModel(account) as FollowListViewModel
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
|
||||
@@ -156,9 +156,8 @@ object HomeLatestItem : LatestItem() {
|
||||
newNotes: Set<Note>
|
||||
): Boolean {
|
||||
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
|
||||
}
|
||||
@@ -171,9 +170,8 @@ object NotificationLatestItem : LatestItem() {
|
||||
newNotes: Set<Note>
|
||||
): Boolean {
|
||||
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
|
||||
}
|
||||
@@ -185,9 +183,7 @@ object MessagesLatestItem : LatestItem() {
|
||||
cache: NotificationCache,
|
||||
newNotes: Set<Note>
|
||||
): Boolean {
|
||||
ChatroomListKnownFeedFilter.account = account
|
||||
|
||||
val newestItem = updateNewestItem(newNotes, account, ChatroomListKnownFeedFilter)
|
||||
val newestItem = updateNewestItem(newNotes, account, ChatroomListKnownFeedFilter(account))
|
||||
|
||||
val roomUserHex = (newestItem?.event as? PrivateDmEvent)?.talkingWith(account.userProfile().pubkeyHex)
|
||||
|
||||
|
||||
@@ -107,7 +107,7 @@ fun MessageSetCompose(messageSetCard: MessageSetCard, routeForLastRead: String,
|
||||
routeForLastRead = routeForLastRead,
|
||||
isBoostedNote = true,
|
||||
addMarginTop = false,
|
||||
parentBackgroundColor = backgroundColor,
|
||||
parentBackgroundColor = null,
|
||||
accountViewModel = accountViewModel,
|
||||
nav = nav
|
||||
)
|
||||
|
||||
@@ -79,9 +79,9 @@ fun MultiSetCompose(multiSetCard: MultiSetCard, routeForLastRead: String, accoun
|
||||
|
||||
LaunchedEffect(key1 = multiSetCard.createdAt()) {
|
||||
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) {
|
||||
isNew = newIsNew
|
||||
|
||||
@@ -476,7 +476,7 @@ fun ZapReaction(
|
||||
}
|
||||
|
||||
if (wantsToChangeZapAmount) {
|
||||
UpdateZapAmountDialog({ wantsToChangeZapAmount = false }, account = account)
|
||||
UpdateZapAmountDialog({ wantsToChangeZapAmount = false }, accountViewModel = accountViewModel)
|
||||
}
|
||||
|
||||
if (wantsToSetCustomZap) {
|
||||
|
||||
@@ -65,6 +65,7 @@ import androidx.compose.ui.unit.sp
|
||||
import androidx.compose.ui.window.Dialog
|
||||
import androidx.compose.ui.window.DialogProperties
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.ViewModelProvider
|
||||
import androidx.lifecycle.viewmodel.compose.viewModel
|
||||
import com.vitorpamplona.amethyst.R
|
||||
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.SaveButton
|
||||
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.getFragmentActivity
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.launch
|
||||
import androidx.compose.runtime.rememberCoroutineScope as rememberCoroutineScope
|
||||
|
||||
class UpdateZapAmountViewModel : ViewModel() {
|
||||
private var account: Account? = null
|
||||
|
||||
class UpdateZapAmountViewModel(val account: Account) : ViewModel() {
|
||||
var nextAmount by mutableStateOf(TextFieldValue(""))
|
||||
var amountSet by mutableStateOf(listOf<Long>())
|
||||
var walletConnectRelay by mutableStateOf(TextFieldValue(""))
|
||||
@@ -90,8 +90,7 @@ class UpdateZapAmountViewModel : ViewModel() {
|
||||
var walletConnectSecret by mutableStateOf(TextFieldValue(""))
|
||||
var selectedZapType by mutableStateOf(LnZapEvent.ZapType.PRIVATE)
|
||||
|
||||
fun load(account: Account) {
|
||||
this.account = account
|
||||
fun load() {
|
||||
this.amountSet = account.zapAmountChoices
|
||||
this.walletConnectPubkey = account.zapPaymentRequest?.pubKeyHex?.let { TextFieldValue(it) } ?: TextFieldValue("")
|
||||
this.walletConnectRelay = account.zapPaymentRequest?.relayUri?.let { TextFieldValue(it) } ?: TextFieldValue("")
|
||||
@@ -185,14 +184,25 @@ class UpdateZapAmountViewModel : ViewModel() {
|
||||
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)
|
||||
@Composable
|
||||
fun UpdateZapAmountDialog(onClose: () -> Unit, account: Account, nip47uri: String? = null) {
|
||||
fun UpdateZapAmountDialog(onClose: () -> Unit, nip47uri: String? = null, accountViewModel: AccountViewModel) {
|
||||
val context = LocalContext.current
|
||||
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 zapTypes = listOf(
|
||||
@@ -205,8 +215,8 @@ fun UpdateZapAmountDialog(onClose: () -> Unit, account: Account, nip47uri: Strin
|
||||
val zapOptions = zapTypes.map { it.second }
|
||||
val zapOptionExplainers = zapTypes.map { it.third }
|
||||
|
||||
LaunchedEffect(account) {
|
||||
postViewModel.load(account)
|
||||
LaunchedEffect(accountViewModel) {
|
||||
postViewModel.load()
|
||||
if (nip47uri != null) {
|
||||
try {
|
||||
postViewModel.updateNIP47(nip47uri)
|
||||
@@ -341,7 +351,7 @@ fun UpdateZapAmountDialog(onClose: () -> Unit, account: Account, nip47uri: Strin
|
||||
) {
|
||||
TextSpinner(
|
||||
label = stringResource(id = R.string.zap_type_explainer),
|
||||
placeholder = zapTypes.filter { it.first == account.defaultZapType }
|
||||
placeholder = zapTypes.filter { it.first == accountViewModel.defaultZapType() }
|
||||
.first().second,
|
||||
options = zapOptions,
|
||||
explainers = zapOptionExplainers,
|
||||
|
||||
@@ -14,10 +14,9 @@ import androidx.compose.material.icons.filled.Bolt
|
||||
import androidx.compose.material.icons.filled.ExpandMore
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.Stable
|
||||
import androidx.compose.runtime.collectAsState
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment.Companion.CenterVertically
|
||||
import androidx.compose.ui.Modifier
|
||||
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.sp
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.ViewModelProvider
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import com.patrykandpatrick.vico.core.chart.composed.ComposedChartEntryModel
|
||||
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.entryOf
|
||||
import com.vitorpamplona.amethyst.R
|
||||
import com.vitorpamplona.amethyst.model.Account
|
||||
import com.vitorpamplona.amethyst.model.HexKey
|
||||
import com.vitorpamplona.amethyst.model.LocalCache
|
||||
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 kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.asStateFlow
|
||||
import kotlinx.coroutines.launch
|
||||
import java.math.BigDecimal
|
||||
import java.time.Instant
|
||||
@@ -80,47 +83,50 @@ fun UserReactionsRow(
|
||||
}
|
||||
|
||||
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)) {
|
||||
UserBoostReaction(model.boosts[model.today()])
|
||||
val boosts by model.boosts.collectAsState()
|
||||
UserBoostReaction(boosts[model.today()])
|
||||
}
|
||||
|
||||
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)) {
|
||||
UserZapReaction(model.zaps[model.today()])
|
||||
val zaps by model.zaps.collectAsState()
|
||||
UserZapReaction(zaps[model.today()])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Stable
|
||||
class UserReactionsViewModel : ViewModel() {
|
||||
var user: User? = null
|
||||
class UserReactionsViewModel(val account: Account) : ViewModel() {
|
||||
val user: User = account.userProfile()
|
||||
|
||||
var reactions by mutableStateOf<Map<String, Int>>(emptyMap())
|
||||
var boosts by mutableStateOf<Map<String, Int>>(emptyMap())
|
||||
var zaps by mutableStateOf<Map<String, BigDecimal>>(emptyMap())
|
||||
var replies by mutableStateOf<Map<String, Int>>(emptyMap())
|
||||
private var _reactions = MutableStateFlow<Map<String, Int>>(emptyMap())
|
||||
private var _boosts = MutableStateFlow<Map<String, Int>>(emptyMap())
|
||||
private var _zaps = MutableStateFlow<Map<String, BigDecimal>>(emptyMap())
|
||||
private var _replies = MutableStateFlow<Map<String, Int>>(emptyMap())
|
||||
|
||||
var chartModel by mutableStateOf<ComposedChartEntryModel<ChartEntryModel>?>(null)
|
||||
var axisLabels by mutableStateOf<List<String>>(emptyList())
|
||||
private var _chartModel = MutableStateFlow<ComposedChartEntryModel<ChartEntryModel>?>(null)
|
||||
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 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 {
|
||||
return sdf.format(
|
||||
Instant.ofEpochSecond(createAt)
|
||||
@@ -131,8 +137,8 @@ class UserReactionsViewModel : ViewModel() {
|
||||
|
||||
fun today() = sdf.format(LocalDateTime.now())
|
||||
|
||||
fun initializeSuspend() {
|
||||
val currentUser = user?.pubkeyHex ?: return
|
||||
private suspend fun initializeSuspend() {
|
||||
val currentUser = user.pubkeyHex
|
||||
|
||||
val reactions = mutableMapOf<String, Int>()
|
||||
val boosts = mutableMapOf<String, Int>()
|
||||
@@ -172,21 +178,21 @@ class UserReactionsViewModel : ViewModel() {
|
||||
}
|
||||
|
||||
this.takenIntoAccount = takenIntoAccount
|
||||
this.reactions = reactions
|
||||
this.replies = replies
|
||||
this.zaps = zaps
|
||||
this.boosts = boosts
|
||||
this._reactions.emit(reactions)
|
||||
this._replies.emit(replies)
|
||||
this._zaps.emit(zaps)
|
||||
this._boosts.emit(boosts)
|
||||
|
||||
refreshChartModel()
|
||||
}
|
||||
|
||||
fun addToStatsSuspend(newNotes: Set<Note>) {
|
||||
val currentUser = user?.pubkeyHex ?: return
|
||||
suspend fun addToStatsSuspend(newNotes: Set<Note>) {
|
||||
val currentUser = user.pubkeyHex
|
||||
|
||||
val reactions = this.reactions.toMutableMap()
|
||||
val boosts = this.boosts.toMutableMap()
|
||||
val zaps = this.zaps.toMutableMap()
|
||||
val replies = this.replies.toMutableMap()
|
||||
val reactions = this._reactions.value.toMutableMap()
|
||||
val boosts = this._boosts.value.toMutableMap()
|
||||
val zaps = this._zaps.value.toMutableMap()
|
||||
val replies = this._replies.value.toMutableMap()
|
||||
val takenIntoAccount = this.takenIntoAccount.toMutableSet()
|
||||
var hasNewElements = false
|
||||
|
||||
@@ -227,16 +233,16 @@ class UserReactionsViewModel : ViewModel() {
|
||||
|
||||
if (hasNewElements) {
|
||||
this.takenIntoAccount = takenIntoAccount
|
||||
this.reactions = reactions
|
||||
this.replies = replies
|
||||
this.zaps = zaps
|
||||
this.boosts = boosts
|
||||
this._reactions.emit(reactions)
|
||||
this._replies.emit(replies)
|
||||
this._zaps.emit(zaps)
|
||||
this._boosts.emit(boosts)
|
||||
|
||||
refreshChartModel()
|
||||
}
|
||||
}
|
||||
|
||||
private fun refreshChartModel() {
|
||||
private suspend fun refreshChartModel() {
|
||||
val day = 24 * 60 * 60L
|
||||
val now = LocalDateTime.now()
|
||||
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 listOfCountCurves = listOf(
|
||||
dataAxisLabels.mapIndexed { index, dateStr -> entryOf(index, replies[dateStr]?.toFloat() ?: 0f) },
|
||||
dataAxisLabels.mapIndexed { index, dateStr -> entryOf(index, boosts[dateStr]?.toFloat() ?: 0f) },
|
||||
dataAxisLabels.mapIndexed { index, dateStr -> entryOf(index, reactions[dateStr]?.toFloat() ?: 0f) }
|
||||
dataAxisLabels.mapIndexed { index, dateStr -> entryOf(index, _replies.value[dateStr]?.toFloat() ?: 0f) },
|
||||
dataAxisLabels.mapIndexed { index, dateStr -> entryOf(index, _boosts.value[dateStr]?.toFloat() ?: 0f) },
|
||||
dataAxisLabels.mapIndexed { index, dateStr -> entryOf(index, _reactions.value[dateStr]?.toFloat() ?: 0f) }
|
||||
)
|
||||
|
||||
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 chartEntryModelProducer2 = ChartEntryModelProducer(listOfValueCurves).getModel()
|
||||
|
||||
this.axisLabels = listOf(6, 5, 4, 3, 2, 1, 0).map { displayAxisFormatter.format(now.minusSeconds(day * it)) }
|
||||
this.chartModel = chartEntryModelProducer1.plus(chartEntryModelProducer2)
|
||||
this._axisLabels.emit(listOf(6, 5, 4, 3, 2, 1, 0).map { displayAxisFormatter.format(now.minusSeconds(day * it)) })
|
||||
this._chartModel.emit(chartEntryModelProducer1.plus(chartEntryModelProducer2))
|
||||
}
|
||||
|
||||
var collectorJob: Job? = null
|
||||
|
||||
init {
|
||||
collectorJob = viewModelScope.launch(Dispatchers.IO) {
|
||||
LocalCache.live.newEventBundles.collect { newNotes ->
|
||||
addToStatsSuspend(newNotes)
|
||||
_reactions.value = emptyMap()
|
||||
_boosts.value = emptyMap()
|
||||
_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() {
|
||||
collectorJob?.cancel()
|
||||
super.onCleared()
|
||||
}
|
||||
|
||||
class Factory(val account: Account) : ViewModelProvider.Factory {
|
||||
override fun <UserReactionsViewModel : ViewModel> create(modelClass: Class<UserReactionsViewModel>): UserReactionsViewModel {
|
||||
return UserReactionsViewModel(account) as UserReactionsViewModel
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
|
||||
@@ -61,16 +61,22 @@ class ZapUserSetCard(val user: User, val zapEvents: ImmutableMap<Note, Note>) :
|
||||
|
||||
@Immutable
|
||||
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,
|
||||
likeEvents.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 {
|
||||
return createdAt
|
||||
return maxCreatedAt
|
||||
}
|
||||
override fun id() = note.idHex + "X" + createdAt
|
||||
override fun id() = note.idHex + "X" + maxCreatedAt + "X" + minCreatedAt
|
||||
}
|
||||
|
||||
@Immutable
|
||||
|
||||
@@ -39,72 +39,42 @@ import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
||||
|
||||
@OptIn(ExperimentalMaterialApi::class)
|
||||
@Composable
|
||||
fun CardFeedView(
|
||||
fun RefresheableCardView(
|
||||
viewModel: CardFeedViewModel,
|
||||
accountViewModel: AccountViewModel,
|
||||
nav: (String) -> Unit,
|
||||
routeForLastRead: String,
|
||||
scrollStateKey: String? = null,
|
||||
scrollToTop: Boolean = false
|
||||
enablePullRefresh: Boolean = true
|
||||
) {
|
||||
val feedState by viewModel.feedContent.collectAsState()
|
||||
|
||||
var refreshing by remember { mutableStateOf(false) }
|
||||
val refresh = { refreshing = true; viewModel.invalidateData(); refreshing = false }
|
||||
val pullRefreshState = rememberPullRefreshState(refreshing, onRefresh = refresh)
|
||||
|
||||
Box(Modifier.fillMaxSize().pullRefresh(pullRefreshState)) {
|
||||
Column(modifier = Modifier.fillMaxSize()) {
|
||||
Crossfade(
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
targetState = feedState,
|
||||
animationSpec = tween(durationMillis = 100)
|
||||
) { state ->
|
||||
val modifier = if (enablePullRefresh) {
|
||||
Modifier.pullRefresh(pullRefreshState)
|
||||
} else {
|
||||
Modifier
|
||||
}
|
||||
|
||||
when (state) {
|
||||
is CardFeedState.Empty -> {
|
||||
FeedEmpty {
|
||||
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()
|
||||
}
|
||||
}
|
||||
}
|
||||
Box(modifier) {
|
||||
Column {
|
||||
SaveableCardFeedState(viewModel, accountViewModel, nav, routeForLastRead, scrollStateKey)
|
||||
}
|
||||
|
||||
PullRefreshIndicator(refreshing, pullRefreshState, Modifier.align(Alignment.TopCenter))
|
||||
if (enablePullRefresh) {
|
||||
PullRefreshIndicator(refreshing, pullRefreshState, Modifier.align(Alignment.TopCenter))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun FeedLoaded(
|
||||
state: CardFeedState.Loaded,
|
||||
private fun SaveableCardFeedState(
|
||||
viewModel: CardFeedViewModel,
|
||||
accountViewModel: AccountViewModel,
|
||||
nav: (String) -> Unit,
|
||||
routeForLastRead: String,
|
||||
scrollStateKey: String?,
|
||||
scrollToTop: Boolean = false
|
||||
scrollStateKey: String? = null
|
||||
) {
|
||||
val listState = if (scrollStateKey != null) {
|
||||
rememberForeverLazyListState(scrollStateKey)
|
||||
@@ -112,24 +82,76 @@ private fun FeedLoaded(
|
||||
rememberLazyListState()
|
||||
}
|
||||
|
||||
if (scrollToTop) {
|
||||
LaunchedEffect(Unit) {
|
||||
if (listState.firstVisibleItemIndex > 0 || listState.firstVisibleItemScrollOffset > 0) {
|
||||
listState.scrollToItem(index = 0)
|
||||
}
|
||||
}
|
||||
}
|
||||
WatchScrollToTop(viewModel, listState)
|
||||
|
||||
LazyNotificationList(listState, state, routeForLastRead, accountViewModel, nav)
|
||||
RenderCardFeed(viewModel, accountViewModel, listState, nav, routeForLastRead)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun LazyNotificationList(
|
||||
listState: LazyListState,
|
||||
state: CardFeedState.Loaded,
|
||||
routeForLastRead: String,
|
||||
private fun WatchScrollToTop(
|
||||
viewModel: CardFeedViewModel,
|
||||
listState: LazyListState
|
||||
) {
|
||||
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,
|
||||
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(
|
||||
modifier = remember { Modifier.fillMaxSize() },
|
||||
|
||||
@@ -4,6 +4,7 @@ import android.util.Log
|
||||
import androidx.compose.runtime.Stable
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.ViewModelProvider
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import com.vitorpamplona.amethyst.model.Account
|
||||
import com.vitorpamplona.amethyst.model.LocalCache
|
||||
@@ -35,12 +36,35 @@ import kotlin.time.ExperimentalTime
|
||||
import kotlin.time.measureTimedValue
|
||||
|
||||
@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() {
|
||||
private val _feedContent = MutableStateFlow<CardFeedState>(CardFeedState.Loading)
|
||||
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 lastNotes: Set<Note>? = null
|
||||
|
||||
@@ -150,7 +174,10 @@ open class CardFeedViewModel(val localFilter: FeedFilter<Note>) : ViewModel() {
|
||||
val reactionsInCard = reactionsPerEvent[baseNote] ?: emptyList()
|
||||
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 ->
|
||||
MultiSetCard(
|
||||
baseNote,
|
||||
@@ -234,19 +261,32 @@ open class CardFeedViewModel(val localFilter: FeedFilter<Note>) : ViewModel() {
|
||||
}
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalTime::class)
|
||||
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 bundler = BundledUpdate(1000, Dispatchers.IO)
|
||||
private val bundlerInsert = BundledInsert<Set<Note>>(1000, Dispatchers.IO)
|
||||
|
||||
@OptIn(ExperimentalTime::class)
|
||||
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)
|
||||
|
||||
@@ -9,6 +9,7 @@ import androidx.compose.foundation.layout.PaddingValues
|
||||
import androidx.compose.foundation.layout.fillMaxHeight
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.LazyListState
|
||||
import androidx.compose.foundation.lazy.itemsIndexed
|
||||
import androidx.compose.foundation.lazy.rememberLazyListState
|
||||
import androidx.compose.material.Button
|
||||
@@ -35,17 +36,14 @@ import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
||||
|
||||
@OptIn(ExperimentalMaterialApi::class)
|
||||
@Composable
|
||||
fun FeedView(
|
||||
fun RefresheableView(
|
||||
viewModel: FeedViewModel,
|
||||
accountViewModel: AccountViewModel,
|
||||
nav: (String) -> Unit,
|
||||
routeForLastRead: String?,
|
||||
scrollStateKey: String? = null,
|
||||
scrollToTop: Boolean = false,
|
||||
enablePullRefresh: Boolean = true
|
||||
) {
|
||||
val feedState by viewModel.feedContent.collectAsState()
|
||||
|
||||
var refreshing by remember { mutableStateOf(false) }
|
||||
val refresh = { refreshing = true; viewModel.invalidateData(); refreshing = false }
|
||||
val pullRefreshState = rememberPullRefreshState(refreshing, onRefresh = refresh)
|
||||
@@ -58,42 +56,7 @@ fun FeedView(
|
||||
|
||||
Box(modifier) {
|
||||
Column {
|
||||
Crossfade(
|
||||
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()
|
||||
}
|
||||
}
|
||||
}
|
||||
SaveableFeedState(viewModel, accountViewModel, nav, routeForLastRead, scrollStateKey)
|
||||
}
|
||||
|
||||
if (enablePullRefresh) {
|
||||
@@ -103,13 +66,12 @@ fun FeedView(
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun FeedLoaded(
|
||||
state: FeedState.Loaded,
|
||||
routeForLastRead: String?,
|
||||
private fun SaveableFeedState(
|
||||
viewModel: FeedViewModel,
|
||||
accountViewModel: AccountViewModel,
|
||||
nav: (String) -> Unit,
|
||||
scrollStateKey: String?,
|
||||
scrollToTop: Boolean = false
|
||||
routeForLastRead: String?,
|
||||
scrollStateKey: String? = null
|
||||
) {
|
||||
val listState = if (scrollStateKey != null) {
|
||||
rememberForeverLazyListState(scrollStateKey)
|
||||
@@ -117,14 +79,78 @@ private fun FeedLoaded(
|
||||
rememberLazyListState()
|
||||
}
|
||||
|
||||
if (scrollToTop) {
|
||||
LaunchedEffect(Unit) {
|
||||
if (listState.firstVisibleItemIndex > 0 || listState.firstVisibleItemScrollOffset > 0) {
|
||||
listState.scrollToItem(index = 0)
|
||||
WatchScrollToTop(viewModel, listState)
|
||||
|
||||
RenderFeed(viewModel, accountViewModel, listState, nav, routeForLastRead)
|
||||
}
|
||||
|
||||
@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 {
|
||||
Modifier
|
||||
}
|
||||
|
||||
@@ -1,8 +1,11 @@
|
||||
package com.vitorpamplona.amethyst.ui.screen
|
||||
|
||||
import androidx.compose.runtime.Stable
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.ViewModelProvider
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import com.vitorpamplona.amethyst.model.Account
|
||||
import com.vitorpamplona.amethyst.model.LocalCache
|
||||
import com.vitorpamplona.amethyst.model.Note
|
||||
import com.vitorpamplona.amethyst.ui.components.BundledInsert
|
||||
@@ -37,26 +40,90 @@ import kotlinx.coroutines.launch
|
||||
|
||||
class NostrChannelFeedViewModel : FeedViewModel(ChannelFeedFilter)
|
||||
class NostrChatRoomFeedViewModel : FeedViewModel(ChatroomFeedFilter)
|
||||
class NostrGlobalFeedViewModel : FeedViewModel(GlobalFeedFilter)
|
||||
class NostrVideoFeedViewModel : FeedViewModel(VideoFeedFilter)
|
||||
class NostrThreadFeedViewModel : FeedViewModel(ThreadFeedFilter)
|
||||
class NostrGlobalFeedViewModel(val account: Account) : FeedViewModel(GlobalFeedFilter(account)) {
|
||||
class Factory(val account: Account) : ViewModelProvider.Factory {
|
||||
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 NostrUserProfileNewThreadsFeedViewModel : FeedViewModel(UserProfileNewThreadFeedFilter)
|
||||
class NostrUserProfileConversationsFeedViewModel : FeedViewModel(UserProfileConversationsFeedFilter)
|
||||
class NostrUserProfileReportFeedViewModel : FeedViewModel(UserProfileReportsFeedFilter)
|
||||
class NostrUserProfileBookmarksFeedViewModel : FeedViewModel(UserProfileBookmarksFeedFilter)
|
||||
class NostrChatroomListKnownFeedViewModel : FeedViewModel(ChatroomListKnownFeedFilter)
|
||||
class NostrChatroomListNewFeedViewModel : FeedViewModel(ChatroomListNewFeedFilter)
|
||||
class NostrHomeFeedViewModel : FeedViewModel(HomeNewThreadFeedFilter)
|
||||
class NostrHomeRepliesFeedViewModel : FeedViewModel(HomeConversationsFeedFilter)
|
||||
class NostrChatroomListKnownFeedViewModel(val account: Account) : FeedViewModel(ChatroomListKnownFeedFilter(account)) {
|
||||
class Factory(val account: Account) : ViewModelProvider.Factory {
|
||||
override fun <NostrChatroomListKnownFeedViewModel : ViewModel> create(modelClass: Class<NostrChatroomListKnownFeedViewModel>): NostrChatroomListKnownFeedViewModel {
|
||||
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 NostrBookmarkPrivateFeedViewModel : FeedViewModel(BookmarkPrivateFeedFilter)
|
||||
|
||||
@Stable
|
||||
abstract class FeedViewModel(val localFilter: FeedFilter<Note>) : ViewModel() {
|
||||
private val _feedContent = MutableStateFlow<FeedState>(FeedState.Loading)
|
||||
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> {
|
||||
return localFilter.loadTop().toImmutableList()
|
||||
}
|
||||
@@ -119,15 +186,24 @@ abstract class FeedViewModel(val localFilter: FeedFilter<Note>) : ViewModel() {
|
||||
}
|
||||
}
|
||||
|
||||
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 bundler = BundledUpdate(250, Dispatchers.IO)
|
||||
private val bundlerInsert = BundledInsert<Set<Note>>(250, Dispatchers.IO)
|
||||
|
||||
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>) {
|
||||
@@ -136,7 +212,7 @@ abstract class FeedViewModel(val localFilter: FeedFilter<Note>) : ViewModel() {
|
||||
}
|
||||
}
|
||||
|
||||
var collectorJob: Job? = null
|
||||
private var collectorJob: Job? = null
|
||||
|
||||
init {
|
||||
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) {
|
||||
// adds the time to perform the refresh into this delay
|
||||
// holding off new updates in case of heavy refresh routines.
|
||||
refreshSuspended()
|
||||
}
|
||||
private val bundler = BundledUpdate(250, Dispatchers.IO)
|
||||
|
||||
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
|
||||
|
||||
@@ -13,7 +13,6 @@ import androidx.compose.material.pullrefresh.rememberPullRefreshState
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.collectAsState
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.livedata.observeAsState
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.setValue
|
||||
@@ -83,14 +82,14 @@ class RelayFeedViewModel : ViewModel() {
|
||||
currentUser = null
|
||||
}
|
||||
|
||||
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 bundler = BundledUpdate(250, Dispatchers.IO)
|
||||
|
||||
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,
|
||||
enablePullRefresh: Boolean = true
|
||||
) {
|
||||
val accountState by accountViewModel.accountLiveData.observeAsState()
|
||||
val account = accountState?.account ?: return
|
||||
|
||||
val feedState by viewModel.feedContent.collectAsState()
|
||||
|
||||
var wantsToAddRelay by remember {
|
||||
@@ -111,7 +107,7 @@ fun RelayFeedView(
|
||||
}
|
||||
|
||||
if (wantsToAddRelay.isNotEmpty()) {
|
||||
NewRelayListView({ wantsToAddRelay = "" }, account, wantsToAddRelay)
|
||||
NewRelayListView({ wantsToAddRelay = "" }, accountViewModel, wantsToAddRelay)
|
||||
}
|
||||
|
||||
var refreshing by remember { mutableStateOf(false) }
|
||||
|
||||
@@ -21,6 +21,10 @@ object ScrollStateKeys {
|
||||
val HOME_REPLIES = Route.Home.base + "FollowsReplies"
|
||||
}
|
||||
|
||||
object PagerStateKeys {
|
||||
const val HOME_SCREEN = "Home"
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun rememberForeverLazyListState(
|
||||
key: String,
|
||||
@@ -62,7 +66,7 @@ fun rememberForeverPagerState(
|
||||
savedOffset
|
||||
)
|
||||
}
|
||||
DisposableEffect(Unit) {
|
||||
DisposableEffect(scrollState) {
|
||||
onDispose {
|
||||
val lastIndex = scrollState.currentPage
|
||||
val lastOffset = scrollState.currentPageOffsetFraction
|
||||
|
||||
@@ -62,14 +62,14 @@ open class UserFeedViewModel(val dataSource: FeedFilter<User>) : ViewModel() {
|
||||
}
|
||||
}
|
||||
|
||||
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 bundler = BundledUpdate(250, Dispatchers.IO)
|
||||
|
||||
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
|
||||
|
||||
@@ -3,7 +3,7 @@ package com.vitorpamplona.amethyst.ui.screen.loggedIn
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.net.Uri
|
||||
import androidx.compose.runtime.Immutable
|
||||
import androidx.compose.runtime.Stable
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.lifecycle.LiveData
|
||||
import androidx.lifecycle.ViewModel
|
||||
@@ -26,8 +26,8 @@ import kotlinx.coroutines.launch
|
||||
import java.math.BigDecimal
|
||||
import java.util.Locale
|
||||
|
||||
@Immutable
|
||||
class AccountViewModel(private val account: Account) : ViewModel() {
|
||||
@Stable
|
||||
class AccountViewModel(val account: Account) : ViewModel() {
|
||||
val accountLiveData: LiveData<AccountState> = account.live.map { it }
|
||||
val accountLanguagesLiveData: LiveData<AccountState> = account.liveLanguages.map { it }
|
||||
|
||||
@@ -249,6 +249,10 @@ class AccountViewModel(private val account: Account) : ViewModel() {
|
||||
account.updateShowSensitiveContent(null)
|
||||
}
|
||||
|
||||
fun defaultZapType(): LnZapEvent.ZapType {
|
||||
return account.defaultZapType
|
||||
}
|
||||
|
||||
class Factory(val account: Account) : ViewModelProvider.Factory {
|
||||
override fun <AccountViewModel : ViewModel> create(modelClass: Class<AccountViewModel>): 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.ui.dal.BookmarkPrivateFeedFilter
|
||||
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.NostrBookmarkPublicFeedViewModel
|
||||
import com.vitorpamplona.amethyst.ui.screen.RefresheableView
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
@OptIn(ExperimentalFoundationApi::class)
|
||||
@@ -73,8 +73,8 @@ fun BookmarkListScreen(accountViewModel: AccountViewModel, nav: (String) -> Unit
|
||||
}
|
||||
HorizontalPager(pageCount = 2, state = pagerState) { page ->
|
||||
when (page) {
|
||||
0 -> FeedView(privateFeedViewModel, accountViewModel, nav, null)
|
||||
1 -> FeedView(publicFeedViewModel, accountViewModel, nav, null)
|
||||
0 -> RefresheableView(privateFeedViewModel, 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.runtime.Composable
|
||||
import androidx.compose.runtime.DisposableEffect
|
||||
import androidx.compose.runtime.Immutable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.MutableState
|
||||
import androidx.compose.runtime.derivedStateOf
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.livedata.observeAsState
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.rememberCoroutineScope
|
||||
@@ -41,8 +41,6 @@ import androidx.lifecycle.LifecycleEventObserver
|
||||
import androidx.lifecycle.viewmodel.compose.viewModel
|
||||
import com.vitorpamplona.amethyst.R
|
||||
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.FeedViewModel
|
||||
import com.vitorpamplona.amethyst.ui.screen.NostrChatroomListKnownFeedViewModel
|
||||
@@ -51,7 +49,12 @@ import kotlinx.coroutines.launch
|
||||
|
||||
@OptIn(ExperimentalFoundationApi::class)
|
||||
@Composable
|
||||
fun ChatroomListScreen(accountViewModel: AccountViewModel, nav: (String) -> Unit) {
|
||||
fun ChatroomListScreen(
|
||||
knownFeedViewModel: NostrChatroomListKnownFeedViewModel,
|
||||
newFeedViewModel: NostrChatroomListNewFeedViewModel,
|
||||
accountViewModel: AccountViewModel,
|
||||
nav: (String) -> Unit
|
||||
) {
|
||||
val pagerState = rememberPagerState()
|
||||
val coroutineScope = rememberCoroutineScope()
|
||||
|
||||
@@ -59,30 +62,15 @@ fun ChatroomListScreen(accountViewModel: AccountViewModel, nav: (String) -> Unit
|
||||
val markKnownAsRead = remember { mutableStateOf(false) }
|
||||
val markNewAsRead = remember { mutableStateOf(false) }
|
||||
|
||||
val accountState by accountViewModel.accountLiveData.observeAsState()
|
||||
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()
|
||||
}
|
||||
WatchAccountForListScreen(knownFeedViewModel, newFeedViewModel, accountViewModel)
|
||||
|
||||
val lifeCycleOwner = LocalLifecycleOwner.current
|
||||
DisposableEffect(accountViewModel) {
|
||||
val observer = LifecycleEventObserver { _, event ->
|
||||
if (event == Lifecycle.Event.ON_RESUME) {
|
||||
NostrChatroomListDataSource.account = account
|
||||
NostrChatroomListDataSource.start()
|
||||
knownFeedViewModel.invalidateData()
|
||||
newFeedViewModel.invalidateData()
|
||||
knownFeedViewModel.invalidateData(true)
|
||||
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 {
|
||||
listOf(
|
||||
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>)
|
||||
|
||||
@Composable
|
||||
|
||||
@@ -25,8 +25,8 @@ import androidx.lifecycle.viewmodel.compose.viewModel
|
||||
import com.vitorpamplona.amethyst.model.Account
|
||||
import com.vitorpamplona.amethyst.service.NostrHashtagDataSource
|
||||
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.RefresheableView
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
@@ -75,7 +75,7 @@ fun HashtagScreen(tag: String?, accountViewModel: AccountViewModel, nav: (String
|
||||
modifier = Modifier.padding(vertical = 0.dp)
|
||||
) {
|
||||
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.runtime.Composable
|
||||
import androidx.compose.runtime.DisposableEffect
|
||||
import androidx.compose.runtime.Immutable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.livedata.observeAsState
|
||||
@@ -27,15 +28,17 @@ import androidx.lifecycle.Lifecycle
|
||||
import androidx.lifecycle.LifecycleEventObserver
|
||||
import com.vitorpamplona.amethyst.R
|
||||
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.note.UpdateZapAmountDialog
|
||||
import com.vitorpamplona.amethyst.ui.screen.FeedView
|
||||
import com.vitorpamplona.amethyst.ui.screen.FeedViewModel
|
||||
import com.vitorpamplona.amethyst.ui.screen.NostrHomeFeedViewModel
|
||||
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.rememberForeverPagerState
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
import kotlinx.collections.immutable.toImmutableList
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
@@ -46,34 +49,22 @@ fun HomeScreen(
|
||||
repliesFeedViewModel: NostrHomeRepliesFeedViewModel,
|
||||
accountViewModel: AccountViewModel,
|
||||
nav: (String) -> Unit,
|
||||
pagerState: PagerState,
|
||||
scrollToTop: Boolean = false,
|
||||
nip47: String? = null
|
||||
) {
|
||||
val coroutineScope = rememberCoroutineScope()
|
||||
var wantsToAddNip47 by remember { mutableStateOf(nip47) }
|
||||
|
||||
val accountState by accountViewModel.accountLiveData.observeAsState()
|
||||
val account = accountState?.account ?: return
|
||||
val pagerState = rememberForeverPagerState(key = PagerStateKeys.HOME_SCREEN)
|
||||
|
||||
LaunchedEffect(accountViewModel, account.defaultHomeFollowList) {
|
||||
HomeNewThreadFeedFilter.account = account
|
||||
HomeConversationsFeedFilter.account = account
|
||||
NostrHomeDataSource.invalidateFilters()
|
||||
homeFeedViewModel.invalidateData(true)
|
||||
repliesFeedViewModel.invalidateData(true)
|
||||
}
|
||||
WatchAccountForHomeScreen(homeFeedViewModel, repliesFeedViewModel, accountViewModel)
|
||||
|
||||
if (wantsToAddNip47 != null) {
|
||||
UpdateZapAmountDialog({ wantsToAddNip47 = null }, account = account, wantsToAddNip47)
|
||||
UpdateZapAmountDialog({ wantsToAddNip47 = null }, wantsToAddNip47, accountViewModel)
|
||||
}
|
||||
|
||||
val lifeCycleOwner = LocalLifecycleOwner.current
|
||||
DisposableEffect(accountViewModel) {
|
||||
val observer = LifecycleEventObserver { _, event ->
|
||||
if (event == Lifecycle.Event.ON_RESUME) {
|
||||
HomeNewThreadFeedFilter.account = account
|
||||
HomeConversationsFeedFilter.account = account
|
||||
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) {
|
||||
mutableStateOf(
|
||||
listOf(
|
||||
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)
|
||||
)
|
||||
).toImmutableList()
|
||||
)
|
||||
}
|
||||
|
||||
@@ -108,35 +88,67 @@ fun HomeScreen(
|
||||
Column(
|
||||
modifier = Modifier.padding(vertical = 0.dp)
|
||||
) {
|
||||
TabRow(
|
||||
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
|
||||
)
|
||||
}
|
||||
HomePages(pagerState, tabs, accountViewModel, nav)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@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)
|
||||
|
||||
@@ -20,7 +20,6 @@ import androidx.compose.material.rememberScaffoldState
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.collectAsState
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.livedata.observeAsState
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.rememberCoroutineScope
|
||||
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.Route
|
||||
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.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
|
||||
|
||||
@OptIn(ExperimentalMaterialApi::class)
|
||||
@@ -61,11 +68,51 @@ fun MainScreen(accountViewModel: AccountViewModel, accountStateViewModel: Accoun
|
||||
}
|
||||
}
|
||||
|
||||
val accountState by accountViewModel.accountLiveData.observeAsState()
|
||||
val account = remember(accountState) { accountState?.account }
|
||||
val followLists: FollowListViewModel = viewModel(
|
||||
key = accountViewModel.userProfile().pubkeyHex + "FollowListViewModel",
|
||||
factory = FollowListViewModel.Factory(accountViewModel.account)
|
||||
)
|
||||
|
||||
val followLists: FollowListViewModel = viewModel()
|
||||
followLists.load(account)
|
||||
// Avoids creating ViewModels for performance reasons (up to 1 second delays)
|
||||
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(
|
||||
sheetState = sheetState,
|
||||
@@ -95,7 +142,19 @@ fun MainScreen(accountViewModel: AccountViewModel, accountStateViewModel: Accoun
|
||||
scaffoldState = scaffoldState
|
||||
) {
|
||||
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.DisposableEffect
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.collectAsState
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.livedata.observeAsState
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.rememberCoroutineScope
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Brush
|
||||
@@ -23,7 +23,6 @@ import androidx.compose.ui.platform.LocalLifecycleOwner
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.lifecycle.Lifecycle
|
||||
import androidx.lifecycle.LifecycleEventObserver
|
||||
import androidx.lifecycle.viewmodel.compose.viewModel
|
||||
import com.patrykandpatrick.vico.compose.axis.horizontal.bottomAxis
|
||||
import com.patrykandpatrick.vico.compose.axis.vertical.endAxis
|
||||
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.values.ChartValues
|
||||
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.ui.dal.NotificationFeedFilter
|
||||
import com.vitorpamplona.amethyst.ui.navigation.Route
|
||||
import com.vitorpamplona.amethyst.ui.note.OneGiga
|
||||
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.UserReactionsViewModel
|
||||
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.RefresheableCardView
|
||||
import com.vitorpamplona.amethyst.ui.screen.ScrollStateKeys
|
||||
import com.vitorpamplona.amethyst.ui.theme.BitcoinOrange
|
||||
import com.vitorpamplona.amethyst.ui.theme.RoyalBlue
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import java.math.BigDecimal
|
||||
import java.math.RoundingMode
|
||||
import kotlin.math.roundToInt
|
||||
@@ -64,24 +59,15 @@ fun NotificationScreen(
|
||||
notifFeedViewModel: NotificationViewModel,
|
||||
userReactionsStatsModel: UserReactionsViewModel,
|
||||
accountViewModel: AccountViewModel,
|
||||
nav: (String) -> Unit,
|
||||
scrollToTop: Boolean = false
|
||||
nav: (String) -> Unit
|
||||
) {
|
||||
val accountState by accountViewModel.accountLiveData.observeAsState()
|
||||
val account = remember(accountState) { accountState?.account } ?: return
|
||||
|
||||
LaunchedEffect(accountViewModel, account.defaultNotificationFollowList) {
|
||||
NostrAccountDataSource.invalidateFilters()
|
||||
NotificationFeedFilter.account = account
|
||||
notifFeedViewModel.invalidateData(true)
|
||||
}
|
||||
WatchAccountForNotifications(notifFeedViewModel, accountViewModel)
|
||||
|
||||
val lifeCycleOwner = LocalLifecycleOwner.current
|
||||
DisposableEffect(accountViewModel) {
|
||||
val observer = LifecycleEventObserver { _, event ->
|
||||
if (event == Lifecycle.Event.ON_RESUME) {
|
||||
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 = Modifier.padding(vertical = 0.dp)
|
||||
@@ -108,18 +84,35 @@ fun NotificationScreen(
|
||||
SummaryBar(
|
||||
model = userReactionsStatsModel
|
||||
)
|
||||
CardFeedView(
|
||||
RefresheableCardView(
|
||||
viewModel = notifFeedViewModel,
|
||||
accountViewModel = accountViewModel,
|
||||
nav = nav,
|
||||
routeForLastRead = Route.Notification.base,
|
||||
scrollStateKey = ScrollStateKeys.NOTIFICATION_SCREEN,
|
||||
scrollToTop = scrollToTop
|
||||
scrollStateKey = ScrollStateKeys.NOTIFICATION_SCREEN
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@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
|
||||
fun SummaryBar(model: UserReactionsViewModel) {
|
||||
var showChart by remember {
|
||||
@@ -167,7 +160,9 @@ fun SummaryBar(model: UserReactionsViewModel) {
|
||||
)
|
||||
|
||||
if (showChart) {
|
||||
model.chartModel?.let {
|
||||
val axisModel by model.axisLabels.collectAsState()
|
||||
val chartModel by model.chartModel.collectAsState()
|
||||
chartModel?.let {
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.padding(vertical = 10.dp, horizontal = 20.dp)
|
||||
@@ -186,7 +181,7 @@ fun SummaryBar(model: UserReactionsViewModel) {
|
||||
valueFormatter = AmountAxisValueFormatter()
|
||||
),
|
||||
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.note.UserPicture
|
||||
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.NostrUserProfileBookmarksFeedViewModel
|
||||
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.NostrUserProfileReportFeedViewModel
|
||||
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.RelayFeedViewModel
|
||||
import com.vitorpamplona.amethyst.ui.screen.UserFeedView
|
||||
@@ -115,7 +115,7 @@ fun ProfileScreen(userId: String?, accountViewModel: AccountViewModel, nav: (Str
|
||||
|
||||
userBase?.let {
|
||||
ProfileScreen(
|
||||
user = it,
|
||||
baseUser = it,
|
||||
accountViewModel = accountViewModel,
|
||||
nav = nav
|
||||
)
|
||||
@@ -124,19 +124,16 @@ fun ProfileScreen(userId: String?, accountViewModel: AccountViewModel, nav: (Str
|
||||
|
||||
@OptIn(ExperimentalFoundationApi::class)
|
||||
@Composable
|
||||
fun ProfileScreen(user: User, accountViewModel: AccountViewModel, nav: (String) -> Unit) {
|
||||
val accountState by accountViewModel.accountLiveData.observeAsState()
|
||||
val account = remember(accountState) { accountState?.account } ?: return
|
||||
fun ProfileScreen(baseUser: User, accountViewModel: AccountViewModel, nav: (String) -> Unit) {
|
||||
UserProfileNewThreadFeedFilter.loadUserProfile(accountViewModel.account, baseUser)
|
||||
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)
|
||||
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)
|
||||
NostrUserProfileDataSource.loadUserProfile(baseUser)
|
||||
|
||||
val lifeCycleOwner = LocalLifecycleOwner.current
|
||||
|
||||
@@ -148,7 +145,7 @@ fun ProfileScreen(user: User, accountViewModel: AccountViewModel, nav: (String)
|
||||
val observer = LifecycleEventObserver { _, event ->
|
||||
if (event == Lifecycle.Event.ON_RESUME) {
|
||||
println("Profidle Start")
|
||||
NostrUserProfileDataSource.loadUserProfile(user)
|
||||
NostrUserProfileDataSource.loadUserProfile(baseUser)
|
||||
NostrUserProfileDataSource.start()
|
||||
}
|
||||
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 tabsSize by remember { mutableStateOf(IntSize.Zero) }
|
||||
|
||||
@@ -209,7 +204,7 @@ fun ProfileScreen(user: User, accountViewModel: AccountViewModel, nav: (String)
|
||||
.fillMaxHeight()
|
||||
) {
|
||||
Column(modifier = Modifier.padding()) {
|
||||
ProfileHeader(baseUser, nav, account, accountViewModel)
|
||||
ProfileHeader(baseUser, nav, accountViewModel)
|
||||
ScrollableTabRow(
|
||||
backgroundColor = MaterialTheme.colors.background,
|
||||
selectedTabIndex = pagerState.currentPage,
|
||||
@@ -342,7 +337,6 @@ private fun FollowTabHeader(baseUser: User) {
|
||||
private fun ProfileHeader(
|
||||
baseUser: User,
|
||||
nav: (String) -> Unit,
|
||||
account: Account,
|
||||
accountViewModel: AccountViewModel
|
||||
) {
|
||||
var popupExpanded by remember { mutableStateOf(false) }
|
||||
@@ -394,7 +388,7 @@ private fun ProfileHeader(
|
||||
) {
|
||||
UserPicture(
|
||||
baseUser = baseUser,
|
||||
baseUserAccount = account.userProfile(),
|
||||
baseUserAccount = accountViewModel.userProfile(),
|
||||
size = 100.dp,
|
||||
modifier = Modifier.border(
|
||||
3.dp,
|
||||
@@ -427,11 +421,11 @@ private fun ProfileHeader(
|
||||
// No need for this button anymore
|
||||
// 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))
|
||||
}
|
||||
@@ -446,11 +440,14 @@ private fun ProfileHeader(
|
||||
@Composable
|
||||
private fun ProfileActions(
|
||||
baseUser: User,
|
||||
account: Account,
|
||||
accountViewModel: AccountViewModel,
|
||||
coroutineScope: CoroutineScope
|
||||
) {
|
||||
val accountUserState by account.userProfile().live().follows.observeAsState()
|
||||
val accountLocalUserState by account.live.observeAsState()
|
||||
val accountLocalUserState by accountViewModel.accountLiveData.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 isHidden by remember(accountUserState, accountLocalUserState) {
|
||||
@@ -497,7 +494,7 @@ private fun ProfileActions(
|
||||
}
|
||||
|
||||
@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 user = remember(userState) { userState?.user } ?: return
|
||||
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 pubkeyHex = remember { baseUser.pubkeyHex }
|
||||
DisplayLNAddress(lud16, pubkeyHex, account, scope, context)
|
||||
DisplayLNAddress(lud16, pubkeyHex, accountViewModel.account, scope, context)
|
||||
|
||||
val identities = user.info?.latestMetadata?.identityClaims()
|
||||
if (!identities.isNullOrEmpty()) {
|
||||
@@ -733,25 +730,54 @@ private fun DisplayBadges(
|
||||
nav: (String) -> Unit
|
||||
) {
|
||||
val userBadgeState by baseUser.live().badges.observeAsState()
|
||||
val userBadge = remember(userBadgeState) { userBadgeState?.user } ?: return
|
||||
|
||||
userBadge.acceptedBadges?.let { note ->
|
||||
(note.event as? BadgeProfilesEvent)?.let { event ->
|
||||
FlowRow(verticalAlignment = Alignment.CenterVertically, modifier = Modifier.padding(vertical = 5.dp)) {
|
||||
event.badgeAwardEvents().forEach { badgeAwardEvent ->
|
||||
val baseNote = LocalCache.notes[badgeAwardEvent]
|
||||
if (baseNote != null) {
|
||||
val badgeAwardState by baseNote.live().metadata.observeAsState()
|
||||
val baseBadgeDefinition = badgeAwardState?.note?.replyTo?.firstOrNull()
|
||||
|
||||
if (baseBadgeDefinition != null) {
|
||||
BadgeThumb(baseBadgeDefinition, nav, 35.dp)
|
||||
}
|
||||
}
|
||||
}
|
||||
val badgeList by remember(userBadgeState) {
|
||||
derivedStateOf {
|
||||
val list = (userBadgeState?.user?.acceptedBadges?.event as? BadgeProfilesEvent)?.badgeAwardEvents()
|
||||
if (list.isNullOrEmpty()) {
|
||||
null
|
||||
} else {
|
||||
list
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
@@ -762,7 +788,7 @@ fun BadgeThumb(
|
||||
pictureModifier: Modifier = Modifier
|
||||
) {
|
||||
BadgeThumb(note, size, pictureModifier) {
|
||||
nav("Note/${it.idHex}")
|
||||
nav("Note/$it")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -771,19 +797,19 @@ fun BadgeThumb(
|
||||
baseNote: Note,
|
||||
size: Dp,
|
||||
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(
|
||||
Modifier
|
||||
.width(size)
|
||||
.height(size)
|
||||
remember {
|
||||
Modifier
|
||||
.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) {
|
||||
RobohashAsyncImage(
|
||||
robot = "authornotfound",
|
||||
@@ -796,7 +822,7 @@ fun BadgeThumb(
|
||||
)
|
||||
} else {
|
||||
RobohashFallbackAsyncImage(
|
||||
robot = note.idHex,
|
||||
robot = event.id,
|
||||
robotSize = size,
|
||||
model = image,
|
||||
contentDescription = stringResource(id = R.string.profile_image),
|
||||
@@ -807,7 +833,7 @@ fun BadgeThumb(
|
||||
.background(MaterialTheme.colors.background)
|
||||
.run {
|
||||
if (onClick != null) {
|
||||
this.clickable(onClick = { onClick(note) })
|
||||
this.clickable(onClick = { onClick(event.id) })
|
||||
} else {
|
||||
this
|
||||
}
|
||||
@@ -871,7 +897,7 @@ fun TabNotesNewThreads(accountViewModel: AccountViewModel, nav: (String) -> Unit
|
||||
Column(
|
||||
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(
|
||||
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(
|
||||
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(
|
||||
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.NostrSearchEventOrUserDataSource
|
||||
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.ChannelName
|
||||
import com.vitorpamplona.amethyst.ui.note.NoteCompose
|
||||
import com.vitorpamplona.amethyst.ui.note.UserCompose
|
||||
import com.vitorpamplona.amethyst.ui.note.UserPicture
|
||||
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.RefresheableView
|
||||
import com.vitorpamplona.amethyst.ui.screen.ScrollStateKeys
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.FlowPreview
|
||||
@@ -81,25 +80,16 @@ import kotlinx.coroutines.channels.Channel as CoroutineChannel
|
||||
fun SearchScreen(
|
||||
searchFeedViewModel: NostrGlobalFeedViewModel,
|
||||
accountViewModel: AccountViewModel,
|
||||
nav: (String) -> Unit,
|
||||
scrollToTop: Boolean = false
|
||||
nav: (String) -> Unit
|
||||
) {
|
||||
val lifeCycleOwner = LocalLifecycleOwner.current
|
||||
val account = accountViewModel.accountLiveData.value?.account ?: return
|
||||
|
||||
GlobalFeedFilter.account = account
|
||||
|
||||
LaunchedEffect(accountViewModel) {
|
||||
GlobalFeedFilter.account = account
|
||||
NostrGlobalDataSource.resetFilters()
|
||||
searchFeedViewModel.invalidateData()
|
||||
}
|
||||
WatchAccountForSearchScreen(searchFeedViewModel, accountViewModel)
|
||||
|
||||
DisposableEffect(accountViewModel) {
|
||||
val observer = LifecycleEventObserver { _, event ->
|
||||
if (event == Lifecycle.Event.ON_RESUME) {
|
||||
println("Global Start")
|
||||
GlobalFeedFilter.account = account
|
||||
NostrGlobalDataSource.start()
|
||||
NostrSearchEventOrUserDataSource.start()
|
||||
searchFeedViewModel.invalidateData()
|
||||
@@ -123,11 +113,19 @@ fun SearchScreen(
|
||||
modifier = Modifier.padding(vertical = 0.dp)
|
||||
) {
|
||||
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() {
|
||||
var account: Account? = null
|
||||
|
||||
@@ -168,14 +166,14 @@ class SearchBarViewModel : ViewModel() {
|
||||
searchResultsNotes.value = emptyList()
|
||||
}
|
||||
|
||||
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()
|
||||
}
|
||||
private val bundler = BundledUpdate(250, Dispatchers.IO)
|
||||
|
||||
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()
|
||||
|
||||
@@ -7,7 +7,6 @@ import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.DisposableEffect
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.livedata.observeAsState
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.platform.LocalLifecycleOwner
|
||||
import androidx.compose.ui.unit.dp
|
||||
@@ -15,55 +14,51 @@ import androidx.lifecycle.Lifecycle
|
||||
import androidx.lifecycle.LifecycleEventObserver
|
||||
import androidx.lifecycle.viewmodel.compose.viewModel
|
||||
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.ThreadFeedView
|
||||
|
||||
@Composable
|
||||
fun ThreadScreen(noteId: String?, accountViewModel: AccountViewModel, nav: (String) -> Unit) {
|
||||
val account by accountViewModel.accountLiveData.observeAsState()
|
||||
if (noteId == null) return
|
||||
|
||||
val lifeCycleOwner = LocalLifecycleOwner.current
|
||||
|
||||
if (account != null && noteId != null) {
|
||||
ThreadFeedFilter.loadThread(noteId)
|
||||
val feedViewModel: NostrThreadFeedViewModel = viewModel()
|
||||
val feedViewModel: NostrThreadFeedViewModel = viewModel(
|
||||
key = noteId + "NostrThreadFeedViewModel",
|
||||
factory = NostrThreadFeedViewModel.Factory(noteId)
|
||||
)
|
||||
|
||||
LaunchedEffect(noteId) {
|
||||
NostrThreadDataSource.loadThread(noteId)
|
||||
ThreadFeedFilter.loadThread(noteId)
|
||||
feedViewModel.invalidateData()
|
||||
}
|
||||
LaunchedEffect(noteId) {
|
||||
NostrThreadDataSource.loadThread(noteId)
|
||||
feedViewModel.invalidateData()
|
||||
}
|
||||
|
||||
DisposableEffect(accountViewModel) {
|
||||
val observer = LifecycleEventObserver { _, event ->
|
||||
if (event == Lifecycle.Event.ON_RESUME) {
|
||||
println("Thread Start")
|
||||
NostrThreadDataSource.loadThread(noteId)
|
||||
NostrThreadDataSource.start()
|
||||
ThreadFeedFilter.loadThread(noteId)
|
||||
feedViewModel.invalidateData()
|
||||
}
|
||||
if (event == Lifecycle.Event.ON_PAUSE) {
|
||||
println("Thread Stop")
|
||||
ThreadFeedFilter.loadThread(null)
|
||||
NostrThreadDataSource.loadThread(null)
|
||||
NostrThreadDataSource.stop()
|
||||
}
|
||||
DisposableEffect(accountViewModel) {
|
||||
val observer = LifecycleEventObserver { _, event ->
|
||||
if (event == Lifecycle.Event.ON_RESUME) {
|
||||
println("Thread Start")
|
||||
NostrThreadDataSource.loadThread(noteId)
|
||||
NostrThreadDataSource.start()
|
||||
feedViewModel.invalidateData()
|
||||
}
|
||||
|
||||
lifeCycleOwner.lifecycle.addObserver(observer)
|
||||
onDispose {
|
||||
lifeCycleOwner.lifecycle.removeObserver(observer)
|
||||
if (event == Lifecycle.Event.ON_PAUSE) {
|
||||
println("Thread Stop")
|
||||
NostrThreadDataSource.loadThread(null)
|
||||
NostrThreadDataSource.stop()
|
||||
}
|
||||
}
|
||||
|
||||
Column(Modifier.fillMaxHeight()) {
|
||||
Column(
|
||||
modifier = Modifier.padding(vertical = 0.dp)
|
||||
) {
|
||||
ThreadFeedView(noteId, feedViewModel, accountViewModel, nav)
|
||||
}
|
||||
lifeCycleOwner.lifecycle.addObserver(observer)
|
||||
onDispose {
|
||||
lifeCycleOwner.lifecycle.removeObserver(observer)
|
||||
}
|
||||
}
|
||||
|
||||
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.NewPostView
|
||||
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.note.FileHeaderDisplay
|
||||
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.FeedError
|
||||
import com.vitorpamplona.amethyst.ui.screen.FeedState
|
||||
import com.vitorpamplona.amethyst.ui.screen.FeedViewModel
|
||||
import com.vitorpamplona.amethyst.ui.screen.LoadingFeed
|
||||
import com.vitorpamplona.amethyst.ui.screen.NostrVideoFeedViewModel
|
||||
import com.vitorpamplona.amethyst.ui.screen.ScrollStateKeys
|
||||
import com.vitorpamplona.amethyst.ui.screen.rememberForeverPagerState
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
@@ -95,30 +94,16 @@ import kotlinx.coroutines.launch
|
||||
fun VideoScreen(
|
||||
videoFeedView: NostrVideoFeedViewModel,
|
||||
accountViewModel: AccountViewModel,
|
||||
nav: (String) -> Unit,
|
||||
scrollToTop: Boolean = false
|
||||
nav: (String) -> Unit
|
||||
) {
|
||||
val lifeCycleOwner = LocalLifecycleOwner.current
|
||||
val account = accountViewModel.accountLiveData.value?.account ?: return
|
||||
|
||||
val accountState = account.live.observeAsState()
|
||||
|
||||
NostrVideoDataSource.account = account
|
||||
VideoFeedFilter.account = account
|
||||
|
||||
LaunchedEffect(accountViewModel, accountState.value?.account?.defaultStoriesFollowList) {
|
||||
VideoFeedFilter.account = account
|
||||
NostrVideoDataSource.account = account
|
||||
NostrVideoDataSource.resetFilters()
|
||||
videoFeedView.invalidateData()
|
||||
}
|
||||
WatchAccountForVideoScreen(videoFeedView = videoFeedView, accountViewModel = accountViewModel)
|
||||
|
||||
DisposableEffect(accountViewModel) {
|
||||
val observer = LifecycleEventObserver { _, event ->
|
||||
if (event == Lifecycle.Event.ON_RESUME) {
|
||||
println("Video Start")
|
||||
VideoFeedFilter.account = account
|
||||
NostrVideoDataSource.account = account
|
||||
NostrVideoDataSource.start()
|
||||
}
|
||||
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 = Modifier.padding(vertical = 0.dp)
|
||||
) {
|
||||
FeedView(videoFeedView, accountViewModel, nav, ScrollStateKeys.VIDEO_SCREEN, scrollToTop)
|
||||
SaveableFeedState(videoFeedView, accountViewModel, nav, ScrollStateKeys.VIDEO_SCREEN)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@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,
|
||||
accountViewModel: AccountViewModel,
|
||||
nav: (String) -> Unit,
|
||||
scrollStateKey: String? = null,
|
||||
scrollToTop: Boolean = false
|
||||
routeForLastRead: String?,
|
||||
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()
|
||||
|
||||
@@ -180,10 +202,9 @@ fun FeedView(
|
||||
is FeedState.Loaded -> {
|
||||
SlidingCarousel(
|
||||
state.feed,
|
||||
pagerState,
|
||||
accountViewModel,
|
||||
nav,
|
||||
scrollStateKey,
|
||||
scrollToTop
|
||||
nav
|
||||
)
|
||||
}
|
||||
|
||||
@@ -200,23 +221,10 @@ fun FeedView(
|
||||
@Composable
|
||||
fun SlidingCarousel(
|
||||
feed: MutableState<ImmutableList<Note>>,
|
||||
pagerState: PagerState,
|
||||
accountViewModel: AccountViewModel,
|
||||
nav: (String) -> Unit,
|
||||
scrollStateKey: String? = null,
|
||||
scrollToTop: Boolean = false
|
||||
nav: (String) -> Unit
|
||||
) {
|
||||
val pagerState = if (scrollStateKey != null) {
|
||||
rememberForeverPagerState(scrollStateKey)
|
||||
} else {
|
||||
remember { PagerState() }
|
||||
}
|
||||
|
||||
if (scrollToTop) {
|
||||
LaunchedEffect(Unit) {
|
||||
pagerState.scrollToPage(page = 0)
|
||||
}
|
||||
}
|
||||
|
||||
VerticalPager(
|
||||
pageCount = feed.value.size,
|
||||
state = pagerState,
|
||||
|
||||
+153
-140
@@ -55,26 +55,16 @@ fun TranslatableRichTextViewer(
|
||||
var showOriginal by remember { mutableStateOf(false) }
|
||||
var langSettingsPopupExpanded by remember { mutableStateOf(false) }
|
||||
|
||||
val accountState by accountViewModel.accountLanguagesLiveData.observeAsState()
|
||||
val account = remember(accountState) { accountState?.account } ?: return
|
||||
WatchLanguageChanges(content, accountViewModel) { result, newShowOriginal ->
|
||||
if (translatedTextState.result != result.result ||
|
||||
translatedTextState.sourceLang != result.sourceLang ||
|
||||
translatedTextState.targetLang != result.targetLang
|
||||
) {
|
||||
translatedTextState = result
|
||||
}
|
||||
|
||||
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!!)
|
||||
showOriginal = preference == task.result.sourceLang
|
||||
}
|
||||
translatedTextState = task.result
|
||||
}
|
||||
}
|
||||
if (showOriginal != newShowOriginal) {
|
||||
showOriginal = newShowOriginal
|
||||
}
|
||||
}
|
||||
|
||||
@@ -98,137 +88,135 @@ fun TranslatableRichTextViewer(
|
||||
val target = translatedTextState.targetLang
|
||||
val source = translatedTextState.sourceLang
|
||||
|
||||
if (source != null && target != null) {
|
||||
if (source != target) {
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(top = 5.dp)
|
||||
if (source != null && target != null && source != target) {
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.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 =
|
||||
SpanStyle(color = MaterialTheme.colors.primary.copy(alpha = 0.52f))
|
||||
|
||||
val annotatedTranslationString = buildAnnotatedString {
|
||||
withStyle(clickableTextStyle) {
|
||||
pushStringAnnotation("langSettings", true.toString())
|
||||
append(stringResource(R.string.translations_auto))
|
||||
DropdownMenuItem(onClick = {
|
||||
accountViewModel.dontTranslateFrom(source)
|
||||
langSettingsPopupExpanded = false
|
||||
}) {
|
||||
if (source in accountViewModel.account.dontTranslateFrom) {
|
||||
Icon(
|
||||
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) {
|
||||
pushStringAnnotation("showOriginal", true.toString())
|
||||
append(Locale(source).displayName)
|
||||
}
|
||||
|
||||
append(" ${stringResource(R.string.translations_to)} ")
|
||||
|
||||
withStyle(clickableTextStyle) {
|
||||
pushStringAnnotation("showOriginal", false.toString())
|
||||
append(Locale(target).displayName)
|
||||
}
|
||||
Text(stringResource(R.string.translations_never_translate_from_lang, Locale(source).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(
|
||||
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()
|
||||
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 (accountViewModel.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 accountViewModel.account.translateTo) {
|
||||
Icon(
|
||||
imageVector = Icons.Default.Check,
|
||||
contentDescription = null,
|
||||
modifier = Modifier.size(24.dp)
|
||||
)
|
||||
} else {
|
||||
langSettingsPopupExpanded = !langSettingsPopupExpanded
|
||||
Spacer(modifier = Modifier.size(24.dp))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
DropdownMenu(
|
||||
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))
|
||||
|
||||
Spacer(modifier = Modifier.size(10.dp))
|
||||
|
||||
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))
|
||||
}
|
||||
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