Moving unstable parameters into stable ones to reduce recompositions.

This commit is contained in:
Vitor Pamplona 2023-06-06 15:51:43 -04:00
parent 50f3f7f176
commit c10db10430
46 changed files with 724 additions and 441 deletions

View File

@ -3,15 +3,15 @@ package com.vitorpamplona.amethyst.ui.components
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import com.vitorpamplona.amethyst.ui.actions.ImmutableListOfLists
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
import kotlinx.collections.immutable.ImmutableList
@Composable
fun TranslatableRichTextViewer(
content: String,
canPreview: Boolean,
modifier: Modifier = Modifier,
tags: ImmutableList<List<String>>,
tags: ImmutableListOfLists<String>,
backgroundColor: Color,
accountViewModel: AccountViewModel,
nav: (String) -> Unit

View File

@ -1,7 +1,9 @@
package com.vitorpamplona.amethyst.model
import androidx.compose.runtime.Immutable
import com.vitorpamplona.amethyst.service.relays.FeedType
@Immutable
data class RelaySetupInfo(
val url: String,
val read: Boolean,

View File

@ -362,6 +362,7 @@ class UserLiveSet(u: User) {
}
}
@Immutable
data class RelayInfo(
val url: String,
var lastEvent: Long,
@ -403,7 +404,7 @@ class UserMetadata {
fun anyNameStartsWith(prefix: String): Boolean {
return listOfNotNull(name, username, display_name, displayName, nip05, lud06, lud16)
.any { it.startsWith(prefix, true) }
.any { it.contains(prefix, true) }
}
fun lnAddress(): String? {

View File

@ -48,14 +48,14 @@ object NostrSearchEventOrUserDataSource : NostrDataSource("SingleEventFeed") {
filter = JsonFilter(
kinds = listOf(MetadataEvent.kind),
search = mySearchString,
limit = 20
limit = 100
)
),
TypedFilter(
types = setOf(FeedType.SEARCH),
filter = JsonFilter(
search = mySearchString,
limit = 20
limit = 100
)
)
)

View File

@ -14,6 +14,7 @@ import androidx.compose.foundation.layout.heightIn
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
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.Divider
@ -28,6 +29,7 @@ import androidx.compose.material.icons.filled.Clear
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.remember
@ -73,9 +75,23 @@ import kotlinx.coroutines.withContext
@Composable
fun JoinUserOrChannelView(onClose: () -> Unit, accountViewModel: AccountViewModel, nav: (String) -> Unit) {
val searchBarViewModel: SearchBarViewModel = viewModel()
searchBarViewModel.account = accountViewModel.account
val searchBarViewModel: SearchBarViewModel = viewModel(
key = accountViewModel.account.userProfile().pubkeyHex + "SearchBarViewModel",
factory = SearchBarViewModel.Factory(
accountViewModel.account
)
)
JoinUserOrChannelView(
searchBarViewModel = searchBarViewModel,
onClose = onClose,
accountViewModel = accountViewModel,
nav = nav
)
}
@Composable
fun JoinUserOrChannelView(searchBarViewModel: SearchBarViewModel, onClose: () -> Unit, accountViewModel: AccountViewModel, nav: (String) -> Unit) {
Dialog(
onDismissRequest = {
NostrSearchEventOrUserDataSource.clear()
@ -88,7 +104,9 @@ fun JoinUserOrChannelView(onClose: () -> Unit, accountViewModel: AccountViewMode
) {
Surface() {
Column(
modifier = Modifier.padding(10.dp).heightIn(min = 500.dp)
modifier = Modifier
.padding(10.dp)
.heightIn(min = 500.dp)
) {
Row(
modifier = Modifier.fillMaxWidth(),
@ -115,28 +133,20 @@ fun JoinUserOrChannelView(onClose: () -> Unit, accountViewModel: AccountViewMode
Spacer(modifier = Modifier.height(15.dp))
RenderSeach(searchBarViewModel, accountViewModel, nav)
RenderSearch(searchBarViewModel, accountViewModel, nav)
}
}
}
}
@OptIn(ExperimentalComposeUiApi::class)
@Composable
private fun RenderSeach(
private fun RenderSearch(
searchBarViewModel: SearchBarViewModel,
accountViewModel: AccountViewModel,
nav: (String) -> Unit
) {
val scope = rememberCoroutineScope()
val listState = rememberLazyListState()
// initialize focus reference to be able to request focus programmatically
val focusRequester = remember { FocusRequester() }
val keyboardController = LocalSoftwareKeyboardController.current
val onlineSearch = NostrSearchEventOrUserDataSource
val lifeCycleOwner = LocalLifecycleOwner.current
// Create a channel for processing search queries.
@ -147,7 +157,7 @@ private fun RenderSeach(
LaunchedEffect(Unit) {
launch(Dispatchers.IO) {
LocalCache.live.newEventBundles.collect {
if (searchBarViewModel.isSearching()) {
if (searchBarViewModel.isSearchingFun()) {
searchBarViewModel.invalidateData()
}
}
@ -155,9 +165,6 @@ private fun RenderSeach(
}
LaunchedEffect(Unit) {
delay(100)
focusRequester.requestFocus()
// Wait for text changes to stop for 300 ms before firing off search.
withContext(Dispatchers.IO) {
searchTextChanges.receiveAsFlow()
@ -166,13 +173,13 @@ private fun RenderSeach(
.debounce(300)
.collectLatest {
if (it.length >= 2) {
onlineSearch.search(it.trim())
NostrSearchEventOrUserDataSource.search(it.trim())
}
searchBarViewModel.invalidateData()
// makes sure to show the top of the search
scope.launch(Dispatchers.Main) {
launch(Dispatchers.Main) {
listState.animateScrollToItem(0)
}
}
@ -200,8 +207,32 @@ private fun RenderSeach(
}
// LAST ROW
SearchEditTextForJoin(searchBarViewModel, searchTextChanges)
RenderSearchResults(searchBarViewModel, listState, accountViewModel, nav)
}
@OptIn(ExperimentalComposeUiApi::class)
@Composable
private fun SearchEditTextForJoin(
searchBarViewModel: SearchBarViewModel,
searchTextChanges: Channel<String>
) {
val scope = rememberCoroutineScope()
// initialize focus reference to be able to request focus programmatically
val focusRequester = remember { FocusRequester() }
val keyboardController = LocalSoftwareKeyboardController.current
LaunchedEffect(Unit) {
delay(100)
focusRequester.requestFocus()
}
Row(
modifier = Modifier.padding(horizontal = 10.dp).fillMaxWidth(),
modifier = Modifier
.padding(horizontal = 10.dp)
.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically
) {
@ -238,11 +269,11 @@ private fun RenderSeach(
)
},
trailingIcon = {
if (searchBarViewModel.isTrailingIconVisible) {
if (searchBarViewModel.isSearching) {
IconButton(
onClick = {
searchBarViewModel.clean()
onlineSearch.clear()
NostrSearchEventOrUserDataSource.clear()
}
) {
Icon(
@ -254,10 +285,24 @@ private fun RenderSeach(
}
)
}
}
@Composable
private fun RenderSearchResults(
searchBarViewModel: SearchBarViewModel,
listState: LazyListState,
accountViewModel: AccountViewModel,
nav: (String) -> Unit
) {
if (searchBarViewModel.isSearching) {
val users by searchBarViewModel.searchResultsUsers.collectAsState()
val channels by searchBarViewModel.searchResultsChannels.collectAsState()
if (searchBarViewModel.searchValue.isNotBlank()) {
Row(
modifier = Modifier.fillMaxWidth().fillMaxHeight().padding(vertical = 10.dp)
modifier = Modifier
.fillMaxWidth()
.fillMaxHeight()
.padding(vertical = 10.dp)
) {
LazyColumn(
modifier = Modifier.fillMaxHeight(),
@ -268,7 +313,7 @@ private fun RenderSeach(
state = listState
) {
itemsIndexed(
searchBarViewModel.searchResultsUsers.value,
users,
key = { _, item -> "u" + item.pubkeyHex }
) { _, item ->
UserComposeForChat(
@ -279,7 +324,7 @@ private fun RenderSeach(
}
itemsIndexed(
searchBarViewModel.searchResultsChannels.value,
channels,
key = { _, item -> "c" + item.idHex }
) { _, item ->
ChannelName(
@ -325,7 +370,11 @@ fun UserComposeForChat(
) {
UserPicture(baseUser, nav, accountViewModel, 55.dp)
Column(modifier = Modifier.padding(start = 10.dp).weight(1f)) {
Column(
modifier = Modifier
.padding(start = 10.dp)
.weight(1f)
) {
Row(verticalAlignment = Alignment.CenterVertically) {
UsernameDisplay(baseUser)
}

View File

@ -3,6 +3,7 @@ package com.vitorpamplona.amethyst.ui.actions
import android.content.Context
import android.net.Uri
import android.util.Log
import androidx.compose.runtime.Stable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.setValue
@ -14,6 +15,7 @@ import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.launch
@Stable
open class NewMediaModel : ViewModel() {
var account: Account? = null

View File

@ -33,6 +33,7 @@ import com.vitorpamplona.amethyst.model.Account
import com.vitorpamplona.amethyst.ui.components.*
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
import com.vitorpamplona.amethyst.ui.screen.loggedIn.TextSpinner
import kotlinx.collections.immutable.toImmutableList
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
@ -124,8 +125,8 @@ fun ImageVideoPost(postViewModel: NewMediaModel, acc: Account) {
Triple(ServersAvailable.NIP95, stringResource(id = R.string.upload_server_relays_nip95), stringResource(id = R.string.upload_server_relays_nip95_explainer))
)
val fileServerOptions = fileServers.map { it.second }
val fileServerExplainers = fileServers.map { it.third }
val fileServerOptions = remember { fileServers.map { it.second }.toImmutableList() }
val fileServerExplainers = remember { fileServers.map { it.third }.toImmutableList() }
val resolver = LocalContext.current.contentResolver
Row(
@ -172,7 +173,7 @@ fun ImageVideoPost(postViewModel: NewMediaModel, acc: Account) {
}
} else {
postViewModel.galleryUri?.let {
VideoView(it)
VideoView(it.toString())
}
}
}

View File

@ -30,6 +30,7 @@ import androidx.compose.material.icons.rounded.Warning
import androidx.compose.runtime.Composable
import androidx.compose.runtime.DisposableEffect
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.Stable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.livedata.observeAsState
import androidx.compose.runtime.mutableStateOf
@ -70,6 +71,7 @@ import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
import com.vitorpamplona.amethyst.ui.screen.loggedIn.TextSpinner
import com.vitorpamplona.amethyst.ui.screen.loggedIn.UserLine
import com.vitorpamplona.amethyst.ui.theme.BitcoinOrange
import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.toImmutableList
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.delay
@ -163,7 +165,7 @@ fun NewPostView(onClose: () -> Unit, baseReplyTo: Note? = null, quote: Note? = n
.fillMaxWidth()
.verticalScroll(scrollState)
) {
Notifying(postViewModel.mentions) {
Notifying(postViewModel.mentions?.toImmutableList()) {
postViewModel.removeFromReplyList(it)
}
@ -373,7 +375,7 @@ fun NewPostView(onClose: () -> Unit, baseReplyTo: Note? = null, quote: Note? = n
@OptIn(ExperimentalLayoutApi::class)
@Composable
fun Notifying(baseMentions: List<User>?, onClick: (User) -> Unit) {
fun Notifying(baseMentions: ImmutableList<User>?, onClick: (User) -> Unit) {
val mentions = baseMentions?.toSet()
FlowRow(verticalAlignment = Alignment.CenterVertically, modifier = Modifier.padding(horizontal = 10.dp)) {
@ -390,7 +392,7 @@ fun Notifying(baseMentions: List<User>?, onClick: (User) -> Unit) {
Spacer(modifier = Modifier.width(5.dp))
val tags = remember(innerUserState) {
myUser.info?.latestMetadata?.tags?.toImmutableList()
myUser.info?.latestMetadata?.tags?.toImmutableListOfLists()
}
Button(
@ -747,8 +749,8 @@ fun ImageVideoDescription(
Triple(ServersAvailable.NIP95, stringResource(id = R.string.upload_server_relays_nip95), stringResource(id = R.string.upload_server_relays_nip95_explainer))
)
val fileServerOptions = fileServers.map { it.second }
val fileServerExplainers = fileServers.map { it.third }
val fileServerOptions = remember { fileServers.map { it.second }.toImmutableList() }
val fileServerExplainers = remember { fileServers.map { it.third }.toImmutableList() }
var selectedServer by remember { mutableStateOf(defaultServer) }
var message by remember { mutableStateOf("") }
@ -854,7 +856,7 @@ fun ImageVideoDescription(
)
}
} else {
VideoView(uri)
VideoView(uri.toString())
}
}
@ -922,3 +924,10 @@ fun ImageVideoDescription(
}
}
}
@Stable
data class ImmutableListOfLists<T>(val lists: List<List<T>> = emptyList())
fun List<List<String>>.toImmutableListOfLists(): ImmutableListOfLists<String> {
return ImmutableListOfLists(this)
}

View File

@ -2,6 +2,7 @@ package com.vitorpamplona.amethyst.ui.actions
import android.content.Context
import android.net.Uri
import androidx.compose.runtime.Stable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateListOf
import androidx.compose.runtime.mutableStateMapOf
@ -24,6 +25,7 @@ import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.launch
@Stable
open class NewPostViewModel() : ViewModel() {
var account: Account? = null
var originalNote: Note? = null

View File

@ -7,6 +7,7 @@ import com.vitorpamplona.amethyst.service.model.ContactListEvent
import com.vitorpamplona.amethyst.service.relays.Constants
import com.vitorpamplona.amethyst.service.relays.FeedType
import com.vitorpamplona.amethyst.service.relays.RelayPool
import kotlinx.collections.immutable.toImmutableSet
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.update
@ -46,7 +47,7 @@ class NewRelayListViewModel : ViewModel() {
if (relayFile != null) {
relayFile.map {
val liveRelay = RelayPool.getRelay(it.key)
val localInfoFeedTypes = account.localRelays.filter { localRelay -> localRelay.url == it.key }.firstOrNull()?.feedTypes ?: FeedType.values().toSet()
val localInfoFeedTypes = account.localRelays.filter { localRelay -> localRelay.url == it.key }.firstOrNull()?.feedTypes ?: FeedType.values().toSet().toImmutableSet()
val errorCounter = liveRelay?.errorCounter ?: 0
val eventDownloadCounter = liveRelay?.eventDownloadCounterInBytes ?: 0

View File

@ -13,6 +13,7 @@ import com.vitorpamplona.amethyst.model.Account
import com.vitorpamplona.amethyst.service.model.GitHubIdentity
import com.vitorpamplona.amethyst.service.model.MastodonIdentity
import com.vitorpamplona.amethyst.service.model.TwitterIdentity
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.launch
import java.io.ByteArrayInputStream
@ -119,7 +120,9 @@ class NewUserMetadataViewModel : ViewModel() {
val writer = StringWriter()
ObjectMapper().writeValue(writer, currentJson)
account.sendNewUserMetadata(writer.buffer.toString(), newClaims)
viewModelScope.launch(Dispatchers.IO) {
account.sendNewUserMetadata(writer.buffer.toString(), newClaims)
}
clear()
}

View File

@ -28,6 +28,8 @@ import com.google.accompanist.permissions.isGranted
import com.google.accompanist.permissions.rememberPermissionState
import com.vitorpamplona.amethyst.R
import com.vitorpamplona.amethyst.ui.GetMediaActivityResultContract
import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.toImmutableList
import java.util.concurrent.atomic.AtomicBoolean
@OptIn(ExperimentalPermissionsApi::class)
@ -99,21 +101,23 @@ private fun UploadBoxButton(
}
}
val DefaultAnimationColors = listOf(
Color(0xFF5851D8),
Color(0xFF833AB4),
Color(0xFFC13584),
Color(0xFFE1306C),
Color(0xFFFD1D1D),
Color(0xFFF56040),
Color(0xFFF77737),
Color(0xFFFCAF45),
Color(0xFFFFDC80),
Color(0xFF5851D8)
).toImmutableList()
@Composable
fun LoadingAnimation(
indicatorSize: Dp = 20.dp,
circleColors: List<Color> = listOf(
Color(0xFF5851D8),
Color(0xFF833AB4),
Color(0xFFC13584),
Color(0xFFE1306C),
Color(0xFFFD1D1D),
Color(0xFFF56040),
Color(0xFFF77737),
Color(0xFFFCAF45),
Color(0xFFFFDC80),
Color(0xFF5851D8)
),
circleColors: ImmutableList<Color> = DefaultAnimationColors,
animationDuration: Int = 1000
) {
val infiniteTransition = rememberInfiniteTransition()

View File

@ -47,6 +47,8 @@ import com.vitorpamplona.amethyst.service.NIP30Parser
import com.vitorpamplona.amethyst.service.model.ChannelCreateEvent
import com.vitorpamplona.amethyst.service.model.PrivateDmEvent
import com.vitorpamplona.amethyst.service.nip19.Nip19
import com.vitorpamplona.amethyst.ui.actions.ImmutableListOfLists
import com.vitorpamplona.amethyst.ui.actions.toImmutableListOfLists
import com.vitorpamplona.amethyst.ui.note.LoadChannel
import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.ImmutableMap
@ -271,7 +273,7 @@ private fun DisplayUser(
val userState by it.live().metadata.observeAsState()
val route = remember(userState) { "User/${it.pubkeyHex}" }
val userDisplayName = remember(userState) { userState?.user?.toBestDisplayName() }
val userTags = remember(userState) { userState?.user?.info?.latestMetadata?.tags?.toImmutableList() }
val userTags = remember(userState) { userState?.user?.info?.latestMetadata?.tags?.toImmutableListOfLists() }
val addedCharts = remember {
"${nip19.additionalChars} "
}
@ -325,7 +327,7 @@ fun CreateClickableText(
@Composable
fun CreateTextWithEmoji(
text: String,
tags: ImmutableList<List<String>>?,
tags: ImmutableListOfLists<String>?,
color: Color = Color.Unspecified,
textAlign: TextAlign? = null,
fontWeight: FontWeight? = null,
@ -339,7 +341,7 @@ fun CreateTextWithEmoji(
LaunchedEffect(key1 = text) {
launch(Dispatchers.Default) {
val emojis =
tags?.filter { it.size > 2 && it[0] == "emoji" }?.associate { ":${it[1]}:" to it[2] } ?: emptyMap()
tags?.lists?.filter { it.size > 2 && it[0] == "emoji" }?.associate { ":${it[1]}:" to it[2] } ?: emptyMap()
if (emojis.isNotEmpty()) {
val newEmojiList = assembleAnnotatedList(text, emojis)
@ -440,7 +442,7 @@ fun CreateTextWithEmoji(
@Composable
fun CreateClickableTextWithEmoji(
clickablePart: String,
tags: ImmutableList<List<String>>?,
tags: ImmutableListOfLists<String>?,
style: TextStyle,
onClick: (Int) -> Unit
) {
@ -449,7 +451,7 @@ fun CreateClickableTextWithEmoji(
LaunchedEffect(key1 = clickablePart) {
launch(Dispatchers.Default) {
val emojis =
tags?.filter { it.size > 2 && it[0] == "emoji" }?.associate { ":${it[1]}:" to it[2] } ?: emptyMap()
tags?.lists?.filter { it.size > 2 && it[0] == "emoji" }?.associate { ":${it[1]}:" to it[2] } ?: emptyMap()
if (emojis.isNotEmpty()) {
val newEmojiList = assembleAnnotatedList(clickablePart, emojis)
@ -477,7 +479,7 @@ fun CreateClickableTextWithEmoji(
fun CreateClickableTextWithEmoji(
clickablePart: String,
suffix: String,
tags: ImmutableList<List<String>>?,
tags: ImmutableListOfLists<String>?,
overrideColor: Color? = null,
fontWeight: FontWeight = FontWeight.Normal,
route: String,
@ -490,7 +492,7 @@ fun CreateClickableTextWithEmoji(
LaunchedEffect(key1 = clickablePart) {
launch(Dispatchers.Default) {
val emojis =
tags?.filter { it.size > 2 && it[0] == "emoji" }?.associate { ":${it[1]}:" to it[2] } ?: emptyMap()
tags?.lists?.filter { it.size > 2 && it[0] == "emoji" }?.associate { ":${it[1]}:" to it[2] } ?: emptyMap()
if (emojis.isNotEmpty()) {
val newEmojiList1 = assembleAnnotatedList(clickablePart, emojis)

View File

@ -26,8 +26,8 @@ import androidx.compose.ui.graphics.compositeOver
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import com.vitorpamplona.amethyst.R
import com.vitorpamplona.amethyst.ui.actions.ImmutableListOfLists
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
import kotlinx.collections.immutable.ImmutableList
const val SHORT_TEXT_LENGTH = 350
@ -36,7 +36,7 @@ fun ExpandableRichTextViewer(
content: String,
canPreview: Boolean,
modifier: Modifier,
tags: ImmutableList<List<String>>,
tags: ImmutableListOfLists<String>,
backgroundColor: Color,
accountViewModel: AccountViewModel,
nav: (String) -> Unit

View File

@ -27,19 +27,18 @@ import com.vitorpamplona.amethyst.R
import com.vitorpamplona.amethyst.service.lnurl.LnInvoiceUtil
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import java.math.BigDecimal
import java.text.NumberFormat
@Composable
fun MayBeInvoicePreview(lnbcWord: String) {
var lnInvoice by remember { mutableStateOf<Pair<String, BigDecimal?>?>(null) }
var lnInvoice by remember { mutableStateOf<Pair<String, String?>?>(null) }
LaunchedEffect(key1 = lnbcWord) {
withContext(Dispatchers.IO) {
val myInvoice = LnInvoiceUtil.findInvoice(lnbcWord)
if (myInvoice != null) {
val myInvoiceAmount = try {
LnInvoiceUtil.getAmountInSats(myInvoice)
NumberFormat.getInstance().format(LnInvoiceUtil.getAmountInSats(myInvoice))
} catch (e: Exception) {
e.printStackTrace()
null
@ -60,7 +59,7 @@ fun MayBeInvoicePreview(lnbcWord: String) {
}
@Composable
fun InvoicePreview(lnInvoice: String, amount: BigDecimal?) {
fun InvoicePreview(lnInvoice: String, amount: String?) {
val context = LocalContext.current
Column(
@ -100,9 +99,7 @@ fun InvoicePreview(lnInvoice: String, amount: BigDecimal?) {
amount?.let {
Text(
text = "${
NumberFormat.getInstance().format(amount)
} ${stringResource(id = R.string.sats)}",
text = "$it ${stringResource(id = R.string.sats)}",
fontSize = 25.sp,
fontWeight = FontWeight.W500,
modifier = Modifier

View File

@ -39,6 +39,8 @@ import com.vitorpamplona.amethyst.model.Note
import com.vitorpamplona.amethyst.model.User
import com.vitorpamplona.amethyst.model.checkForHashtagWithIcon
import com.vitorpamplona.amethyst.service.nip19.Nip19
import com.vitorpamplona.amethyst.ui.actions.ImmutableListOfLists
import com.vitorpamplona.amethyst.ui.actions.toImmutableListOfLists
import com.vitorpamplona.amethyst.ui.note.NoteCompose
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
import com.vitorpamplona.amethyst.ui.uriToRoute
@ -57,7 +59,6 @@ import java.net.MalformedURLException
import java.net.URISyntaxException
import java.net.URL
import java.util.regex.Pattern
import kotlin.time.ExperimentalTime
val imageExtensions = listOf("png", "jpg", "gif", "bmp", "jpeg", "webp", "svg")
val videoExtensions = listOf("mp4", "avi", "wmv", "mpg", "amv", "webm", "mov", "mp3")
@ -98,7 +99,7 @@ fun RichTextViewer(
content: String,
canPreview: Boolean,
modifier: Modifier,
tags: ImmutableList<List<String>>,
tags: ImmutableListOfLists<String>,
backgroundColor: Color,
accountViewModel: AccountViewModel,
nav: (String) -> Unit
@ -122,11 +123,11 @@ data class RichTextViewerState(
val customEmoji: ImmutableMap<String, String>
)
@OptIn(ExperimentalTime::class, ExperimentalLayoutApi::class)
@OptIn(ExperimentalLayoutApi::class)
@Composable
private fun RenderRegular(
content: String,
tags: ImmutableList<List<String>>,
tags: ImmutableListOfLists<String>,
canPreview: Boolean,
backgroundColor: Color,
accountViewModel: AccountViewModel,
@ -169,7 +170,7 @@ private fun RenderRegular(
private fun parseUrls(
content: String,
tags: List<List<String>>
tags: ImmutableListOfLists<String>
): RichTextViewerState {
val urls = UrlDetector(content, UrlDetectorOptions.Default).detect()
val urlSet = urls.mapTo(LinkedHashSet(urls.size)) { it.originalUrl }
@ -186,7 +187,7 @@ private fun parseUrls(
val imageList = imagesForPager.values.toList()
val emojiMap =
tags.filter { it.size > 2 && it[0] == "emoji" }.associate { ":${it[1]}:" to it[2] }
tags.lists.filter { it.size > 2 && it[0] == "emoji" }.associate { ":${it[1]}:" to it[2] }
return if (urlSet.isNotEmpty() || emojiMap.isNotEmpty()) {
RichTextViewerState(
@ -217,7 +218,7 @@ private fun RenderWord(
backgroundColor: Color,
accountViewModel: AccountViewModel,
nav: (String) -> Unit,
tags: ImmutableList<List<String>>
tags: ImmutableListOfLists<String>
) {
val type = remember(word) {
if (state.imagesForPager[word] != null) {
@ -238,7 +239,7 @@ private fun RenderWord(
WordType.BECH
} else if (word.startsWith("#")) {
if (tagIndex.matcher(word).matches()) {
if (tags.isNotEmpty()) {
if (tags.lists.isNotEmpty()) {
WordType.HASH_INDEX
} else {
WordType.OTHER
@ -267,7 +268,7 @@ private fun RenderWordWithoutPreview(
type: WordType,
word: String,
state: RichTextViewerState,
tags: ImmutableList<List<String>>,
tags: ImmutableListOfLists<String>,
backgroundColor: Color,
accountViewModel: AccountViewModel,
nav: (String) -> Unit
@ -300,7 +301,7 @@ private fun RenderWordWithPreview(
type: WordType,
word: String,
state: RichTextViewerState,
tags: ImmutableList<List<String>>,
tags: ImmutableListOfLists<String>,
backgroundColor: Color,
accountViewModel: AccountViewModel,
nav: (String) -> Unit
@ -382,7 +383,7 @@ fun RenderCustomEmoji(word: String, state: RichTextViewerState) {
}
@Composable
private fun RenderContentAsMarkdown(content: String, backgroundColor: Color, tags: ImmutableList<List<String>>?, nav: (String) -> Unit) {
private fun RenderContentAsMarkdown(content: String, backgroundColor: Color, tags: ImmutableListOfLists<String>?, nav: (String) -> Unit) {
val myMarkDownStyle = richTextDefaults.copy(
codeBlockStyle = richTextDefaults.codeBlockStyle?.copy(
textStyle = TextStyle(
@ -442,17 +443,13 @@ private fun RenderContentAsMarkdown(content: String, backgroundColor: Color, tag
}
@Composable
private fun RefreshableContent(content: String, tags: List<List<String>>?, onCompose: @Composable (String) -> Unit) {
private fun RefreshableContent(content: String, tags: ImmutableListOfLists<String>?, onCompose: @Composable (String) -> Unit) {
var markdownWithSpecialContent by remember(content) { mutableStateOf<String?>(content) }
val scope = rememberCoroutineScope()
ObserverAllNIP19References(content, tags) {
scope.launch(Dispatchers.IO) {
val newMarkdownWithSpecialContent = returnMarkdownWithSpecialContent(content, tags)
if (markdownWithSpecialContent != newMarkdownWithSpecialContent) {
markdownWithSpecialContent = newMarkdownWithSpecialContent
}
val newMarkdownWithSpecialContent = returnMarkdownWithSpecialContent(content, tags)
if (markdownWithSpecialContent != newMarkdownWithSpecialContent) {
markdownWithSpecialContent = newMarkdownWithSpecialContent
}
}
@ -462,7 +459,7 @@ private fun RefreshableContent(content: String, tags: List<List<String>>?, onCom
}
@Composable
fun ObserverAllNIP19References(content: String, tags: List<List<String>>?, onRefresh: () -> Unit) {
fun ObserverAllNIP19References(content: String, tags: ImmutableListOfLists<String>?, onRefresh: () -> Unit) {
var nip19References by remember(content) { mutableStateOf<List<Nip19.Return>>(emptyList()) }
LaunchedEffect(key1 = content) {
launch(Dispatchers.IO) {
@ -490,7 +487,7 @@ fun ObserveNIP19(
@Composable
private fun ObserveNIP19Event(
it: Nip19.Return,
onRefresh: suspend () -> Unit
onRefresh: () -> Unit
) {
var baseNote by remember(it) { mutableStateOf<Note?>(null) }
@ -551,7 +548,7 @@ private fun ObserveNIP19User(
}
}
private fun getDisplayNameAndNIP19FromTag(tag: String, tags: List<List<String>>): Pair<String, String>? {
private fun getDisplayNameAndNIP19FromTag(tag: String, tags: ImmutableListOfLists<String>): Pair<String, String>? {
val matcher = tagIndex.matcher(tag)
val (index, suffix) = try {
matcher.find()
@ -561,8 +558,8 @@ private fun getDisplayNameAndNIP19FromTag(tag: String, tags: List<List<String>>)
Pair(null, null)
}
if (index != null && index >= 0 && index < tags.size) {
val tag = tags[index]
if (index != null && index >= 0 && index < tags.lists.size) {
val tag = tags.lists[index]
if (tag.size > 1) {
if (tag[0] == "p") {
@ -602,7 +599,7 @@ private fun getDisplayNameFromNip19(nip19: Nip19.Return): Pair<String, String>?
return null
}
private fun returnNIP19References(content: String, tags: List<List<String>>?): List<Nip19.Return> {
private fun returnNIP19References(content: String, tags: ImmutableListOfLists<String>?): List<Nip19.Return> {
val listOfReferences = mutableListOf<Nip19.Return>()
content.split('\n').forEach { paragraph ->
paragraph.split(' ').forEach { word: String ->
@ -615,7 +612,7 @@ private fun returnNIP19References(content: String, tags: List<List<String>>?): L
}
}
tags?.forEach {
tags?.lists?.forEach {
if (it[0] == "p" && it.size > 1) {
listOfReferences.add(Nip19.Return(Nip19.Type.USER, it[1], null, null, null, ""))
} else if (it[0] == "e" && it.size > 1) {
@ -628,7 +625,7 @@ private fun returnNIP19References(content: String, tags: List<List<String>>?): L
return listOfReferences
}
private fun returnMarkdownWithSpecialContent(content: String, tags: List<List<String>>?): String {
private fun returnMarkdownWithSpecialContent(content: String, tags: ImmutableListOfLists<String>?): String {
var returnContent = ""
content.split('\n').forEach { paragraph ->
paragraph.split(' ').forEach { word: String ->
@ -867,7 +864,7 @@ fun HashTag(word: String, nav: (String) -> Unit) {
}
@Composable
fun TagLink(word: String, tags: List<List<String>>, canPreview: Boolean, backgroundColor: Color, accountViewModel: AccountViewModel, nav: (String) -> Unit) {
fun TagLink(word: String, tags: ImmutableListOfLists<String>, canPreview: Boolean, backgroundColor: Color, accountViewModel: AccountViewModel, nav: (String) -> Unit) {
var baseUserPair by remember { mutableStateOf<Pair<User, String?>?>(null) }
var baseNotePair by remember { mutableStateOf<Pair<Note, String?>?>(null) }
@ -883,8 +880,8 @@ fun TagLink(word: String, tags: List<List<String>>, canPreview: Boolean, backgro
Pair(null, null)
}
if (index != null && index >= 0 && index < tags.size) {
val tag = tags[index]
if (index != null && index >= 0 && index < tags.lists.size) {
val tag = tags.lists[index]
if (tag.size > 1) {
if (tag[0] == "p") {
@ -911,7 +908,7 @@ fun TagLink(word: String, tags: List<List<String>>, canPreview: Boolean, backgro
"User/${it.first.pubkeyHex}"
}
val userTags = remember(innerUserState) {
innerUserState?.user?.info?.latestMetadata?.tags?.toImmutableList()
innerUserState?.user?.info?.latestMetadata?.tags?.toImmutableListOfLists()
}
CreateClickableTextWithEmoji(

View File

@ -31,13 +31,14 @@ import androidx.compose.ui.graphics.Color
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.compose.ui.window.Dialog
import kotlinx.collections.immutable.ImmutableList
@Composable
fun TextSpinner(
label: String,
placeholder: String,
options: List<String>,
explainers: List<String>? = null,
options: ImmutableList<String>,
explainers: ImmutableList<String>? = null,
onSelect: (Int) -> Unit,
modifier: Modifier = Modifier
) {
@ -83,7 +84,12 @@ fun TextSpinner(
}
@Composable
fun SpinnerSelectionDialog(options: List<String>, explainers: List<String>?, onDismiss: () -> Unit, onSelect: (Int) -> Unit) {
fun SpinnerSelectionDialog(
options: ImmutableList<String>,
explainers: ImmutableList<String>?,
onDismiss: () -> Unit,
onSelect: (Int) -> Unit
) {
Dialog(onDismissRequest = onDismiss) {
Surface(
border = BorderStroke(0.25.dp, Color.LightGray),

View File

@ -1,8 +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
import android.view.View
import android.view.ViewGroup
@ -27,6 +25,7 @@ import androidx.compose.material.icons.filled.VolumeUp
import androidx.compose.runtime.Composable
import androidx.compose.runtime.DisposableEffect
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.Stable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
@ -45,7 +44,6 @@ import androidx.compose.ui.platform.LocalView
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.isFinite
import androidx.compose.ui.viewinterop.AndroidView
import androidx.core.net.toUri
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.LifecycleEventObserver
import coil.imageLoader
@ -60,26 +58,9 @@ import com.google.android.exoplayer2.ui.StyledPlayerView
import com.vitorpamplona.amethyst.VideoCache
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import java.io.File
public var DefaultMutedSetting = mutableStateOf(true)
@Composable
fun VideoView(localFile: File?, description: String? = null, onDialog: ((Boolean) -> Unit)? = null) {
if (localFile != null) {
val video = remember(localFile) { localFile.toUri() }
VideoView(video, description, null, onDialog)
}
}
@Composable
fun VideoView(videoUri: String, description: String? = null, onDialog: ((Boolean) -> Unit)? = null) {
val video = remember(videoUri) { Uri.parse(videoUri) }
VideoView(video, description, null, onDialog)
}
@Composable
fun LoadThumbAndThenVideoView(videoUri: String, description: String? = null, thumbUri: String, onDialog: ((Boolean) -> Unit)? = null) {
var loadingFinished by remember { mutableStateOf<Pair<Boolean, Drawable?>>(Pair(false, null)) }
@ -105,39 +86,37 @@ fun LoadThumbAndThenVideoView(videoUri: String, description: String? = null, thu
if (loadingFinished.first) {
if (loadingFinished.second != null) {
val video = remember(videoUri) { Uri.parse(videoUri) }
VideoView(video, description, loadingFinished.second, onDialog)
VideoView(videoUri, description, VideoThumb(loadingFinished.second), onDialog)
} else {
VideoView(videoUri, description, onDialog)
VideoView(videoUri, description, null, onDialog)
}
}
}
@Composable
fun VideoView(videoUri: Uri, description: String? = null, thumb: Drawable? = null, onDialog: ((Boolean) -> Unit)? = null) {
fun VideoView(videoUri: String, description: String? = null, thumb: VideoThumb? = null, onDialog: ((Boolean) -> Unit)? = null) {
val context = LocalContext.current
val lifecycleOwner = rememberUpdatedState(LocalLifecycleOwner.current)
var exoPlayer by remember { mutableStateOf<ExoPlayer?>(null) }
var exoPlayerData by remember { mutableStateOf<VideoPlayer?>(null) }
val defaultVolume = remember { if (DefaultMutedSetting.value) 0f else 1f }
LaunchedEffect(key1 = videoUri) {
if (exoPlayer == null) {
if (exoPlayerData == null) {
launch(Dispatchers.Default) {
exoPlayer = ExoPlayer.Builder(context).build()
exoPlayerData = VideoPlayer(ExoPlayer.Builder(context).build())
}
}
}
exoPlayer?.let {
exoPlayerData?.let {
val media = remember { MediaItem.Builder().setUri(videoUri).build() }
it.apply {
it.exoPlayer.apply {
repeatMode = Player.REPEAT_MODE_ALL
videoScalingMode = C.VIDEO_SCALING_MODE_SCALE_TO_FIT
volume = defaultVolume
if (videoUri.scheme?.startsWith("file") == true) {
if (videoUri.startsWith("file") == true) {
setMediaItem(media)
} else {
setMediaSource(
@ -149,14 +128,14 @@ fun VideoView(videoUri: Uri, description: String? = null, thumb: Drawable? = nul
prepare()
}
RenderVideoPlayer(it, context, thumb, onDialog)
RenderVideoPlayer(it, thumb, onDialog)
}
DisposableEffect(Unit) {
val observer = LifecycleEventObserver { _, event ->
when (event) {
Lifecycle.Event.ON_PAUSE -> {
exoPlayer?.pause()
exoPlayerData?.exoPlayer?.pause()
}
else -> {}
}
@ -165,19 +144,30 @@ fun VideoView(videoUri: Uri, description: String? = null, thumb: Drawable? = nul
lifecycle.addObserver(observer)
onDispose {
exoPlayer?.release()
exoPlayerData?.exoPlayer?.release()
lifecycle.removeObserver(observer)
}
}
}
@Stable
data class VideoPlayer(
val exoPlayer: ExoPlayer
)
@Stable
data class VideoThumb(
val thumb: Drawable?
)
@Composable
private fun RenderVideoPlayer(
exoPlayer: ExoPlayer,
context: Context,
thumb: Drawable?,
playerData: VideoPlayer,
thumbData: VideoThumb?,
onDialog: ((Boolean) -> Unit)?
) {
val context = LocalContext.current
BoxWithConstraints() {
AndroidView(
modifier = Modifier
@ -185,27 +175,27 @@ private fun RenderVideoPlayer(
.defaultMinSize(minHeight = 70.dp)
.align(Alignment.Center)
.onVisibilityChanges { visible ->
if (visible && !exoPlayer.isPlaying) {
exoPlayer.play()
} else if (!visible && exoPlayer.isPlaying) {
exoPlayer.pause()
if (visible && !playerData.exoPlayer.isPlaying) {
playerData.exoPlayer.play()
} else if (!visible && playerData.exoPlayer.isPlaying) {
playerData.exoPlayer.pause()
}
},
factory = {
StyledPlayerView(context).apply {
player = exoPlayer
player = playerData.exoPlayer
layoutParams = FrameLayout.LayoutParams(
ViewGroup.LayoutParams.MATCH_PARENT,
ViewGroup.LayoutParams.WRAP_CONTENT
)
controllerAutoShow = false
thumb?.let { defaultArtwork = thumb }
thumbData?.thumb?.let { defaultArtwork = it }
hideController()
resizeMode =
if (maxHeight.isFinite) AspectRatioFrameLayout.RESIZE_MODE_FIT else AspectRatioFrameLayout.RESIZE_MODE_FIXED_WIDTH
onDialog?.let { innerOnDialog ->
setFullscreenButtonClickListener {
exoPlayer.pause()
playerData.exoPlayer.pause()
innerOnDialog(it)
}
}
@ -216,7 +206,7 @@ private fun RenderVideoPlayer(
MuteButton() { mute: Boolean ->
DefaultMutedSetting.value = mute
exoPlayer.volume = if (mute) 0f else 1f
playerData.exoPlayer.volume = if (mute) 0f else 1f
}
}
}

View File

@ -64,6 +64,7 @@ import androidx.compose.ui.unit.isFinite
import androidx.compose.ui.unit.sp
import androidx.compose.ui.window.Dialog
import androidx.compose.ui.window.DialogProperties
import androidx.core.net.toUri
import coil.annotation.ExperimentalCoilApi
import coil.compose.AsyncImage
import coil.imageLoader
@ -75,6 +76,8 @@ import com.vitorpamplona.amethyst.ui.actions.LoadingAnimation
import com.vitorpamplona.amethyst.ui.actions.SaveToGallery
import com.vitorpamplona.amethyst.ui.note.BlankNote
import com.vitorpamplona.amethyst.ui.theme.Nip05
import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.toImmutableList
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
@ -164,7 +167,7 @@ fun figureOutMimeType(fullUrl: String): ZoomableContent {
@Composable
@OptIn(ExperimentalFoundationApi::class)
fun ZoomableContentView(content: ZoomableContent, images: List<ZoomableContent> = listOf(content)) {
fun ZoomableContentView(content: ZoomableContent, images: ImmutableList<ZoomableContent> = listOf(content).toImmutableList()) {
val clipboardManager = LocalClipboardManager.current
// store the dialog open or close state
@ -201,7 +204,10 @@ fun ZoomableContentView(content: ZoomableContent, images: List<ZoomableContent>
is ZoomableUrlImage -> UrlImageView(content, mainImageModifier)
is ZoomableUrlVideo -> VideoView(content.url, content.description) { dialogOpen = true }
is ZoomableLocalImage -> LocalImageView(content, mainImageModifier)
is ZoomableLocalVideo -> VideoView(content.localFile, content.description) { dialogOpen = true }
is ZoomableLocalVideo ->
content.localFile?.let {
VideoView(it.toUri().toString(), content.description) { dialogOpen = true }
}
}
if (dialogOpen) {
@ -442,7 +448,7 @@ private fun DisplayBlurHash(
@OptIn(ExperimentalFoundationApi::class)
@Composable
fun ZoomableImageDialog(imageUrl: ZoomableContent, allImages: List<ZoomableContent> = listOf(imageUrl), onDismiss: () -> Unit) {
fun ZoomableImageDialog(imageUrl: ZoomableContent, allImages: ImmutableList<ZoomableContent> = listOf(imageUrl).toImmutableList(), onDismiss: () -> Unit) {
Dialog(
onDismissRequest = onDismiss,
properties = DialogProperties(usePlatformDefaultWidth = false)
@ -507,7 +513,9 @@ fun RenderImageOrVideo(content: ZoomableContent) {
LocalImageView(content = content, mainImageModifier = mainModifier)
} else if (content is ZoomableLocalVideo) {
Row(verticalAlignment = Alignment.CenterVertically, modifier = Modifier.fillMaxSize(1f)) {
VideoView(content.localFile, content.description)
content.localFile?.let {
VideoView(it.toUri().toString(), content.description)
}
}
}
}

View File

@ -50,6 +50,7 @@ import com.vitorpamplona.amethyst.model.LocalCache
import com.vitorpamplona.amethyst.model.User
import com.vitorpamplona.amethyst.model.decodePublicKey
import com.vitorpamplona.amethyst.model.toHexKey
import com.vitorpamplona.amethyst.ui.actions.toImmutableListOfLists
import com.vitorpamplona.amethyst.ui.components.CreateTextWithEmoji
import com.vitorpamplona.amethyst.ui.components.ResizeImage
import com.vitorpamplona.amethyst.ui.components.RobohashAsyncImageProxy
@ -57,7 +58,6 @@ import com.vitorpamplona.amethyst.ui.note.toShortenHex
import com.vitorpamplona.amethyst.ui.screen.AccountStateViewModel
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
import com.vitorpamplona.amethyst.ui.screen.loggedOff.LoginPage
import kotlinx.collections.immutable.toImmutableList
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
@ -228,7 +228,7 @@ private fun AccountName(
}
val tags by remember(userState) {
derivedStateOf {
user.info?.latestMetadata?.tags?.toImmutableList()
user.info?.latestMetadata?.tags?.toImmutableListOfLists()
}
}

View File

@ -21,6 +21,7 @@ import androidx.compose.runtime.Composable
import androidx.compose.runtime.DisposableEffect
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.State
import androidx.compose.runtime.derivedStateOf
import androidx.compose.runtime.getValue
import androidx.compose.runtime.livedata.observeAsState
import androidx.compose.runtime.mutableStateOf
@ -37,8 +38,7 @@ import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.navigation.NavHostController
import androidx.navigation.compose.currentBackStackEntryAsState
import androidx.navigation.NavBackStackEntry
import com.vitorpamplona.amethyst.NotificationCache
import com.vitorpamplona.amethyst.model.LocalCache
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
@ -84,7 +84,7 @@ fun keyboardAsState(): State<Keyboard> {
}
@Composable
fun AppBottomBar(navController: NavHostController, accountViewModel: AccountViewModel) {
fun AppBottomBar(accountViewModel: AccountViewModel, navEntryState: State<NavBackStackEntry?>, nav: (Route, Boolean) -> Unit) {
val isKeyboardOpen by keyboardAsState()
if (isKeyboardOpen == Keyboard.Closed) {
Column() {
@ -97,7 +97,7 @@ fun AppBottomBar(navController: NavHostController, accountViewModel: AccountView
backgroundColor = MaterialTheme.colors.background
) {
bottomNavigationItems.forEach { item ->
HasNewItemsIcon(item, accountViewModel, navController)
HasNewItemsIcon(item, accountViewModel, navEntryState, nav)
}
}
}
@ -108,7 +108,8 @@ fun AppBottomBar(navController: NavHostController, accountViewModel: AccountView
private fun RowScope.HasNewItemsIcon(
route: Route,
accountViewModel: AccountViewModel,
navController: NavHostController
navEntryState: State<NavBackStackEntry?>,
nav: (Route, Boolean) -> Unit
) {
var hasNewItems by remember { mutableStateOf(false) }
@ -126,23 +127,10 @@ private fun RowScope.HasNewItemsIcon(
iconSize = if ("Home" == route.base) 24.dp else 20.dp,
base = route.base,
hasNewItems = hasNewItems,
navController
navEntryState = navEntryState
) { selected ->
scope.launch {
if (!selected) {
navController.navigate(route.base) {
popUpTo(Route.Home.route)
launchSingleTop = true
restoreState = true
}
} else {
val newRoute = route.route.replace("{scrollToTop}", "true")
navController.navigate(newRoute) {
popUpTo(Route.Home.route)
launchSingleTop = true
restoreState = true
}
}
nav(route, selected)
}
}
}
@ -183,30 +171,28 @@ private fun RowScope.BottomIcon(
iconSize: Dp,
base: String,
hasNewItems: Boolean,
navController: NavHostController,
navEntryState: State<NavBackStackEntry?>,
onClick: (Boolean) -> Unit
) {
val navBackStackEntry by navController.currentBackStackEntryAsState()
navBackStackEntry?.let {
val selected = remember(it) {
it.destination.route?.substringBefore("?") == base
val selected by remember(navEntryState.value) {
derivedStateOf {
navEntryState.value?.destination?.route?.substringBefore("?") == base
}
BottomNavigationItem(
icon = {
NotifiableIcon(
icon,
size,
iconSize,
selected,
hasNewItems
)
},
selected = selected,
onClick = { onClick(selected) }
)
}
BottomNavigationItem(
icon = {
NotifiableIcon(
icon,
size,
iconSize,
selected,
hasNewItems
)
},
selected = selected,
onClick = { onClick(selected) }
)
}
@Composable

View File

@ -27,6 +27,7 @@ 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.State
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.derivedStateOf
import androidx.compose.runtime.getValue
@ -47,6 +48,7 @@ import androidx.lifecycle.ViewModel
import androidx.lifecycle.ViewModelProvider
import androidx.lifecycle.viewModelScope
import androidx.lifecycle.viewmodel.compose.viewModel
import androidx.navigation.NavBackStackEntry
import androidx.navigation.NavHostController
import coil.Coil
import com.vitorpamplona.amethyst.R
@ -89,8 +91,19 @@ import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.launch
@Composable
fun AppTopBar(followLists: FollowListViewModel, navController: NavHostController, scaffoldState: ScaffoldState, accountViewModel: AccountViewModel) {
when (currentRoute(navController)?.substringBefore("?")) {
fun AppTopBar(
followLists: FollowListViewModel,
navEntryState: State<NavBackStackEntry?>,
scaffoldState: ScaffoldState,
accountViewModel: AccountViewModel
) {
val currentRoute by remember(navEntryState.value) {
derivedStateOf {
navEntryState?.value?.destination?.route?.substringBefore("?")
}
}
when (currentRoute) {
// Route.Profile.route -> TopBarWithBackButton(nav)
Route.Home.base -> HomeTopBar(followLists, scaffoldState, accountViewModel)
Route.Video.base -> StoriesTopBar(followLists, scaffoldState, accountViewModel)
@ -320,15 +333,15 @@ fun FollowList(followListsModel: FollowListViewModel, listName: String, withGlob
(defaultOptions + followLists)
}
val followNames = remember(followLists) {
val followNames by remember(followLists) {
derivedStateOf {
allLists.map { it.second }
allLists.map { it.second }.toImmutableList()
}
}
SimpleTextSpinner(
placeholder = allLists.firstOrNull { it.first == listName }?.second ?: "Select an Option",
options = followNames.value,
options = followNames,
onSelect = {
onChange(allLists.getOrNull(it)?.first ?: KIND3_FOLLOWS)
}
@ -396,8 +409,8 @@ class FollowListViewModel(val account: Account) : ViewModel() {
@Composable
fun SimpleTextSpinner(
placeholder: String,
options: List<String>,
explainers: List<String>? = null,
options: ImmutableList<String>,
explainers: ImmutableList<String>? = null,
onSelect: (Int) -> Unit,
modifier: Modifier = Modifier
) {

View File

@ -60,13 +60,13 @@ import com.vitorpamplona.amethyst.ServiceManager
import com.vitorpamplona.amethyst.model.Account
import com.vitorpamplona.amethyst.model.User
import com.vitorpamplona.amethyst.service.HttpClient
import com.vitorpamplona.amethyst.ui.actions.toImmutableListOfLists
import com.vitorpamplona.amethyst.ui.components.CreateTextWithEmoji
import com.vitorpamplona.amethyst.ui.components.ResizeImage
import com.vitorpamplona.amethyst.ui.components.RobohashAsyncImageProxy
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountBackupDialog
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
import com.vitorpamplona.amethyst.ui.screen.loggedIn.ConnectOrbotDialog
import kotlinx.collections.immutable.toImmutableList
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
@ -129,7 +129,7 @@ fun ProfileContent(
val profilePicture = remember(accountUserState) { accountUserState?.user?.profilePicture()?.ifBlank { null }?.let { ResizeImage(it, 100.dp) } }
val bestUserName = remember(accountUserState) { accountUserState?.user?.bestUsername() }
val bestDisplayName = remember(accountUserState) { accountUserState?.user?.bestDisplayName() }
val tags = remember(accountUserState) { accountUserState?.user?.info?.latestMetadata?.tags?.toImmutableList() }
val tags = remember(accountUserState) { accountUserState?.user?.info?.latestMetadata?.tags?.toImmutableListOfLists() }
val route = remember(accountUserState) { "User/${accountUserState?.user?.pubkeyHex}" }
Box {

View File

@ -2,6 +2,7 @@ package com.vitorpamplona.amethyst.ui.navigation
import android.os.Bundle
import androidx.compose.runtime.Composable
import androidx.compose.runtime.Immutable
import androidx.compose.runtime.getValue
import androidx.navigation.NamedNavArgument
import androidx.navigation.NavDestination
@ -18,12 +19,16 @@ import com.vitorpamplona.amethyst.ui.dal.AdditiveFeedFilter
import com.vitorpamplona.amethyst.ui.dal.ChatroomListKnownFeedFilter
import com.vitorpamplona.amethyst.ui.dal.HomeNewThreadFeedFilter
import com.vitorpamplona.amethyst.ui.dal.NotificationFeedFilter
import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.persistentListOf
import kotlinx.collections.immutable.toImmutableList
@Immutable
sealed class Route(
val route: String,
val icon: Int,
val hasNewItems: (Account, NotificationCache, Set<com.vitorpamplona.amethyst.model.Note>) -> Boolean = { _, _, _ -> false },
val arguments: List<NamedNavArgument> = emptyList()
val arguments: ImmutableList<NamedNavArgument> = persistentListOf()
) {
val base: String
get() = route.substringBefore("?")
@ -34,26 +39,26 @@ sealed class Route(
arguments = listOf(
navArgument("scrollToTop") { type = NavType.BoolType; defaultValue = false },
navArgument("nip47") { type = NavType.StringType; nullable = true; defaultValue = null }
),
).toImmutableList(),
hasNewItems = { accountViewModel, cache, newNotes -> HomeLatestItem.hasNewItems(accountViewModel, cache, newNotes) }
)
object Search : Route(
route = "Search?scrollToTop={scrollToTop}",
icon = R.drawable.ic_globe,
arguments = listOf(navArgument("scrollToTop") { type = NavType.BoolType; defaultValue = false })
arguments = listOf(navArgument("scrollToTop") { type = NavType.BoolType; defaultValue = false }).toImmutableList()
)
object Video : Route(
route = "Video?scrollToTop={scrollToTop}",
icon = R.drawable.ic_video,
arguments = listOf(navArgument("scrollToTop") { type = NavType.BoolType; defaultValue = false })
arguments = listOf(navArgument("scrollToTop") { type = NavType.BoolType; defaultValue = false }).toImmutableList()
)
object Notification : Route(
route = "Notification?scrollToTop={scrollToTop}",
icon = R.drawable.ic_notifications,
arguments = listOf(navArgument("scrollToTop") { type = NavType.BoolType; defaultValue = false }),
arguments = listOf(navArgument("scrollToTop") { type = NavType.BoolType; defaultValue = false }).toImmutableList(),
hasNewItems = { accountViewModel, cache, newNotes -> NotificationLatestItem.hasNewItems(accountViewModel, cache, newNotes) }
)
@ -76,37 +81,37 @@ sealed class Route(
object Profile : Route(
route = "User/{id}",
icon = R.drawable.ic_profile,
arguments = listOf(navArgument("id") { type = NavType.StringType })
arguments = listOf(navArgument("id") { type = NavType.StringType }).toImmutableList()
)
object Note : Route(
route = "Note/{id}",
icon = R.drawable.ic_moments,
arguments = listOf(navArgument("id") { type = NavType.StringType })
arguments = listOf(navArgument("id") { type = NavType.StringType }).toImmutableList()
)
object Hashtag : Route(
route = "Hashtag/{id}",
icon = R.drawable.ic_moments,
arguments = listOf(navArgument("id") { type = NavType.StringType })
arguments = listOf(navArgument("id") { type = NavType.StringType }).toImmutableList()
)
object Room : Route(
route = "Room/{id}",
icon = R.drawable.ic_moments,
arguments = listOf(navArgument("id") { type = NavType.StringType })
arguments = listOf(navArgument("id") { type = NavType.StringType }).toImmutableList()
)
object Channel : Route(
route = "Channel/{id}",
icon = R.drawable.ic_moments,
arguments = listOf(navArgument("id") { type = NavType.StringType })
arguments = listOf(navArgument("id") { type = NavType.StringType }).toImmutableList()
)
object Event : Route(
route = "Event/{id}",
icon = R.drawable.ic_moments,
arguments = listOf(navArgument("id") { type = NavType.StringType })
arguments = listOf(navArgument("id") { type = NavType.StringType }).toImmutableList()
)
}

View File

@ -22,6 +22,7 @@ import androidx.compose.ui.unit.dp
import com.vitorpamplona.amethyst.R
import com.vitorpamplona.amethyst.model.Note
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
import kotlinx.collections.immutable.ImmutableSet
@Composable
fun BlankNote(modifier: Modifier = Modifier, isQuote: Boolean = false, idHex: String? = null) {
@ -57,7 +58,7 @@ fun BlankNote(modifier: Modifier = Modifier, isQuote: Boolean = false, idHex: St
@OptIn(ExperimentalLayoutApi::class)
@Composable
fun HiddenNote(
reports: Set<Note>,
reports: ImmutableSet<Note>,
accountViewModel: AccountViewModel,
modifier: Modifier = Modifier,
isQuote: Boolean = false,

View File

@ -56,6 +56,8 @@ import com.vitorpamplona.amethyst.model.Note
import com.vitorpamplona.amethyst.service.model.ChannelCreateEvent
import com.vitorpamplona.amethyst.service.model.ChannelMessageEvent
import com.vitorpamplona.amethyst.service.model.ChannelMetadataEvent
import com.vitorpamplona.amethyst.ui.actions.ImmutableListOfLists
import com.vitorpamplona.amethyst.ui.actions.toImmutableListOfLists
import com.vitorpamplona.amethyst.ui.components.CreateClickableTextWithEmoji
import com.vitorpamplona.amethyst.ui.components.CreateTextWithEmoji
import com.vitorpamplona.amethyst.ui.components.ResizeImage
@ -65,7 +67,7 @@ import com.vitorpamplona.amethyst.ui.components.SensitivityWarning
import com.vitorpamplona.amethyst.ui.components.TranslatableRichTextViewer
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
import com.vitorpamplona.amethyst.ui.theme.RelayIconFilter
import kotlinx.collections.immutable.toImmutableList
import kotlinx.collections.immutable.toImmutableSet
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
@ -132,8 +134,11 @@ fun ChatroomMessageCompose(
}
if (!isAcceptableAndCanPreview.first && !showHiddenNote) {
val reports = remember {
account.getRelevantReports(noteForReports).toImmutableSet()
}
HiddenNote(
account.getRelevantReports(noteForReports),
reports,
accountViewModel,
Modifier,
innerQuote,
@ -363,7 +368,7 @@ private fun RenderRegularTextNote(
accountViewModel: AccountViewModel,
nav: (String) -> Unit
) {
val tags = remember(note.event) { note.event?.tags()?.toImmutableList() ?: emptyList<List<String>>().toImmutableList() }
val tags = remember(note.event) { note.event?.tags()?.toImmutableListOfLists() ?: ImmutableListOfLists() }
val eventContent = remember { accountViewModel.decrypt(note) }
val modifier = remember { Modifier.padding(top = 5.dp) }
@ -418,7 +423,7 @@ private fun RenderChangeChannelMetadataNote(
CreateTextWithEmoji(
text = text,
tags = remember { note.author?.info?.latestMetadata?.tags?.toImmutableList() }
tags = remember { note.author?.info?.latestMetadata?.tags?.toImmutableListOfLists() }
)
}
@ -441,7 +446,7 @@ private fun RenderCreateChannelNote(note: Note) {
CreateTextWithEmoji(
text = text,
tags = remember { note.author?.info?.latestMetadata?.tags?.toImmutableList() }
tags = remember { note.author?.info?.latestMetadata?.tags?.toImmutableListOfLists() }
)
}
@ -457,7 +462,7 @@ private fun DrawAuthorInfo(
val route = remember { "User/$pubkeyHex" }
val userDisplayName = remember(userState) { userState?.user?.toBestDisplayName() }
val userProfilePicture = remember(userState) { ResizeImage(userState?.user?.profilePicture(), 25.dp) }
val userTags = remember(userState) { userState?.user?.info?.latestMetadata?.tags?.toImmutableList() }
val userTags = remember(userState) { userState?.user?.info?.latestMetadata?.tags?.toImmutableListOfLists() }
Row(
verticalAlignment = Alignment.CenterVertically,

View File

@ -47,6 +47,7 @@ import com.vitorpamplona.amethyst.model.Note
import com.vitorpamplona.amethyst.model.User
import com.vitorpamplona.amethyst.service.model.LnZapEvent
import com.vitorpamplona.amethyst.service.model.LnZapRequestEvent
import com.vitorpamplona.amethyst.ui.actions.ImmutableListOfLists
import com.vitorpamplona.amethyst.ui.components.TranslatableRichTextViewer
import com.vitorpamplona.amethyst.ui.screen.MultiSetCard
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
@ -55,7 +56,6 @@ import com.vitorpamplona.amethyst.ui.theme.BitcoinOrange
import com.vitorpamplona.amethyst.ui.theme.newItemBackgroundColor
import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.ImmutableMap
import kotlinx.collections.immutable.persistentListOf
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
@ -362,7 +362,7 @@ private fun AuthorPictureAndComment(
TranslatableRichTextViewer(
content = it,
canPreview = true,
tags = remember { persistentListOf() },
tags = remember { ImmutableListOfLists() },
modifier = remember { Modifier.fillMaxWidth() },
backgroundColor = backgroundColor,
accountViewModel = accountViewModel,

View File

@ -112,6 +112,8 @@ import com.vitorpamplona.amethyst.service.model.ReactionEvent
import com.vitorpamplona.amethyst.service.model.ReportEvent
import com.vitorpamplona.amethyst.service.model.RepostEvent
import com.vitorpamplona.amethyst.service.model.TextNoteEvent
import com.vitorpamplona.amethyst.ui.actions.ImmutableListOfLists
import com.vitorpamplona.amethyst.ui.actions.toImmutableListOfLists
import com.vitorpamplona.amethyst.ui.components.ClickableUrl
import com.vitorpamplona.amethyst.ui.components.CreateClickableTextWithEmoji
import com.vitorpamplona.amethyst.ui.components.CreateTextWithEmoji
@ -139,8 +141,11 @@ import com.vitorpamplona.amethyst.ui.theme.BitcoinOrange
import com.vitorpamplona.amethyst.ui.theme.Following
import com.vitorpamplona.amethyst.ui.theme.newItemBackgroundColor
import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.ImmutableSet
import kotlinx.collections.immutable.persistentListOf
import kotlinx.collections.immutable.persistentSetOf
import kotlinx.collections.immutable.toImmutableList
import kotlinx.collections.immutable.toImmutableSet
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
@ -245,7 +250,7 @@ fun CheckHiddenNoteCompose(
data class NoteComposeReportState(
val isAcceptable: Boolean,
val canPreview: Boolean,
val relevantReports: Set<Note>
val relevantReports: ImmutableSet<Note>
)
@Composable
@ -267,14 +272,14 @@ fun LoadedNoteCompose(
NoteComposeReportState(
isAcceptable = true,
canPreview = true,
relevantReports = emptySet()
relevantReports = persistentSetOf()
)
)
}
WatchForReports(note, accountViewModel) { newIsAcceptable, newCanPreview, newRelevantReports ->
if (newIsAcceptable != state.isAcceptable || newCanPreview != state.canPreview) {
state = NoteComposeReportState(newIsAcceptable, newCanPreview, newRelevantReports)
state = NoteComposeReportState(newIsAcceptable, newCanPreview, newRelevantReports.toImmutableSet())
}
}
@ -596,7 +601,7 @@ private fun RenderTextEvent(
accountViewModel = accountViewModel
) {
val modifier = remember(note) { Modifier.fillMaxWidth() }
val tags = remember(note) { note.event?.tags()?.toImmutableList() ?: emptyList<List<String>>().toImmutableList() }
val tags = remember(note) { note.event?.tags()?.toImmutableListOfLists() ?: ImmutableListOfLists() }
TranslatableRichTextViewer(
content = eventContent,
@ -609,7 +614,7 @@ private fun RenderTextEvent(
)
}
val hashtags = remember(note.event) { note.event?.hashtags() ?: emptyList() }
val hashtags = remember(note.event) { note.event?.hashtags()?.toImmutableList() ?: persistentListOf() }
DisplayUncitedHashtags(hashtags, eventContent, nav)
}
}
@ -646,7 +651,7 @@ private fun RenderPoll(
} else {
val hasSensitiveContent = remember(note.event) { note.event?.isSensitive() ?: false }
val tags = remember(note) { note.event?.tags()?.toImmutableList() ?: emptyList<List<String>>().toImmutableList() }
val tags = remember(note) { note.event?.tags()?.toImmutableListOfLists() ?: ImmutableListOfLists() }
SensitivityWarning(
hasSensitiveContent = hasSensitiveContent,
@ -671,7 +676,7 @@ private fun RenderPoll(
)
}
var hashtags = remember { noteEvent.hashtags() }
var hashtags = remember { noteEvent.hashtags().toImmutableList() }
DisplayUncitedHashtags(hashtags, eventContent, nav)
}
@ -797,7 +802,7 @@ fun RenderAppDefinition(
Row(verticalAlignment = Alignment.Bottom, modifier = Modifier.padding(top = 7.dp)) {
CreateTextWithEmoji(
text = it,
tags = remember { (note.event?.tags() ?: emptyList()).toImmutableList() },
tags = remember { (note.event?.tags() ?: emptyList()).toImmutableListOfLists() },
fontWeight = FontWeight.Bold,
fontSize = 25.sp
)
@ -827,7 +832,7 @@ fun RenderAppDefinition(
Row(
modifier = Modifier.padding(top = 5.dp, bottom = 5.dp)
) {
val tags = remember(note) { note.event?.tags()?.toImmutableList() ?: emptyList<List<String>>().toImmutableList() }
val tags = remember(note) { note.event?.tags()?.toImmutableListOfLists() ?: ImmutableListOfLists() }
TranslatableRichTextViewer(
content = it,
canPreview = false,
@ -891,11 +896,11 @@ private fun RenderPrivateMessage(
if (withMe) {
val eventContent = remember { accountViewModel.decrypt(note) }
val hashtags = remember(note.event?.id()) { note.event?.hashtags() ?: emptyList() }
val hashtags = remember(note.event?.id()) { note.event?.hashtags()?.toImmutableList() ?: persistentListOf() }
val modifier = remember(note.event?.id()) { Modifier.fillMaxWidth() }
val isAuthorTheLoggedUser = remember(note.event?.id()) { accountViewModel.isLoggedUser(note.author) }
val tags = remember(note) { note.event?.tags()?.toImmutableList() ?: emptyList<List<String>>().toImmutableList() }
val tags = remember(note) { note.event?.tags()?.toImmutableListOfLists() ?: ImmutableListOfLists() }
if (eventContent != null) {
if (makeItShort && isAuthorTheLoggedUser) {
@ -936,7 +941,7 @@ private fun RenderPrivateMessage(
),
canPreview = !makeItShort,
Modifier.fillMaxWidth(),
persistentListOf(),
ImmutableListOfLists(),
backgroundColor,
accountViewModel,
nav
@ -1248,7 +1253,7 @@ fun PinListHeader(
TranslatableRichTextViewer(
content = pin,
canPreview = true,
tags = remember { persistentListOf() },
tags = remember { ImmutableListOfLists() },
backgroundColor = backgroundColor,
accountViewModel = accountViewModel,
nav = nav
@ -1358,7 +1363,7 @@ private fun RenderReport(
content = content,
canPreview = true,
modifier = remember { Modifier },
tags = remember { persistentListOf() },
tags = remember { ImmutableListOfLists() },
backgroundColor = backgroundColor,
accountViewModel = accountViewModel,
nav = nav
@ -1433,8 +1438,12 @@ private fun ReplyRow(
Spacer(modifier = Modifier.height(5.dp))
} else if (noteEvent is ChannelMessageEvent && (note.replyTo != null || noteEvent.hasAnyTaggedUser())) {
note.channelHex()?.let {
ReplyInformationChannel(note.replyTo, noteEvent.mentions(), it, accountViewModel, nav)
val channelHex = note.channelHex()
channelHex?.let {
val replies = remember { note.replyTo?.toImmutableList() }
val mentions = remember { noteEvent.mentions().toImmutableList() }
ReplyInformationChannel(replies, mentions, it, accountViewModel, nav)
}
Spacer(modifier = Modifier.height(5.dp))
@ -1453,7 +1462,7 @@ private fun SecondUserInfoRow(
Row(verticalAlignment = Alignment.CenterVertically) {
ObserveDisplayNip05Status(noteAuthor, remember { Modifier.weight(1f) })
val baseReward = remember { noteEvent.getReward() }
val baseReward = remember { noteEvent.getReward()?.let { Reward(it) } }
if (baseReward != null) {
DisplayReward(baseReward, note, accountViewModel, nav)
}
@ -1695,8 +1704,8 @@ fun DisplayHighlight(
TranslatableRichTextViewer(
quote,
canPreview = canPreview && !makeItShort,
Modifier.fillMaxWidth(),
persistentListOf(),
remember { Modifier.fillMaxWidth() },
remember { ImmutableListOfLists<String>(emptyList()) },
backgroundColor,
accountViewModel,
nav
@ -1718,7 +1727,7 @@ fun DisplayHighlight(
val userState by userBase.live().metadata.observeAsState()
val route = remember { "User/${userBase.pubkeyHex}" }
val userDisplayName = remember(userState) { userState?.user?.toBestDisplayName() }
val userTags = remember(userState) { userState?.user?.info?.latestMetadata?.tags?.toImmutableList() }
val userTags = remember(userState) { userState?.user?.info?.latestMetadata?.tags?.toImmutableListOfLists() }
if (userDisplayName != null) {
CreateClickableTextWithEmoji(
@ -1793,7 +1802,7 @@ fun DisplayFollowingHashtagsInPost(
@OptIn(ExperimentalLayoutApi::class)
@Composable
fun DisplayUncitedHashtags(
hashtags: List<String>,
hashtags: ImmutableList<String>,
eventContent: String,
nav: (String) -> Unit
) {
@ -1832,9 +1841,12 @@ fun DisplayPoW(
)
}
@Stable
data class Reward(val amount: BigDecimal)
@Composable
fun DisplayReward(
baseReward: BigDecimal,
baseReward: Reward,
baseNote: Note,
accountViewModel: AccountViewModel,
nav: (String) -> Unit
@ -1870,13 +1882,13 @@ fun DisplayReward(
@Composable
private fun RenderPledgeAmount(
baseNote: Note,
baseReward: BigDecimal,
baseReward: Reward,
accountViewModel: AccountViewModel
) {
val repliesState by baseNote.live().replies.observeAsState()
var rewardAmount by remember {
mutableStateOf<BigDecimal?>(
baseReward
var reward by remember {
mutableStateOf<String>(
showAmount(baseReward.amount)
)
}
@ -1889,9 +1901,9 @@ private fun RenderPledgeAmount(
LaunchedEffect(key1 = repliesState) {
launch(Dispatchers.IO) {
repliesState?.note?.pledgedAmountByOthers()?.let {
val newRewardAmount = baseReward.add(it)
if (newRewardAmount != rewardAmount) {
rewardAmount = newRewardAmount
val newRewardAmount = showAmount(baseReward.amount.add(it))
if (newRewardAmount != reward) {
reward = newRewardAmount
}
}
val newHasPledge = repliesState?.note?.hasPledgeBy(accountViewModel.userProfile()) == true
@ -1918,7 +1930,7 @@ private fun RenderPledgeAmount(
}
Text(
showAmount(rewardAmount),
text = reward,
color = MaterialTheme.colors.onSurface.copy(alpha = 0.32f)
)
}
@ -2052,7 +2064,7 @@ fun FileHeaderDisplay(note: Note) {
}
content?.let {
ZoomableContentView(content = it, listOf(it))
ZoomableContentView(content = it)
}
}
@ -2110,7 +2122,7 @@ fun FileStorageHeaderDisplay(baseNote: Note) {
}
content?.let {
ZoomableContentView(content = it, listOf(it))
ZoomableContentView(content = it)
}
}
}
@ -2289,14 +2301,14 @@ private fun CreateImageHeader(
private fun RelayBadges(baseNote: Note) {
var expanded by remember { mutableStateOf(false) }
var showShowMore by remember { mutableStateOf(false) }
var lazyRelayList by remember { mutableStateOf(emptyList<String>()) }
var lazyRelayList by remember { mutableStateOf<ImmutableList<String>>(persistentListOf()) }
WatchRelayLists(baseNote) { relayList ->
val relaysToDisplay = if (expanded) relayList else relayList.take(3)
val shouldListChange = lazyRelayList.size < 3 || lazyRelayList.size != relayList.size
if (shouldListChange) {
lazyRelayList = relaysToDisplay
lazyRelayList = relaysToDisplay.toImmutableList()
}
val nextShowMore = relayList.size > 3 && !expanded
@ -2336,7 +2348,7 @@ private fun WatchRelayLists(baseNote: Note, onListChanges: (ImmutableList<String
@Composable
@Stable
private fun VerticalRelayPanelWithFlow(
relays: List<String>
relays: ImmutableList<String>
) {
// FlowRow Seems to be a lot faster than LazyVerticalGrid
FlowRow() {

View File

@ -27,11 +27,11 @@ import androidx.lifecycle.viewmodel.compose.viewModel
import com.vitorpamplona.amethyst.R
import com.vitorpamplona.amethyst.model.Note
import com.vitorpamplona.amethyst.service.model.LnZapEvent
import com.vitorpamplona.amethyst.ui.actions.ImmutableListOfLists
import com.vitorpamplona.amethyst.ui.actions.toImmutableListOfLists
import com.vitorpamplona.amethyst.ui.components.TranslatableRichTextViewer
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
import com.vitorpamplona.amethyst.ui.theme.BitcoinOrange
import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.toImmutableList
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import java.util.*
@ -111,7 +111,9 @@ private fun OptionNote(
backgroundColor: Color,
nav: (String) -> Unit
) {
val tags = remember(baseNote) { baseNote.event?.tags()?.toImmutableList() ?: emptyList<List<String>>().toImmutableList() }
val tags = remember(baseNote) {
baseNote.event?.tags()?.toImmutableListOfLists() ?: ImmutableListOfLists()
}
Row(
verticalAlignment = Alignment.CenterVertically,
@ -165,7 +167,7 @@ private fun RenderOptionAfterVote(
totalRatio: Float,
color: Color,
canPreview: Boolean,
tags: ImmutableList<List<String>>,
tags: ImmutableListOfLists<String>,
backgroundColor: Color,
accountViewModel: AccountViewModel,
nav: (String) -> Unit
@ -232,7 +234,7 @@ private fun RenderOptionAfterVote(
private fun RenderOptionBeforeVote(
description: String,
canPreview: Boolean,
tags: ImmutableList<List<String>>,
tags: ImmutableListOfLists<String>,
backgroundColor: Color,
accountViewModel: AccountViewModel,
nav: (String) -> Unit

View File

@ -18,25 +18,29 @@ import androidx.compose.ui.text.AnnotatedString
import androidx.compose.ui.unit.sp
import com.vitorpamplona.amethyst.R
import com.vitorpamplona.amethyst.model.*
import com.vitorpamplona.amethyst.ui.actions.toImmutableListOfLists
import com.vitorpamplona.amethyst.ui.components.CreateClickableTextWithEmoji
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.toImmutableList
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
@Composable
fun ReplyInformation(
replyTo: List<Note>?,
mentions: List<String>,
replyTo: ImmutableList<Note>?,
mentions: ImmutableList<String>,
accountViewModel: AccountViewModel,
nav: (String) -> Unit
) {
var sortedMentions by remember { mutableStateOf<List<User>?>(null) }
var sortedMentions by remember { mutableStateOf<ImmutableList<User>?>(null) }
LaunchedEffect(Unit) {
launch(Dispatchers.IO) {
sortedMentions = mentions.mapNotNull { LocalCache.checkGetOrCreateUser(it) }
?.toSet()?.sortedBy { !accountViewModel.account.userProfile().isFollowingCached(it) }
.toSet()
.sortedBy { !accountViewModel.account.userProfile().isFollowingCached(it) }
.toImmutableList()
}
}
@ -50,8 +54,8 @@ fun ReplyInformation(
@OptIn(ExperimentalLayoutApi::class)
@Composable
private fun ReplyInformation(
replyTo: List<Note>?,
sortedMentions: List<User>?,
replyTo: ImmutableList<Note>?,
sortedMentions: ImmutableList<User>?,
prefix: String = "",
onUserTagClick: (User) -> Unit
) {
@ -120,13 +124,13 @@ private fun ReplyInformation(
@Composable
fun ReplyInformationChannel(
replyTo: List<Note>?,
mentions: List<String>,
replyTo: ImmutableList<Note>?,
mentions: ImmutableList<String>,
channelHex: String,
accountViewModel: AccountViewModel,
nav: (String) -> Unit
) {
var sortedMentions by remember { mutableStateOf<List<User>?>(null) }
var sortedMentions by remember { mutableStateOf<ImmutableList<User>?>(null) }
LaunchedEffect(Unit) {
launch(Dispatchers.IO) {
@ -134,6 +138,7 @@ fun ReplyInformationChannel(
.mapNotNull { LocalCache.checkGetOrCreateUser(it) }
.toSet()
.sortedBy { accountViewModel.account.isFollowing(it) }
.toImmutableList()
}
}
@ -155,7 +160,7 @@ fun ReplyInformationChannel(
}
@Composable
fun ReplyInformationChannel(replyTo: List<Note>?, mentions: List<User>?, channel: Channel, nav: (String) -> Unit) {
fun ReplyInformationChannel(replyTo: ImmutableList<Note>?, mentions: ImmutableList<User>?, channel: Channel, nav: (String) -> Unit) {
ReplyInformationChannel(
replyTo,
mentions,
@ -172,8 +177,8 @@ fun ReplyInformationChannel(replyTo: List<Note>?, mentions: List<User>?, channel
@OptIn(ExperimentalLayoutApi::class)
@Composable
fun ReplyInformationChannel(
replyTo: List<Note>?,
mentions: List<User>?,
replyTo: ImmutableList<Note>?,
mentions: ImmutableList<User>?,
baseChannel: Channel,
prefix: String = "",
onUserTagClick: (User) -> Unit,
@ -235,7 +240,7 @@ private fun ReplyInfoMention(
CreateClickableTextWithEmoji(
clickablePart = remember(innerUserState) { "$prefix${innerUserState?.user?.toBestDisplayName()}" },
tags = remember(innerUserState) { innerUserState?.user?.info?.latestMetadata?.tags?.toImmutableList() },
tags = remember(innerUserState) { innerUserState?.user?.info?.latestMetadata?.tags?.toImmutableListOfLists() },
style = LocalTextStyle.current.copy(
color = MaterialTheme.colors.primary.copy(alpha = 0.52f),
fontSize = 13.sp

View File

@ -78,6 +78,7 @@ 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.collections.immutable.toImmutableList
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.launch
import androidx.compose.runtime.rememberCoroutineScope as rememberCoroutineScope
@ -212,8 +213,8 @@ fun UpdateZapAmountDialog(onClose: () -> Unit, nip47uri: String? = null, account
Triple(LnZapEvent.ZapType.NONZAP, stringResource(id = R.string.zap_type_nonzap), stringResource(id = R.string.zap_type_nonzap_explainer))
)
val zapOptions = zapTypes.map { it.second }
val zapOptionExplainers = zapTypes.map { it.third }
val zapOptions = remember { zapTypes.map { it.second }.toImmutableList() }
val zapOptionExplainers = remember { zapTypes.map { it.third }.toImmutableList() }
LaunchedEffect(accountViewModel) {
postViewModel.load()

View File

@ -83,28 +83,48 @@ fun UserReactionsRow(
)
}
Row(verticalAlignment = CenterVertically, modifier = Modifier.weight(1f)) {
val replies by model.replies.collectAsState()
UserReplyReaction(replies[model.today()])
Row(verticalAlignment = CenterVertically, modifier = remember { Modifier.weight(1f) }) {
UserReplyModel(model)
}
Row(verticalAlignment = CenterVertically, modifier = Modifier.weight(1f)) {
val boosts by model.boosts.collectAsState()
UserBoostReaction(boosts[model.today()])
Row(verticalAlignment = CenterVertically, modifier = remember { Modifier.weight(1f) }) {
UserBoostModel(model)
}
Row(verticalAlignment = CenterVertically, modifier = Modifier.weight(1f)) {
val reactions by model.reactions.collectAsState()
UserLikeReaction(reactions[model.today()])
Row(verticalAlignment = CenterVertically, modifier = remember { Modifier.weight(1f) }) {
UserReactionModel(model)
}
Row(verticalAlignment = CenterVertically, modifier = Modifier.weight(1f)) {
val zaps by model.zaps.collectAsState()
UserZapReaction(zaps[model.today()])
Row(verticalAlignment = CenterVertically, modifier = remember { Modifier.weight(1f) }) {
UserZapModel(model)
}
}
}
@Composable
private fun UserZapModel(model: UserReactionsViewModel) {
val zaps by model.zaps.collectAsState()
UserZapReaction(showAmountAxis(zaps[model.today()]))
}
@Composable
private fun UserReactionModel(model: UserReactionsViewModel) {
val reactions by model.reactions.collectAsState()
UserLikeReaction(reactions[model.today()])
}
@Composable
private fun UserBoostModel(model: UserReactionsViewModel) {
val boosts by model.boosts.collectAsState()
UserBoostReaction(boosts[model.today()])
}
@Composable
private fun UserReplyModel(model: UserReactionsViewModel) {
val replies by model.replies.collectAsState()
UserReplyReaction(replies[model.today()])
}
@Stable
class UserReactionsViewModel(val account: Account) : ViewModel() {
val user: User = account.userProfile()
@ -368,10 +388,8 @@ fun UserLikeReaction(
@Composable
fun UserZapReaction(
amount: BigDecimal?
amount: String
) {
val showAmounts = remember(amount) { showAmountAxis(amount) }
Icon(
imageVector = Icons.Default.Bolt,
contentDescription = stringResource(R.string.zaps),
@ -382,7 +400,7 @@ fun UserZapReaction(
Spacer(modifier = Modifier.width(8.dp))
Text(
showAmounts,
amount,
fontWeight = FontWeight.Bold,
fontSize = 18.sp
)

View File

@ -11,9 +11,9 @@ import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextOverflow
import com.vitorpamplona.amethyst.model.Note
import com.vitorpamplona.amethyst.model.User
import com.vitorpamplona.amethyst.ui.actions.ImmutableListOfLists
import com.vitorpamplona.amethyst.ui.actions.toImmutableListOfLists
import com.vitorpamplona.amethyst.ui.components.CreateTextWithEmoji
import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.toImmutableList
@Composable
fun NoteUsernameDisplay(baseNote: Note, weight: Modifier = Modifier) {
@ -31,7 +31,7 @@ fun UsernameDisplay(baseUser: User, weight: Modifier = Modifier) {
val bestUserName = remember(userState) { userState?.user?.bestUsername() }
val bestDisplayName = remember(userState) { userState?.user?.bestDisplayName() }
val npubDisplay = remember { baseUser.pubkeyDisplayHex() }
val tags = remember(userState) { userState?.user?.info?.latestMetadata?.tags?.toImmutableList() }
val tags = remember(userState) { userState?.user?.info?.latestMetadata?.tags?.toImmutableListOfLists() }
UserNameDisplay(bestUserName, bestDisplayName, npubDisplay, tags, weight)
}
@ -41,7 +41,7 @@ private fun UserNameDisplay(
bestUserName: String?,
bestDisplayName: String?,
npubDisplay: String,
tags: ImmutableList<List<String>>?,
tags: ImmutableListOfLists<String>?,
modifier: Modifier
) {
if (bestUserName != null && bestDisplayName != null) {

View File

@ -26,6 +26,7 @@ import com.vitorpamplona.amethyst.service.model.LnZapEvent
import com.vitorpamplona.amethyst.ui.actions.CloseButton
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
import com.vitorpamplona.amethyst.ui.screen.loggedIn.TextSpinner
import kotlinx.collections.immutable.toImmutableList
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
@ -74,8 +75,8 @@ fun ZapCustomDialog(onClose: () -> Unit, accountViewModel: AccountViewModel, bas
Triple(LnZapEvent.ZapType.NONZAP, stringResource(id = R.string.zap_type_nonzap), stringResource(id = R.string.zap_type_nonzap_explainer))
)
val zapOptions = zapTypes.map { it.second }
val zapOptionExplainers = zapTypes.map { it.third }
val zapOptions = remember { zapTypes.map { it.second }.toImmutableList() }
val zapOptionExplainers = remember { zapTypes.map { it.third }.toImmutableList() }
var selectedZapType by remember(accountViewModel) { mutableStateOf(accountViewModel.account.defaultZapType) }
Dialog(

View File

@ -34,11 +34,11 @@ import androidx.compose.ui.window.DialogProperties
import com.vitorpamplona.amethyst.R
import com.vitorpamplona.amethyst.model.User
import com.vitorpamplona.amethyst.ui.actions.CloseButton
import com.vitorpamplona.amethyst.ui.actions.toImmutableListOfLists
import com.vitorpamplona.amethyst.ui.components.CreateTextWithEmoji
import com.vitorpamplona.amethyst.ui.components.ResizeImage
import com.vitorpamplona.amethyst.ui.components.RobohashAsyncImageProxy
import com.vitorpamplona.amethyst.ui.qrcode.NIP19QrCodeScanner
import kotlinx.collections.immutable.toImmutableList
@Composable
fun ShowQRDialog(user: User, onScan: (String) -> Unit, onClose: () -> Unit) {
@ -88,7 +88,7 @@ fun ShowQRDialog(user: User, onScan: (String) -> Unit, onClose: () -> Unit) {
Row(horizontalArrangement = Arrangement.Center, modifier = Modifier.fillMaxWidth().padding(top = 5.dp)) {
CreateTextWithEmoji(
text = user.bestDisplayName() ?: user.bestUsername() ?: "",
tags = user.info?.latestMetadata?.tags?.toImmutableList(),
tags = user.info?.latestMetadata?.tags?.toImmutableListOfLists(),
fontWeight = FontWeight.Bold,
fontSize = 18.sp
)

View File

@ -1,6 +1,7 @@
package com.vitorpamplona.amethyst.ui.screen
import android.content.Context
import androidx.compose.runtime.Stable
import androidx.lifecycle.ViewModel
import com.vitorpamplona.amethyst.LocalPreferences
import com.vitorpamplona.amethyst.ServiceManager
@ -22,6 +23,7 @@ import nostr.postr.Persona
import nostr.postr.bechToBytes
import java.util.regex.Pattern
@Stable
class AccountStateViewModel(val context: Context) : ViewModel() {
private val _accountContent = MutableStateFlow<AccountState>(AccountState.LoggedOff)
val accountContent = _accountContent.asStateFlow()

View File

@ -1,5 +1,6 @@
package com.vitorpamplona.amethyst.ui.screen
import androidx.compose.runtime.Stable
import androidx.compose.runtime.mutableStateOf
import androidx.lifecycle.ViewModel
import androidx.lifecycle.ViewModelProvider
@ -28,6 +29,7 @@ class NostrUserProfileZapsFeedViewModel(user: User) : LnZapFeedViewModel(UserPro
}
}
@Stable
open class LnZapFeedViewModel(val dataSource: FeedFilter<ZapReqResponse>) : ViewModel() {
private val _feedContent = MutableStateFlow<LnZapFeedState>(LnZapFeedState.Loading)
val feedContent = _feedContent.asStateFlow()

View File

@ -60,6 +60,8 @@ import com.vitorpamplona.amethyst.service.model.LongTextNoteEvent
import com.vitorpamplona.amethyst.service.model.PeopleListEvent
import com.vitorpamplona.amethyst.service.model.PinListEvent
import com.vitorpamplona.amethyst.service.model.PollNoteEvent
import com.vitorpamplona.amethyst.ui.actions.ImmutableListOfLists
import com.vitorpamplona.amethyst.ui.actions.toImmutableListOfLists
import com.vitorpamplona.amethyst.ui.components.ObserveDisplayNip05Status
import com.vitorpamplona.amethyst.ui.components.SensitivityWarning
import com.vitorpamplona.amethyst.ui.components.TranslatableRichTextViewer
@ -81,6 +83,7 @@ import com.vitorpamplona.amethyst.ui.note.timeAgo
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
import com.vitorpamplona.amethyst.ui.theme.newItemBackgroundColor
import kotlinx.collections.immutable.toImmutableList
import kotlinx.collections.immutable.toImmutableSet
import kotlinx.coroutines.delay
@OptIn(ExperimentalMaterialApi::class)
@ -231,8 +234,12 @@ fun NoteMaster(
if (noteEvent == null) {
BlankNote()
} else if (!account.isAcceptable(noteForReports) && !showHiddenNote) {
val reports = remember {
account.getRelevantReports(noteForReports).toImmutableSet()
}
HiddenNote(
account.getRelevantReports(noteForReports),
reports,
accountViewModel,
Modifier,
false,
@ -289,14 +296,14 @@ fun NoteMaster(
}
Row(verticalAlignment = Alignment.CenterVertically) {
ObserveDisplayNip05Status(baseNote, Modifier.weight(1f))
ObserveDisplayNip05Status(baseNote, remember { Modifier.weight(1f) })
val baseReward = noteEvent.getReward()
val baseReward = remember { noteEvent.getReward()?.let { Reward(it) } }
if (baseReward != null) {
DisplayReward(baseReward, baseNote, accountViewModel, nav)
}
val pow = noteEvent.getPoWRank()
val pow = remember { noteEvent.getPoWRank() }
if (pow > 20) {
DisplayPoW(pow)
}
@ -389,7 +396,7 @@ fun NoteMaster(
if (eventContent != null) {
val hasSensitiveContent = remember(note.event) { note.event?.isSensitive() ?: false }
val tags = remember(note) { note.event?.tags()?.toImmutableList() ?: emptyList<List<String>>().toImmutableList() }
val tags = remember(note) { note.event?.tags()?.toImmutableListOfLists() ?: ImmutableListOfLists() }
SensitivityWarning(
hasSensitiveContent = hasSensitiveContent,
@ -398,7 +405,7 @@ fun NoteMaster(
TranslatableRichTextViewer(
eventContent,
canPreview,
Modifier.fillMaxWidth(),
remember { Modifier.fillMaxWidth() },
tags,
MaterialTheme.colors.background,
accountViewModel,
@ -406,7 +413,10 @@ fun NoteMaster(
)
}
DisplayUncitedHashtags(noteEvent.hashtags(), eventContent, nav)
val hashtags = remember {
noteEvent.hashtags().toImmutableList()
}
DisplayUncitedHashtags(hashtags, eventContent, nav)
if (noteEvent is PollNoteEvent) {
PollNote(

View File

@ -1,5 +1,6 @@
package com.vitorpamplona.amethyst.ui.screen
import androidx.compose.runtime.Stable
import androidx.compose.runtime.mutableStateOf
import androidx.lifecycle.ViewModel
import androidx.lifecycle.ViewModelProvider
@ -47,6 +48,7 @@ class NostrHiddenAccountsFeedViewModel(val account: Account) : UserFeedViewModel
}
}
@Stable
open class UserFeedViewModel(val dataSource: FeedFilter<User>) : ViewModel(), InvalidatableViewModel {
private val _feedContent = MutableStateFlow<UserFeedState>(UserFeedState.Loading)
val feedContent = _feedContent.asStateFlow()

View File

@ -10,6 +10,10 @@ import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.livedata.observeAsState
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
@ -17,36 +21,66 @@ import androidx.compose.ui.unit.dp
import androidx.navigation.NavController
import com.vitorpamplona.amethyst.R
import com.vitorpamplona.amethyst.model.LocalCache
import com.vitorpamplona.amethyst.model.Note
import com.vitorpamplona.amethyst.service.model.ChannelCreateEvent
import com.vitorpamplona.amethyst.service.model.PrivateDmEvent
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
@Composable
fun LoadRedirectScreen(eventId: String?, navController: NavController) {
if (eventId == null) return
val baseNote = LocalCache.checkGetOrCreateNote(eventId) ?: return
var noteBase by remember { mutableStateOf<Note?>(null) }
val nav = remember(navController) {
{ route: String ->
navController.backQueue.removeLast()
navController.navigate(route)
}
}
LaunchedEffect(eventId) {
withContext(Dispatchers.IO) {
val newNoteBase = LocalCache.checkGetOrCreateNote(eventId)
if (newNoteBase != noteBase) {
noteBase = newNoteBase
}
}
}
noteBase?.let {
LoadRedirectScreen(
baseNote = it,
nav = nav
)
}
}
@Composable
fun LoadRedirectScreen(baseNote: Note, nav: (String) -> Unit) {
val noteState by baseNote.live().metadata.observeAsState()
val note = noteState?.note
val scope = rememberCoroutineScope()
LaunchedEffect(key1 = noteState) {
val event = note?.event
val channelHex = note?.channelHex()
scope.launch {
val note = noteState?.note
val event = note?.event
val channelHex = note?.channelHex()
if (event == null) {
// stay here, loading
} else if (event is ChannelCreateEvent) {
navController.backQueue.removeLast()
navController.navigate("Channel/${note.idHex}")
} else if (event is PrivateDmEvent) {
navController.backQueue.removeLast()
navController.navigate("Room/${note.author?.pubkeyHex}")
} else if (channelHex != null) {
navController.backQueue.removeLast()
navController.navigate("Channel/$channelHex")
} else {
navController.backQueue.removeLast()
navController.navigate("Note/${note.idHex}")
if (event == null) {
// stay here, loading
} else if (event is ChannelCreateEvent) {
nav("Channel/${note.idHex}")
} else if (event is PrivateDmEvent) {
nav("Room/${note.author?.pubkeyHex}")
} else if (channelHex != null) {
nav("Channel/$channelHex")
} else {
nav("Note/${note.idHex}")
}
}
}
@ -58,6 +92,6 @@ fun LoadRedirectScreen(eventId: String?, navController: NavController) {
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.Center
) {
Text(stringResource(R.string.looking_for_event, eventId))
Text(stringResource(R.string.looking_for_event, baseNote.idHex))
}
}

View File

@ -18,13 +18,16 @@ import androidx.compose.material.rememberDrawerState
import androidx.compose.material.rememberModalBottomSheetState
import androidx.compose.material.rememberScaffoldState
import androidx.compose.runtime.Composable
import androidx.compose.runtime.State
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.derivedStateOf
import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.ui.Modifier
import androidx.lifecycle.viewmodel.compose.viewModel
import androidx.navigation.NavHostController
import androidx.navigation.NavBackStackEntry
import androidx.navigation.compose.currentBackStackEntryAsState
import androidx.navigation.compose.rememberNavController
import com.vitorpamplona.amethyst.ui.buttons.ChannelFabColumn
import com.vitorpamplona.amethyst.ui.buttons.NewNoteButton
@ -60,7 +63,9 @@ fun MainScreen(accountViewModel: AccountViewModel, accountStateViewModel: Accoun
skipHalfExpanded = true
)
val nav = remember {
val navState = navController.currentBackStackEntryAsState()
val nav = remember(navController) {
{ route: String ->
if (getRouteWithArguments(navController) != route) {
navController.navigate(route)
@ -68,6 +73,25 @@ fun MainScreen(accountViewModel: AccountViewModel, accountStateViewModel: Accoun
}
}
val navBottomRow = remember(navController) {
{ route: Route, selected: Boolean ->
if (!selected) {
navController.navigate(route.base) {
popUpTo(Route.Home.route)
launchSingleTop = true
restoreState = true
}
} else {
val newRoute = route.route.replace("{scrollToTop}", "true")
navController.navigate(newRoute) {
popUpTo(Route.Home.route)
launchSingleTop = true
restoreState = true
}
}
}
}
val followLists: FollowListViewModel = viewModel(
key = accountViewModel.userProfile().pubkeyHex + "FollowListViewModel",
factory = FollowListViewModel.Factory(accountViewModel.account)
@ -125,10 +149,10 @@ fun MainScreen(accountViewModel: AccountViewModel, accountStateViewModel: Accoun
.background(MaterialTheme.colors.primaryVariant)
.statusBarsPadding(),
bottomBar = {
AppBottomBar(navController, accountViewModel)
AppBottomBar(accountViewModel, navState, navBottomRow)
},
topBar = {
AppTopBar(followLists, navController, scaffoldState, accountViewModel)
AppTopBar(followLists, navState, scaffoldState, accountViewModel)
},
drawerContent = {
DrawerContent(nav, scaffoldState, sheetState, accountViewModel)
@ -137,7 +161,7 @@ fun MainScreen(accountViewModel: AccountViewModel, accountStateViewModel: Accoun
}
},
floatingActionButton = {
FloatingButtons(navController, accountViewModel, accountStateViewModel, nav)
FloatingButtons(navState, accountViewModel, accountStateViewModel, nav)
},
scaffoldState = scaffoldState
) {
@ -162,7 +186,7 @@ fun MainScreen(accountViewModel: AccountViewModel, accountStateViewModel: Accoun
@Composable
fun FloatingButtons(
navController: NavHostController,
navEntryState: State<NavBackStackEntry?>,
accountViewModel: AccountViewModel,
accountStateViewModel: AccountStateViewModel,
nav: (String) -> Unit
@ -178,16 +202,27 @@ fun FloatingButtons(
// Does nothing.
}
is AccountState.LoggedIn -> {
if (currentRoute(navController)?.substringBefore("?") == Route.Home.base) {
NewNoteButton(accountViewModel, nav)
}
if (currentRoute(navController) == Route.Message.base) {
ChannelFabColumn(accountViewModel, nav)
}
if (currentRoute(navController)?.substringBefore("?") == Route.Video.base) {
NewImageButton(accountViewModel, nav)
}
WritePermissionButtons(navEntryState, accountViewModel, nav)
}
}
}
}
@Composable
private fun WritePermissionButtons(
navEntryState: State<NavBackStackEntry?>,
accountViewModel: AccountViewModel,
nav: (String) -> Unit
) {
val currentRoute by remember(navEntryState.value) {
derivedStateOf {
navEntryState.value?.destination?.route?.substringBefore("?")
}
}
when (currentRoute) {
Route.Home.base -> NewNoteButton(accountViewModel, nav)
Route.Message.base -> ChannelFabColumn(accountViewModel, nav)
Route.Video.base -> NewImageButton(accountViewModel, nav)
}
}

View File

@ -1,6 +1,5 @@
package com.vitorpamplona.amethyst.ui.screen.loggedIn
import android.content.Context
import android.content.Intent
import android.net.Uri
import android.widget.Toast
@ -65,7 +64,9 @@ import com.vitorpamplona.amethyst.service.model.IdentityClaim
import com.vitorpamplona.amethyst.service.model.PayInvoiceErrorResponse
import com.vitorpamplona.amethyst.service.model.PayInvoiceSuccessResponse
import com.vitorpamplona.amethyst.service.model.ReportEvent
import com.vitorpamplona.amethyst.ui.actions.ImmutableListOfLists
import com.vitorpamplona.amethyst.ui.actions.NewUserMetadataView
import com.vitorpamplona.amethyst.ui.actions.toImmutableListOfLists
import com.vitorpamplona.amethyst.ui.components.CreateTextWithEmoji
import com.vitorpamplona.amethyst.ui.components.DisplayNip05ProfileStatus
import com.vitorpamplona.amethyst.ui.components.InvoiceRequest
@ -96,9 +97,7 @@ import com.vitorpamplona.amethyst.ui.screen.RelayFeedView
import com.vitorpamplona.amethyst.ui.screen.RelayFeedViewModel
import com.vitorpamplona.amethyst.ui.screen.UserFeedViewModel
import com.vitorpamplona.amethyst.ui.theme.BitcoinOrange
import kotlinx.collections.immutable.persistentListOf
import kotlinx.collections.immutable.toImmutableList
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
@ -426,7 +425,6 @@ private fun ProfileHeader(
var popupExpanded by remember { mutableStateOf(false) }
var zoomImageDialogOpen by remember { mutableStateOf(false) }
val coroutineScope = rememberCoroutineScope()
val clipboardManager = LocalClipboardManager.current
Box {
@ -505,7 +503,7 @@ private fun ProfileHeader(
// No need for this button anymore
// NPubCopyButton(baseUser)
ProfileActions(baseUser, accountViewModel, coroutineScope)
ProfileActions(baseUser, accountViewModel)
}
}
@ -524,9 +522,10 @@ private fun ProfileHeader(
@Composable
private fun ProfileActions(
baseUser: User,
accountViewModel: AccountViewModel,
coroutineScope: CoroutineScope
accountViewModel: AccountViewModel
) {
val scope = rememberCoroutineScope()
val accountLocalUserState by accountViewModel.accountLiveData.observeAsState()
val account = remember(accountLocalUserState) { accountLocalUserState?.account } ?: return
@ -561,16 +560,16 @@ private fun ProfileActions(
account.showUser(baseUser.pubkeyHex)
}
} else if (isLoggedInFollowingUser) {
UnfollowButton { coroutineScope.launch(Dispatchers.IO) { account.unfollow(baseUser) } }
UnfollowButton { scope.launch(Dispatchers.IO) { account.unfollow(baseUser) } }
} else {
if (isUserFollowingLoggedIn) {
FollowButton(
{ coroutineScope.launch(Dispatchers.IO) { account.follow(baseUser) } },
{ scope.launch(Dispatchers.IO) { account.follow(baseUser) } },
R.string.follow_back
)
} else {
FollowButton(
{ coroutineScope.launch(Dispatchers.IO) { account.follow(baseUser) } },
{ scope.launch(Dispatchers.IO) { account.follow(baseUser) } },
R.string.follow
)
}
@ -586,12 +585,10 @@ private fun DrawAdditionalInfo(
) {
val userState by baseUser.live().metadata.observeAsState()
val user = remember(userState) { userState?.user } ?: return
val tags = remember(userState) { userState?.user?.info?.latestMetadata?.tags?.toImmutableList() }
val tags = remember(userState) { userState?.user?.info?.latestMetadata?.tags?.toImmutableListOfLists() }
val uri = LocalUriHandler.current
val clipboardManager = LocalClipboardManager.current
val context = LocalContext.current
val scope = rememberCoroutineScope()
(user.bestDisplayName() ?: user.bestUsername())?.let {
Row(verticalAlignment = Alignment.Bottom, modifier = Modifier.padding(top = 7.dp)) {
@ -693,7 +690,7 @@ private fun DrawAdditionalInfo(
val lud16 = remember(userState) { user.info?.lud16?.trim() ?: user.info?.lud06?.trim() }
val pubkeyHex = remember { baseUser.pubkeyHex }
DisplayLNAddress(lud16, pubkeyHex, accountViewModel.account, scope, context)
DisplayLNAddress(lud16, pubkeyHex, accountViewModel.account)
val identities = user.info?.latestMetadata?.identityClaims()
if (!identities.isNullOrEmpty()) {
@ -725,7 +722,7 @@ private fun DrawAdditionalInfo(
TranslatableRichTextViewer(
content = it,
canPreview = false,
tags = remember { persistentListOf() },
tags = remember { ImmutableListOfLists(emptyList()) },
backgroundColor = MaterialTheme.colors.background,
accountViewModel = accountViewModel,
nav = nav
@ -740,10 +737,10 @@ private fun DrawAdditionalInfo(
private fun DisplayLNAddress(
lud16: String?,
userHex: String,
account: Account,
scope: CoroutineScope,
context: Context
account: Account
) {
val context = LocalContext.current
val scope = rememberCoroutineScope()
var zapExpanded by remember { mutableStateOf(false) }
if (!lud16.isNullOrEmpty()) {

View File

@ -42,6 +42,7 @@ import com.vitorpamplona.amethyst.R
import com.vitorpamplona.amethyst.model.Note
import com.vitorpamplona.amethyst.service.model.ReportEvent
import com.vitorpamplona.amethyst.ui.theme.WarningColor
import kotlinx.collections.immutable.toImmutableList
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
@ -55,7 +56,7 @@ fun ReportNoteDialog(note: Note, accountViewModel: AccountViewModel, onDismiss:
Pair(ReportEvent.ReportType.ILLEGAL, stringResource(R.string.report_dialog_illegal))
)
val reasonOptions = reportTypes.map { it.second }
val reasonOptions = remember { reportTypes.map { it.second }.toImmutableList() }
var additionalReason by remember { mutableStateOf("") }
var selectedReason by remember { mutableStateOf(-1) }

View File

@ -11,6 +11,7 @@ import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
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.foundation.shape.RoundedCornerShape
@ -27,6 +28,8 @@ import androidx.compose.material.icons.filled.Clear
import androidx.compose.runtime.Composable
import androidx.compose.runtime.DisposableEffect
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.Stable
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.derivedStateOf
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
@ -45,6 +48,7 @@ import androidx.compose.ui.unit.dp
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.LifecycleEventObserver
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
@ -66,6 +70,8 @@ import com.vitorpamplona.amethyst.ui.screen.RefresheableFeedView
import com.vitorpamplona.amethyst.ui.screen.ScrollStateKeys
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.FlowPreview
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.flow.debounce
import kotlinx.coroutines.flow.distinctUntilChanged
@ -81,6 +87,23 @@ fun SearchScreen(
searchFeedViewModel: NostrGlobalFeedViewModel,
accountViewModel: AccountViewModel,
nav: (String) -> Unit
) {
val searchBarViewModel: SearchBarViewModel = viewModel(
key = accountViewModel.account.userProfile().pubkeyHex + "SearchBarViewModel",
factory = SearchBarViewModel.Factory(
accountViewModel.account
)
)
SearchScreen(searchFeedViewModel, searchBarViewModel, accountViewModel, nav)
}
@Composable
fun SearchScreen(
searchFeedViewModel: NostrGlobalFeedViewModel,
searchBarViewModel: SearchBarViewModel,
accountViewModel: AccountViewModel,
nav: (String) -> Unit
) {
val lifeCycleOwner = LocalLifecycleOwner.current
@ -112,7 +135,7 @@ fun SearchScreen(
Column(
modifier = Modifier.padding(vertical = 0.dp)
) {
SearchBar(accountViewModel, nav)
SearchBar(searchBarViewModel, accountViewModel, nav)
RefresheableFeedView(searchFeedViewModel, null, accountViewModel, nav, ScrollStateKeys.GLOBAL_SCREEN)
}
}
@ -127,17 +150,21 @@ fun WatchAccountForSearchScreen(searchFeedViewModel: NostrGlobalFeedViewModel, a
}
}
class SearchBarViewModel : ViewModel() {
var account: Account? = null
@Stable
class SearchBarViewModel(val account: Account) : ViewModel() {
var searchValue by mutableStateOf("")
val searchResultsUsers = mutableStateOf<List<User>>(emptyList())
val searchResultsNotes = mutableStateOf<List<Note>>(emptyList())
val searchResultsChannels = mutableStateOf<List<Channel>>(emptyList())
val hashtagResults = mutableStateOf<List<String>>(emptyList())
val isTrailingIconVisible by
derivedStateOf {
private var _searchResultsUsers = MutableStateFlow<List<User>>(emptyList())
private var _searchResultsNotes = MutableStateFlow<List<Note>>(emptyList())
private var _searchResultsChannels = MutableStateFlow<List<Channel>>(emptyList())
private var _hashtagResults = MutableStateFlow<List<String>>(emptyList())
val searchResultsUsers = _searchResultsUsers.asStateFlow()
val searchResultsNotes = _searchResultsNotes.asStateFlow()
val searchResultsChannels = _searchResultsChannels.asStateFlow()
val hashtagResults = _hashtagResults.asStateFlow()
val isSearching by derivedStateOf {
searchValue.isNotBlank()
}
@ -145,26 +172,27 @@ class SearchBarViewModel : ViewModel() {
searchValue = newValue
}
private fun runSearch() {
private suspend fun runSearch() {
if (searchValue.isBlank()) {
hashtagResults.value = emptyList()
searchResultsUsers.value = emptyList()
searchResultsChannels.value = emptyList()
searchResultsNotes.value = emptyList()
_hashtagResults.value = emptyList()
_searchResultsUsers.value = emptyList()
_searchResultsChannels.value = emptyList()
_searchResultsNotes.value = emptyList()
return
}
hashtagResults.value = findHashtags(searchValue)
searchResultsUsers.value = LocalCache.findUsersStartingWith(searchValue).sortedWith(compareBy({ account?.isFollowing(it) }, { it.toBestDisplayName() })).reversed()
searchResultsNotes.value = LocalCache.findNotesStartingWith(searchValue).sortedWith(compareBy({ it.createdAt() }, { it.idHex })).reversed()
searchResultsChannels.value = LocalCache.findChannelsStartingWith(searchValue)
_hashtagResults.emit(findHashtags(searchValue))
_searchResultsUsers.emit(LocalCache.findUsersStartingWith(searchValue).sortedWith(compareBy({ account.isFollowing(it) }, { it.toBestDisplayName() })).reversed())
_searchResultsNotes.emit(LocalCache.findNotesStartingWith(searchValue).sortedWith(compareBy({ it.createdAt() }, { it.idHex })).reversed())
_searchResultsChannels.emit(LocalCache.findChannelsStartingWith(searchValue))
}
fun clean() {
searchValue = ""
searchResultsUsers.value = emptyList()
searchResultsChannels.value = emptyList()
searchResultsNotes.value = emptyList()
_searchResultsUsers.value = emptyList()
_searchResultsChannels.value = emptyList()
_searchResultsNotes.value = emptyList()
_searchResultsChannels.value = emptyList()
}
private val bundler = BundledUpdate(250, Dispatchers.IO)
@ -177,20 +205,24 @@ class SearchBarViewModel : ViewModel() {
}
}
fun isSearching() = searchValue.isNotBlank()
fun isSearchingFun() = searchValue.isNotBlank()
class Factory(val account: Account) : ViewModelProvider.Factory {
override fun <SearchBarViewModel : ViewModel> create(modelClass: Class<SearchBarViewModel>): SearchBarViewModel {
return SearchBarViewModel(account) as SearchBarViewModel
}
}
}
@OptIn(FlowPreview::class)
@Composable
private fun SearchBar(accountViewModel: AccountViewModel, nav: (String) -> Unit) {
val searchBarViewModel: SearchBarViewModel = viewModel()
searchBarViewModel.account = accountViewModel.account
val scope = rememberCoroutineScope()
private fun SearchBar(
searchBarViewModel: SearchBarViewModel,
accountViewModel: AccountViewModel,
nav: (String) -> Unit
) {
val listState = rememberLazyListState()
val onlineSearch = NostrSearchEventOrUserDataSource
// Create a channel for processing search queries.
val searchTextChanges = remember {
CoroutineChannel<String>(CoroutineChannel.CONFLATED)
@ -199,7 +231,7 @@ private fun SearchBar(accountViewModel: AccountViewModel, nav: (String) -> Unit)
LaunchedEffect(Unit) {
launch(Dispatchers.IO) {
LocalCache.live.newEventBundles.collect {
if (searchBarViewModel.isSearching()) {
if (searchBarViewModel.isSearchingFun()) {
searchBarViewModel.invalidateData()
}
}
@ -215,13 +247,13 @@ private fun SearchBar(accountViewModel: AccountViewModel, nav: (String) -> Unit)
.debounce(300)
.collectLatest {
if (it.length >= 2) {
onlineSearch.search(it.trim())
NostrSearchEventOrUserDataSource.search(it.trim())
}
searchBarViewModel.invalidateData()
// makes sure to show the top of the search
scope.launch(Dispatchers.Main) { listState.animateScrollToItem(0) }
launch(Dispatchers.Main) { listState.animateScrollToItem(0) }
}
}
}
@ -233,6 +265,18 @@ private fun SearchBar(accountViewModel: AccountViewModel, nav: (String) -> Unit)
}
// LAST ROW
SearchTextField(searchBarViewModel, searchTextChanges)
DisplaySearchResults(listState, searchBarViewModel, nav, accountViewModel)
}
@Composable
private fun SearchTextField(
searchBarViewModel: SearchBarViewModel,
searchTextChanges: kotlinx.coroutines.channels.Channel<String>
) {
val scope = rememberCoroutineScope()
Row(
modifier = Modifier
.padding(10.dp)
@ -270,11 +314,11 @@ private fun SearchBar(accountViewModel: AccountViewModel, nav: (String) -> Unit)
)
},
trailingIcon = {
if (searchBarViewModel.isTrailingIconVisible) {
if (searchBarViewModel.isSearching) {
IconButton(
onClick = {
searchBarViewModel.clean()
onlineSearch.clear()
NostrSearchEventOrUserDataSource.clear()
}
) {
Icon(
@ -291,46 +335,77 @@ private fun SearchBar(accountViewModel: AccountViewModel, nav: (String) -> Unit)
)
)
}
}
if (searchBarViewModel.isSearching()) {
LazyColumn(
modifier = Modifier.fillMaxHeight(),
contentPadding = PaddingValues(
top = 10.dp,
bottom = 10.dp
),
state = listState
) {
itemsIndexed(searchBarViewModel.hashtagResults.value, key = { _, item -> "#" + item }) { _, item ->
HashtagLine(item) {
nav("Hashtag/$item")
}
}
@Composable
private fun DisplaySearchResults(
listState: LazyListState,
searchBarViewModel: SearchBarViewModel,
nav: (String) -> Unit,
accountViewModel: AccountViewModel
) {
if (!searchBarViewModel.isSearching) {
return
}
itemsIndexed(searchBarViewModel.searchResultsUsers.value, key = { _, item -> "u" + item.pubkeyHex }) { _, item ->
UserCompose(item, accountViewModel = accountViewModel, nav = nav)
}
val hashTags by searchBarViewModel.hashtagResults.collectAsState()
val users by searchBarViewModel.searchResultsUsers.collectAsState()
val channels by searchBarViewModel.searchResultsChannels.collectAsState()
val notes by searchBarViewModel.searchResultsNotes.collectAsState()
itemsIndexed(searchBarViewModel.searchResultsChannels.value, key = { _, item -> "c" + item.idHex }) { _, item ->
ChannelName(
channelIdHex = item.idHex,
channelPicture = item.profilePicture(),
channelTitle = {
Text(
"${item.info.name}",
fontWeight = FontWeight.Bold
)
},
channelLastTime = null,
channelLastContent = item.info.about,
false,
onClick = { nav("Channel/${item.idHex}") }
)
LazyColumn(
modifier = Modifier.fillMaxHeight(),
contentPadding = PaddingValues(
top = 10.dp,
bottom = 10.dp
),
state = listState
) {
itemsIndexed(
hashTags,
key = { _, item -> "#$item" }
) { _, item ->
HashtagLine(item) {
nav("Hashtag/$item")
}
}
itemsIndexed(searchBarViewModel.searchResultsNotes.value, key = { _, item -> "n" + item.idHex }) { _, item ->
NoteCompose(item, accountViewModel = accountViewModel, nav = nav)
}
itemsIndexed(
users,
key = { _, item -> "u" + item.pubkeyHex }
) { _, item ->
UserCompose(item, accountViewModel = accountViewModel, nav = nav)
}
itemsIndexed(
channels,
key = { _, item -> "c" + item.idHex }
) { _, item ->
ChannelName(
channelIdHex = item.idHex,
channelPicture = item.profilePicture(),
channelTitle = {
Text(
"${item.info.name}",
fontWeight = FontWeight.Bold
)
},
channelLastTime = null,
channelLastContent = item.info.about,
false,
onClick = { nav("Channel/${item.idHex}") }
)
}
itemsIndexed(
notes,
key = { _, item -> "n" + item.idHex }
) { _, item ->
NoteCompose(
item,
accountViewModel = accountViewModel,
nav = nav
)
}
}
}

View File

@ -1,6 +1,7 @@
package com.vitorpamplona.amethyst.service.lang
import android.util.LruCache
import androidx.compose.runtime.Immutable
import com.google.android.gms.tasks.Task
import com.google.android.gms.tasks.Tasks
import com.google.mlkit.nl.languageid.LanguageIdentification
@ -13,11 +14,12 @@ import com.linkedin.urls.detection.UrlDetector
import com.linkedin.urls.detection.UrlDetectorOptions
import java.util.regex.Pattern
class ResultOrError(
var result: String?,
var sourceLang: String?,
var targetLang: String?,
var error: Exception?
@Immutable
data class ResultOrError(
val result: String?,
val sourceLang: String?,
val targetLang: String?,
val error: Exception?
)
object LanguageTranslatorService {

View File

@ -33,8 +33,8 @@ import androidx.core.os.ConfigurationCompat
import com.vitorpamplona.amethyst.R
import com.vitorpamplona.amethyst.service.lang.LanguageTranslatorService
import com.vitorpamplona.amethyst.service.lang.ResultOrError
import com.vitorpamplona.amethyst.ui.actions.ImmutableListOfLists
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
import kotlinx.collections.immutable.ImmutableList
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import java.util.Locale
@ -44,7 +44,7 @@ fun TranslatableRichTextViewer(
content: String,
canPreview: Boolean,
modifier: Modifier = Modifier,
tags: ImmutableList<List<String>>,
tags: ImmutableListOfLists<String>,
backgroundColor: Color,
accountViewModel: AccountViewModel,
nav: (String) -> Unit