Adds communities, hashtags and geohashes to the lists on the top navigation bar.

This commit is contained in:
Vitor Pamplona
2023-09-20 12:23:50 -04:00
parent dc314a94ff
commit 3efa3a68fe
13 changed files with 265 additions and 129 deletions
@@ -2478,13 +2478,7 @@ class Account(
val privKey = keyPair.privKey val privKey = keyPair.privKey
return if (listName != null) { return if (listName != null) {
val aTag = ATag( val list = LocalCache.addressables[listName]
PeopleListEvent.kind,
userProfile().pubkeyHex,
listName,
null
).toTag()
val list = LocalCache.addressables[aTag]
if (list != null) { if (list != null) {
val publicHexList = (list.event as? PeopleListEvent)?.bookmarkedPeople() ?: emptySet() val publicHexList = (list.event as? PeopleListEvent)?.bookmarkedPeople() ?: emptySet()
val privateHexList = privKey?.let { val privateHexList = privKey?.let {
@@ -2507,13 +2501,7 @@ class Account(
val privKey = keyPair.privKey val privKey = keyPair.privKey
return if (listName != null) { return if (listName != null) {
val aTag = ATag( val list = LocalCache.addressables[listName]
PeopleListEvent.kind,
userProfile().pubkeyHex,
listName,
null
).toTag()
val list = LocalCache.addressables[aTag]
if (list != null) { if (list != null) {
val publicAddresses = list.event?.hashtags() ?: emptySet() val publicAddresses = list.event?.hashtags() ?: emptySet()
val privateAddresses = privKey?.let { val privateAddresses = privKey?.let {
@@ -2536,13 +2524,7 @@ class Account(
val privKey = keyPair.privKey val privKey = keyPair.privKey
return if (listName != null) { return if (listName != null) {
val aTag = ATag( val list = LocalCache.addressables[listName]
PeopleListEvent.kind,
userProfile().pubkeyHex,
listName,
null
).toTag()
val list = LocalCache.addressables[aTag]
if (list != null) { if (list != null) {
val publicAddresses = list.event?.geohashes() ?: emptySet() val publicAddresses = list.event?.geohashes() ?: emptySet()
val privateAddresses = privKey?.let { val privateAddresses = privKey?.let {
@@ -2565,13 +2547,7 @@ class Account(
val privKey = keyPair.privKey val privKey = keyPair.privKey
return if (listName != null) { return if (listName != null) {
val aTag = ATag( val list = LocalCache.addressables[listName]
PeopleListEvent.kind,
userProfile().pubkeyHex,
listName,
null
).toTag()
val list = LocalCache.addressables[aTag]
if (list != null) { if (list != null) {
val publicAddresses = list.event?.taggedAddresses()?.map { it.toTag() } ?: emptySet() val publicAddresses = list.event?.taggedAddresses()?.map { it.toTag() } ?: emptySet()
val privateAddresses = privKey?.let { val privateAddresses = privKey?.let {
@@ -43,6 +43,10 @@ class AddressableNote(val address: ATag) : Note(address.toTag()) {
return minOf(publishedAt, lastCreatedAt) return minOf(publishedAt, lastCreatedAt)
} }
fun dTag(): String? {
return (event as? AddressableEvent)?.dTag()
}
} }
@Stable @Stable
@@ -319,7 +319,7 @@ class User(val pubkeyHex: String) {
return latestContactList?.verifiedFollowKeySet ?: emptySet() return latestContactList?.verifiedFollowKeySet ?: emptySet()
} }
fun cachedFollowingTagSet(): Set<HexKey> { fun cachedFollowingTagSet(): Set<String> {
return latestContactList?.verifiedFollowTagSet ?: emptySet() return latestContactList?.verifiedFollowTagSet ?: emptySet()
} }
@@ -50,6 +50,7 @@ import com.vitorpamplona.amethyst.model.ServersAvailable
import com.vitorpamplona.amethyst.ui.components.VideoView import com.vitorpamplona.amethyst.ui.components.VideoView
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
import com.vitorpamplona.amethyst.ui.screen.loggedIn.TextSpinner import com.vitorpamplona.amethyst.ui.screen.loggedIn.TextSpinner
import com.vitorpamplona.amethyst.ui.screen.loggedIn.TitleExplainer
import com.vitorpamplona.amethyst.ui.theme.placeholderText import com.vitorpamplona.amethyst.ui.theme.placeholderText
import kotlinx.collections.immutable.toImmutableList import kotlinx.collections.immutable.toImmutableList
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
@@ -186,8 +187,7 @@ fun ImageVideoPost(postViewModel: NewMediaModel, accountViewModel: AccountViewMo
Triple(ServersAvailable.NIP95, stringResource(id = R.string.upload_server_relays_nip95), stringResource(id = R.string.upload_server_relays_nip95_explainer)) Triple(ServersAvailable.NIP95, stringResource(id = R.string.upload_server_relays_nip95), stringResource(id = R.string.upload_server_relays_nip95_explainer))
) )
val fileServerOptions = remember { fileServers.map { it.second }.toImmutableList() } val fileServerOptions = remember { fileServers.map { TitleExplainer(it.second, it.third) }.toImmutableList() }
val fileServerExplainers = remember { fileServers.map { it.third }.toImmutableList() }
val resolver = LocalContext.current.contentResolver val resolver = LocalContext.current.contentResolver
Row( Row(
@@ -252,7 +252,6 @@ fun ImageVideoPost(postViewModel: NewMediaModel, accountViewModel: AccountViewMo
label = stringResource(id = R.string.file_server), label = stringResource(id = R.string.file_server),
placeholder = fileServers.firstOrNull { it.first == accountViewModel.account.defaultFileServer }?.second ?: fileServers[0].second, placeholder = fileServers.firstOrNull { it.first == accountViewModel.account.defaultFileServer }?.second ?: fileServers[0].second,
options = fileServerOptions, options = fileServerOptions,
explainers = fileServerExplainers,
onSelect = { onSelect = {
postViewModel.selectedServer = fileServers[it].first postViewModel.selectedServer = fileServers[it].first
}, },
@@ -93,6 +93,7 @@ import com.vitorpamplona.amethyst.ui.note.UsernameDisplay
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
import com.vitorpamplona.amethyst.ui.screen.loggedIn.MyTextField import com.vitorpamplona.amethyst.ui.screen.loggedIn.MyTextField
import com.vitorpamplona.amethyst.ui.screen.loggedIn.TextSpinner import com.vitorpamplona.amethyst.ui.screen.loggedIn.TextSpinner
import com.vitorpamplona.amethyst.ui.screen.loggedIn.TitleExplainer
import com.vitorpamplona.amethyst.ui.screen.loggedIn.UserLine import com.vitorpamplona.amethyst.ui.screen.loggedIn.UserLine
import com.vitorpamplona.amethyst.ui.theme.BitcoinOrange import com.vitorpamplona.amethyst.ui.theme.BitcoinOrange
import com.vitorpamplona.amethyst.ui.theme.ButtonBorder import com.vitorpamplona.amethyst.ui.theme.ButtonBorder
@@ -1318,8 +1319,7 @@ fun ImageVideoDescription(
Triple(ServersAvailable.NIP95, stringResource(id = R.string.upload_server_relays_nip95), stringResource(id = R.string.upload_server_relays_nip95_explainer)) Triple(ServersAvailable.NIP95, stringResource(id = R.string.upload_server_relays_nip95), stringResource(id = R.string.upload_server_relays_nip95_explainer))
) )
val fileServerOptions = remember { fileServers.map { it.second }.toImmutableList() } val fileServerOptions = remember { fileServers.map { TitleExplainer(it.second, it.third) }.toImmutableList() }
val fileServerExplainers = remember { fileServers.map { it.third }.toImmutableList() }
var selectedServer by remember { mutableStateOf(defaultServer) } var selectedServer by remember { mutableStateOf(defaultServer) }
var message by remember { mutableStateOf("") } var message by remember { mutableStateOf("") }
@@ -1433,7 +1433,6 @@ fun ImageVideoDescription(
label = stringResource(id = R.string.file_server), label = stringResource(id = R.string.file_server),
placeholder = fileServers.filter { it.first == defaultServer }.firstOrNull()?.second ?: fileServers[0].second, placeholder = fileServers.filter { it.first == defaultServer }.firstOrNull()?.second ?: fileServers[0].second,
options = fileServerOptions, options = fileServerOptions,
explainers = fileServerExplainers,
onSelect = { onSelect = {
selectedServer = fileServers[it].first selectedServer = fileServers[it].first
}, },
@@ -20,6 +20,7 @@ import androidx.compose.material.OutlinedTextField
import androidx.compose.material.Surface import androidx.compose.material.Surface
import androidx.compose.material.Text import androidx.compose.material.Text
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.Immutable
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
@@ -37,8 +38,7 @@ import kotlinx.collections.immutable.ImmutableList
fun TextSpinner( fun TextSpinner(
label: String?, label: String?,
placeholder: String, placeholder: String,
options: ImmutableList<String>, options: ImmutableList<TitleExplainer>,
explainers: ImmutableList<String>? = null,
onSelect: (Int) -> Unit, onSelect: (Int) -> Unit,
modifier: Modifier = Modifier modifier: Modifier = Modifier
) { ) {
@@ -74,8 +74,8 @@ fun TextSpinner(
if (optionsShowing) { if (optionsShowing) {
options.isNotEmpty().also { options.isNotEmpty().also {
SpinnerSelectionDialog(options = options, explainers = explainers, onDismiss = { optionsShowing = false }) { SpinnerSelectionDialog(options = options, onDismiss = { optionsShowing = false }) {
currentText = options[it] currentText = options[it].title
optionsShowing = false optionsShowing = false
onSelect(it) onSelect(it)
} }
@@ -85,10 +85,40 @@ fun TextSpinner(
@Composable @Composable
fun SpinnerSelectionDialog( fun SpinnerSelectionDialog(
options: ImmutableList<String>, options: ImmutableList<TitleExplainer>,
explainers: ImmutableList<String>?,
onDismiss: () -> Unit, onDismiss: () -> Unit,
onSelect: (Int) -> Unit onSelect: (Int) -> Unit
) {
SpinnerSelectionDialog(
options = options,
onSelect = onSelect,
onDismiss = onDismiss
) { item ->
Row(
horizontalArrangement = Arrangement.Center,
modifier = Modifier.fillMaxWidth()
) {
Text(text = item.title, color = MaterialTheme.colors.onSurface)
}
item.explainer?.let {
Spacer(modifier = Modifier.height(5.dp))
Row(
horizontalArrangement = Arrangement.Start,
modifier = Modifier
.fillMaxWidth()
) {
Text(text = it, color = Color.Gray, fontSize = Font14SP)
}
}
}
}
@Composable
fun <T> SpinnerSelectionDialog(
options: ImmutableList<T>,
onSelect: (Int) -> Unit,
onDismiss: () -> Unit,
onRenderItem: @Composable (T) -> Unit
) { ) {
Dialog(onDismissRequest = onDismiss) { Dialog(onDismissRequest = onDismiss) {
Surface( Surface(
@@ -106,23 +136,7 @@ fun SpinnerSelectionDialog(
} }
) { ) {
Column() { Column() {
Row( onRenderItem(item)
horizontalArrangement = Arrangement.Center,
modifier = Modifier
.fillMaxWidth()
) {
Text(text = item, color = MaterialTheme.colors.onSurface)
}
explainers?.getOrNull(index)?.let {
Spacer(modifier = Modifier.height(5.dp))
Row(
horizontalArrangement = Arrangement.Start,
modifier = Modifier
.fillMaxWidth()
) {
Text(text = it, color = Color.Gray, fontSize = Font14SP)
}
}
} }
} }
if (index < options.lastIndex) { if (index < options.lastIndex) {
@@ -133,3 +147,6 @@ fun SpinnerSelectionDialog(
} }
} }
} }
@Immutable
data class TitleExplainer(val title: String, val explainer: String? = null)
@@ -33,6 +33,7 @@ import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.ExpandMore import androidx.compose.material.icons.filled.ExpandMore
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.Immutable
import androidx.compose.runtime.Stable import androidx.compose.runtime.Stable
import androidx.compose.runtime.State import androidx.compose.runtime.State
import androidx.compose.runtime.collectAsState import androidx.compose.runtime.collectAsState
@@ -54,11 +55,13 @@ import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModel
import androidx.lifecycle.ViewModelProvider import androidx.lifecycle.ViewModelProvider
import androidx.lifecycle.map
import androidx.lifecycle.viewModelScope import androidx.lifecycle.viewModelScope
import androidx.navigation.NavBackStackEntry import androidx.navigation.NavBackStackEntry
import coil.Coil import coil.Coil
import com.vitorpamplona.amethyst.R import com.vitorpamplona.amethyst.R
import com.vitorpamplona.amethyst.model.Account import com.vitorpamplona.amethyst.model.Account
import com.vitorpamplona.amethyst.model.AddressableNote
import com.vitorpamplona.amethyst.model.GLOBAL_FOLLOWS import com.vitorpamplona.amethyst.model.GLOBAL_FOLLOWS
import com.vitorpamplona.amethyst.model.KIND3_FOLLOWS import com.vitorpamplona.amethyst.model.KIND3_FOLLOWS
import com.vitorpamplona.amethyst.model.LiveActivitiesChannel import com.vitorpamplona.amethyst.model.LiveActivitiesChannel
@@ -88,6 +91,7 @@ import com.vitorpamplona.amethyst.ui.note.ArrowBackIcon
import com.vitorpamplona.amethyst.ui.note.ClickableUserPicture import com.vitorpamplona.amethyst.ui.note.ClickableUserPicture
import com.vitorpamplona.amethyst.ui.note.LoadAddressableNote import com.vitorpamplona.amethyst.ui.note.LoadAddressableNote
import com.vitorpamplona.amethyst.ui.note.LoadChannel import com.vitorpamplona.amethyst.ui.note.LoadChannel
import com.vitorpamplona.amethyst.ui.note.LoadCityName
import com.vitorpamplona.amethyst.ui.note.LoadUser import com.vitorpamplona.amethyst.ui.note.LoadUser
import com.vitorpamplona.amethyst.ui.note.LongCommunityHeader import com.vitorpamplona.amethyst.ui.note.LongCommunityHeader
import com.vitorpamplona.amethyst.ui.note.NonClickableUserPictures import com.vitorpamplona.amethyst.ui.note.NonClickableUserPictures
@@ -117,6 +121,7 @@ import com.vitorpamplona.amethyst.ui.theme.Size34dp
import com.vitorpamplona.amethyst.ui.theme.Size40dp import com.vitorpamplona.amethyst.ui.theme.Size40dp
import com.vitorpamplona.amethyst.ui.theme.placeholderText import com.vitorpamplona.amethyst.ui.theme.placeholderText
import com.vitorpamplona.quartz.events.ChatroomKey import com.vitorpamplona.quartz.events.ChatroomKey
import com.vitorpamplona.quartz.events.ContactListEvent
import com.vitorpamplona.quartz.events.PeopleListEvent import com.vitorpamplona.quartz.events.PeopleListEvent
import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.toImmutableList import kotlinx.collections.immutable.toImmutableList
@@ -382,11 +387,12 @@ fun StoriesTopBar(followLists: FollowListViewModel, scaffoldState: ScaffoldState
val list by accountViewModel.storiesListLiveData.observeAsState(GLOBAL_FOLLOWS) val list by accountViewModel.storiesListLiveData.observeAsState(GLOBAL_FOLLOWS)
FollowList( FollowList(
followLists, followListsModel = followLists,
list, listName = list,
true withGlobal = true,
withRoutes = false
) { listName -> ) { listName ->
accountViewModel.account.changeDefaultStoriesFollowList(listName) accountViewModel.account.changeDefaultStoriesFollowList(listName.code)
} }
} }
} }
@@ -397,11 +403,16 @@ fun HomeTopBar(followLists: FollowListViewModel, scaffoldState: ScaffoldState, a
val list by accountViewModel.homeListLiveData.observeAsState(KIND3_FOLLOWS) val list by accountViewModel.homeListLiveData.observeAsState(KIND3_FOLLOWS)
FollowList( FollowList(
followLists, followListsModel = followLists,
list, listName = list,
true withGlobal = true,
withRoutes = true
) { listName -> ) { listName ->
accountViewModel.account.changeDefaultHomeFollowList(listName) if (listName.type == CodeNameType.ROUTE) {
nav(listName.code)
} else {
accountViewModel.account.changeDefaultHomeFollowList(listName.code)
}
} }
} }
} }
@@ -412,11 +423,12 @@ fun NotificationTopBar(followLists: FollowListViewModel, scaffoldState: Scaffold
val list by accountViewModel.notificationListLiveData.observeAsState(GLOBAL_FOLLOWS) val list by accountViewModel.notificationListLiveData.observeAsState(GLOBAL_FOLLOWS)
FollowList( FollowList(
followLists, followListsModel = followLists,
list, listName = list,
true withGlobal = true,
withRoutes = false
) { listName -> ) { listName ->
accountViewModel.account.changeDefaultNotificationFollowList(listName) accountViewModel.account.changeDefaultNotificationFollowList(listName.code)
} }
} }
} }
@@ -427,11 +439,12 @@ fun DiscoveryTopBar(followLists: FollowListViewModel, scaffoldState: ScaffoldSta
val list by accountViewModel.discoveryListLiveData.observeAsState(GLOBAL_FOLLOWS) val list by accountViewModel.discoveryListLiveData.observeAsState(GLOBAL_FOLLOWS)
FollowList( FollowList(
followLists, followListsModel = followLists,
list, listName = list,
true withGlobal = true,
withRoutes = false
) { listName -> ) { listName ->
accountViewModel.account.changeDefaultDiscoveryFollowList(listName) accountViewModel.account.changeDefaultDiscoveryFollowList(listName.code)
} }
} }
} }
@@ -523,37 +536,81 @@ private fun LoggedInUserPictureDrawer(
} }
@Composable @Composable
fun FollowList(followListsModel: FollowListViewModel, listName: String, withGlobal: Boolean, onChange: (String) -> Unit) { fun FollowList(
val kind3Follow = Pair(KIND3_FOLLOWS, stringResource(id = R.string.follow_list_kind3follows)) followListsModel: FollowListViewModel,
val globalFollow = Pair(GLOBAL_FOLLOWS, stringResource(id = R.string.follow_list_global)) listName: String,
withGlobal: Boolean,
withRoutes: Boolean,
onChange: (CodeName) -> Unit
) {
val context = LocalContext.current
val kind3Follow = CodeName(KIND3_FOLLOWS, ResourceName(R.string.follow_list_kind3follows, context), CodeNameType.HARDCODED)
val globalFollow = CodeName(GLOBAL_FOLLOWS, ResourceName(R.string.follow_list_global, context), CodeNameType.HARDCODED)
val defaultOptions = if (withGlobal) listOf(kind3Follow, globalFollow) else listOf(kind3Follow) val defaultOptions = if (withGlobal) listOf(kind3Follow, globalFollow) else listOf(kind3Follow)
val followLists by followListsModel.followLists.collectAsState() val followLists by followListsModel.peopleLists.collectAsState()
val routeList by followListsModel.routes.collectAsState()
val allLists = remember(followLists) { val allLists = remember(followLists) {
(defaultOptions + followLists) if (withRoutes) {
(defaultOptions + followLists + routeList)
} else {
(defaultOptions + followLists)
}
} }
val followNames by remember(followLists) { val followNames by remember(followLists) {
derivedStateOf { derivedStateOf {
allLists.map { it.second }.toImmutableList() allLists.map { it.name }.toImmutableList()
} }
} }
SimpleTextSpinner( SimpleTextSpinner(
placeholder = allLists.firstOrNull { it.first == listName }?.second ?: "Select an Option", placeholder = allLists.firstOrNull { it.code == listName }?.name?.name() ?: "Select an Option",
options = followNames, options = followNames,
onSelect = { onSelect = {
onChange(allLists.getOrNull(it)?.first ?: KIND3_FOLLOWS) onChange(allLists.getOrNull(it) ?: kind3Follow)
} }
) )
} }
enum class CodeNameType {
HARDCODED, PEOPLE_LIST, ROUTE
}
abstract class Name {
abstract fun name(): String
}
class GeoHashName(val geoHashTag: String) : Name() {
override fun name() = "/g/$geoHashTag"
}
class HashtagName(val hashTag: String) : Name() {
override fun name() = "#$hashTag"
}
class ResourceName(val resourceId: Int, val context: Context) : Name() {
override fun name() = context.getString(resourceId)
}
class PeopleListName(val note: AddressableNote) : Name() {
override fun name() = note.dTag() ?: ""
}
class CommunityName(val note: AddressableNote) : Name() {
override fun name() = "/n/${(note.dTag() ?: "")}"
}
@Immutable
data class CodeName(val code: String, val name: Name, val type: CodeNameType)
@Stable @Stable
class FollowListViewModel(val account: Account) : ViewModel() { class FollowListViewModel(val account: Account) : ViewModel() {
private var _followLists = MutableStateFlow<ImmutableList<Pair<String, String>>>(emptyList<Pair<String, String>>().toPersistentList()) private var _peopleLists = MutableStateFlow<ImmutableList<CodeName>>(emptyList<CodeName>().toPersistentList())
val followLists = _followLists.asStateFlow() val peopleLists = _peopleLists.asStateFlow()
private var _routes = MutableStateFlow<ImmutableList<CodeName>>(emptyList<CodeName>().toPersistentList())
val routes = _routes.asStateFlow()
fun refresh() { fun refresh() {
viewModelScope.launch(Dispatchers.Default) { viewModelScope.launch(Dispatchers.Default) {
@@ -571,14 +628,34 @@ class FollowListViewModel(val account: Account) : ViewModel() {
event.pubKey == account.userProfile().pubkeyHex && event.pubKey == account.userProfile().pubkeyHex &&
(event.tags.size > 1 || event.content.length > 50) (event.tags.size > 1 || event.content.length > 50)
) { ) {
Pair(event.dTag(), event.dTag()) CodeName(event.address().toTag(), PeopleListName(it.value), CodeNameType.PEOPLE_LIST)
} else { } else {
null null
} }
}.sortedBy { it.second }.toImmutableList() }.sortedBy { it.name.name() }.toImmutableList()
if (!equalImmutableLists(_followLists.value, newFollowLists)) { if (!equalImmutableLists(_peopleLists.value, newFollowLists)) {
_followLists.emit(newFollowLists) _peopleLists.emit(newFollowLists)
}
val communities = account.userProfile().cachedFollowingCommunitiesSet().mapNotNull {
LocalCache.checkGetOrCreateAddressableNote(it)?.let { communityNote ->
CodeName("Community/${communityNote.idHex}", CommunityName(communityNote), CodeNameType.ROUTE)
}
}
val hashtags = account.userProfile().cachedFollowingTagSet().map {
CodeName("Hashtag/$it", HashtagName(it), CodeNameType.ROUTE)
}
val geotags = account.userProfile().cachedFollowingGeohashSet().map {
CodeName("Geohash/$it", GeoHashName(it), CodeNameType.ROUTE)
}
val routeList = (communities + hashtags + geotags).sortedBy { it.name.name() }.toImmutableList()
if (!equalImmutableLists(_routes.value, routeList)) {
_routes.emit(routeList)
} }
} }
@@ -590,7 +667,10 @@ class FollowListViewModel(val account: Account) : ViewModel() {
collectorJob = viewModelScope.launch(Dispatchers.IO) { collectorJob = viewModelScope.launch(Dispatchers.IO) {
LocalCache.live.newEventBundles.collect { newNotes -> LocalCache.live.newEventBundles.collect { newNotes ->
checkNotInMainThread() checkNotInMainThread()
if (newNotes.any { it.event is PeopleListEvent }) { if (newNotes.any {
it.event?.pubKey() == account.userProfile().pubkeyHex && (it.event is PeopleListEvent || it.event is ContactListEvent)
}
) {
refresh() refresh()
} }
} }
@@ -612,8 +692,7 @@ class FollowListViewModel(val account: Account) : ViewModel() {
@Composable @Composable
fun SimpleTextSpinner( fun SimpleTextSpinner(
placeholder: String, placeholder: String,
options: ImmutableList<String>, options: ImmutableList<Name>,
explainers: ImmutableList<String>? = null,
onSelect: (Int) -> Unit, onSelect: (Int) -> Unit,
modifier: Modifier = Modifier modifier: Modifier = Modifier
) { ) {
@@ -649,10 +728,68 @@ fun SimpleTextSpinner(
if (optionsShowing) { if (optionsShowing) {
options.isNotEmpty().also { options.isNotEmpty().also {
SpinnerSelectionDialog(options = options, explainers = explainers, onDismiss = { optionsShowing = false }) { SpinnerSelectionDialog(
currentText = options[it] options = options,
optionsShowing = false onDismiss = { optionsShowing = false },
onSelect(it) onSelect = {
currentText = options[it].name()
optionsShowing = false
onSelect(it)
}
) {
RenderOption(it)
}
}
}
}
@Composable
fun RenderOption(it: Name) {
when (it) {
is GeoHashName -> {
LoadCityName(it.geoHashTag) {
Row(
horizontalArrangement = Arrangement.Center,
modifier = Modifier.fillMaxWidth()
) {
Text(text = "/g/$it", color = MaterialTheme.colors.onSurface)
}
}
}
is HashtagName -> {
Row(
horizontalArrangement = Arrangement.Center,
modifier = Modifier.fillMaxWidth()
) {
Text(text = it.name(), color = MaterialTheme.colors.onSurface)
}
}
is ResourceName -> {
Row(
horizontalArrangement = Arrangement.Center,
modifier = Modifier.fillMaxWidth()
) {
Text(text = stringResource(id = it.resourceId), color = MaterialTheme.colors.onSurface)
}
}
is PeopleListName -> {
Row(
horizontalArrangement = Arrangement.Center,
modifier = Modifier.fillMaxWidth()
) {
Text(text = it.name(), color = MaterialTheme.colors.onSurface)
}
}
is CommunityName -> {
Row(
horizontalArrangement = Arrangement.Center,
modifier = Modifier.fillMaxWidth()
) {
val name by it.note.live().metadata.map {
"/n/" + (it.note as? AddressableNote)?.dTag()
}.observeAsState()
Text(text = name ?: "", color = MaterialTheme.colors.onSurface)
} }
} }
} }
@@ -2579,7 +2579,7 @@ fun LoadStatuses(
} }
@Composable @Composable
fun DisplayLocation(geohash: String, nav: (String) -> Unit) { fun LoadCityName(geohash: String, content: @Composable (String) -> Unit) {
val context = LocalContext.current val context = LocalContext.current
var cityName by remember(geohash) { var cityName by remember(geohash) {
mutableStateOf<String>(geohash) mutableStateOf<String>(geohash)
@@ -2594,18 +2594,25 @@ fun DisplayLocation(geohash: String, nav: (String) -> Unit) {
} }
} }
ClickableText( content(cityName)
text = AnnotatedString(cityName), }
onClick = { nav("Geohash/$geohash") },
style = LocalTextStyle.current.copy( @Composable
color = MaterialTheme.colors.primary.copy( fun DisplayLocation(geohash: String, nav: (String) -> Unit) {
alpha = 0.52f LoadCityName(geohash) { cityName ->
ClickableText(
text = AnnotatedString(cityName),
onClick = { nav("Geohash/$geohash") },
style = LocalTextStyle.current.copy(
color = MaterialTheme.colors.primary.copy(
alpha = 0.52f
),
fontSize = Font14SP,
fontWeight = FontWeight.Bold
), ),
fontSize = Font14SP, maxLines = 1
fontWeight = FontWeight.Bold )
), }
maxLines = 1
)
} }
@Composable @Composable
@@ -89,7 +89,6 @@ import com.vitorpamplona.amethyst.ui.theme.DarkerGreen
import com.vitorpamplona.amethyst.ui.theme.Font14SP import com.vitorpamplona.amethyst.ui.theme.Font14SP
import com.vitorpamplona.amethyst.ui.theme.HalfDoubleVertSpacer import com.vitorpamplona.amethyst.ui.theme.HalfDoubleVertSpacer
import com.vitorpamplona.amethyst.ui.theme.HalfStartPadding import com.vitorpamplona.amethyst.ui.theme.HalfStartPadding
import com.vitorpamplona.amethyst.ui.theme.Height4dpModifier
import com.vitorpamplona.amethyst.ui.theme.ModifierWidth3dp import com.vitorpamplona.amethyst.ui.theme.ModifierWidth3dp
import com.vitorpamplona.amethyst.ui.theme.NoSoTinyBorders import com.vitorpamplona.amethyst.ui.theme.NoSoTinyBorders
import com.vitorpamplona.amethyst.ui.theme.ReactionRowExpandButton import com.vitorpamplona.amethyst.ui.theme.ReactionRowExpandButton
@@ -135,13 +134,14 @@ fun ReactionsRow(
InnerReactionRow(baseNote, showReactionDetail, wantsToSeeReactions, accountViewModel, nav) InnerReactionRow(baseNote, showReactionDetail, wantsToSeeReactions, accountViewModel, nav)
Spacer(modifier = HalfDoubleVertSpacer)
LoadAndDisplayZapraiser(baseNote, showReactionDetail, wantsToSeeReactions, accountViewModel) LoadAndDisplayZapraiser(baseNote, showReactionDetail, wantsToSeeReactions, accountViewModel)
if (showReactionDetail && wantsToSeeReactions.value) { if (showReactionDetail && wantsToSeeReactions.value) {
ReactionDetailGallery(baseNote, nav, accountViewModel) ReactionDetailGallery(baseNote, nav, accountViewModel)
Spacer(modifier = HalfDoubleVertSpacer)
} }
Spacer(modifier = HalfDoubleVertSpacer)
} }
@Composable @Composable
@@ -251,7 +251,6 @@ private fun LoadAndDisplayZapraiser(
} }
if (zapraiserAmount > 0) { if (zapraiserAmount > 0) {
Spacer(modifier = Height4dpModifier)
Box( Box(
modifier = remember { modifier = remember {
ReactionRowZapraiserSize ReactionRowZapraiserSize
@@ -73,6 +73,7 @@ import com.vitorpamplona.amethyst.ui.actions.SaveButton
import com.vitorpamplona.amethyst.ui.qrcode.SimpleQrCodeScanner import com.vitorpamplona.amethyst.ui.qrcode.SimpleQrCodeScanner
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
import com.vitorpamplona.amethyst.ui.screen.loggedIn.TextSpinner import com.vitorpamplona.amethyst.ui.screen.loggedIn.TextSpinner
import com.vitorpamplona.amethyst.ui.screen.loggedIn.TitleExplainer
import com.vitorpamplona.amethyst.ui.screen.loggedIn.getFragmentActivity import com.vitorpamplona.amethyst.ui.screen.loggedIn.getFragmentActivity
import com.vitorpamplona.amethyst.ui.theme.ButtonBorder import com.vitorpamplona.amethyst.ui.theme.ButtonBorder
import com.vitorpamplona.amethyst.ui.theme.Font14SP import com.vitorpamplona.amethyst.ui.theme.Font14SP
@@ -219,8 +220,7 @@ fun UpdateZapAmountDialog(
Triple(LnZapEvent.ZapType.NONZAP, stringResource(id = R.string.zap_type_nonzap), stringResource(id = R.string.zap_type_nonzap_explainer)) Triple(LnZapEvent.ZapType.NONZAP, stringResource(id = R.string.zap_type_nonzap), stringResource(id = R.string.zap_type_nonzap_explainer))
) )
val zapOptions = remember { zapTypes.map { it.second }.toImmutableList() } val zapOptions = remember { zapTypes.map { TitleExplainer(it.second, it.third) }.toImmutableList() }
val zapOptionExplainers = remember { zapTypes.map { it.third }.toImmutableList() }
LaunchedEffect(accountViewModel, nip47uri) { LaunchedEffect(accountViewModel, nip47uri) {
postViewModel.load() postViewModel.load()
@@ -361,7 +361,6 @@ fun UpdateZapAmountDialog(
placeholder = zapTypes.filter { it.first == accountViewModel.defaultZapType() } placeholder = zapTypes.filter { it.first == accountViewModel.defaultZapType() }
.first().second, .first().second,
options = zapOptions, options = zapOptions,
explainers = zapOptionExplainers,
onSelect = { onSelect = {
postViewModel.selectedZapType = zapTypes[it].first postViewModel.selectedZapType = zapTypes[it].first
}, },
@@ -47,6 +47,7 @@ import com.vitorpamplona.amethyst.service.ZapPaymentHandler
import com.vitorpamplona.amethyst.ui.actions.CloseButton import com.vitorpamplona.amethyst.ui.actions.CloseButton
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
import com.vitorpamplona.amethyst.ui.screen.loggedIn.TextSpinner import com.vitorpamplona.amethyst.ui.screen.loggedIn.TextSpinner
import com.vitorpamplona.amethyst.ui.screen.loggedIn.TitleExplainer
import com.vitorpamplona.amethyst.ui.theme.ButtonBorder import com.vitorpamplona.amethyst.ui.theme.ButtonBorder
import com.vitorpamplona.amethyst.ui.theme.DoubleHorzSpacer import com.vitorpamplona.amethyst.ui.theme.DoubleHorzSpacer
import com.vitorpamplona.amethyst.ui.theme.DoubleVertSpacer import com.vitorpamplona.amethyst.ui.theme.DoubleVertSpacer
@@ -107,8 +108,7 @@ fun ZapCustomDialog(
Triple(LnZapEvent.ZapType.NONZAP, stringResource(id = R.string.zap_type_nonzap), stringResource(id = R.string.zap_type_nonzap_explainer)) Triple(LnZapEvent.ZapType.NONZAP, stringResource(id = R.string.zap_type_nonzap), stringResource(id = R.string.zap_type_nonzap_explainer))
) )
val zapOptions = remember { zapTypes.map { it.second }.toImmutableList() } val zapOptions = remember { zapTypes.map { TitleExplainer(it.second, it.third) }.toImmutableList() }
val zapOptionExplainers = remember { zapTypes.map { it.third }.toImmutableList() }
var selectedZapType by remember(accountViewModel) { mutableStateOf(accountViewModel.account.defaultZapType) } var selectedZapType by remember(accountViewModel) { mutableStateOf(accountViewModel.account.defaultZapType) }
Dialog( Dialog(
@@ -181,7 +181,6 @@ fun ZapCustomDialog(
label = stringResource(id = R.string.zap_type), label = stringResource(id = R.string.zap_type),
placeholder = zapTypes.filter { it.first == accountViewModel.account.defaultZapType }.first().second, placeholder = zapTypes.filter { it.first == accountViewModel.account.defaultZapType }.first().second,
options = zapOptions, options = zapOptions,
explainers = zapOptionExplainers,
onSelect = { onSelect = {
selectedZapType = zapTypes[it].first selectedZapType = zapTypes[it].first
}, },
@@ -56,7 +56,7 @@ fun ReportNoteDialog(note: Note, accountViewModel: AccountViewModel, onDismiss:
Pair(ReportEvent.ReportType.ILLEGAL, stringResource(R.string.report_dialog_illegal)) Pair(ReportEvent.ReportType.ILLEGAL, stringResource(R.string.report_dialog_illegal))
) )
val reasonOptions = remember { reportTypes.map { it.second }.toImmutableList() } val reasonOptions = remember { reportTypes.map { TitleExplainer(it.second) }.toImmutableList() }
var additionalReason by remember { mutableStateOf("") } var additionalReason by remember { mutableStateOf("") }
var selectedReason by remember { mutableStateOf(-1) } var selectedReason by remember { mutableStateOf(-1) }
@@ -111,15 +111,15 @@ fun SettingsScreen(
) { ) {
val scope = rememberCoroutineScope() val scope = rememberCoroutineScope()
val selectedItens = persistentListOf( val selectedItens = persistentListOf(
stringResource(ConnectivityType.ALWAYS.reourceId), TitleExplainer(stringResource(ConnectivityType.ALWAYS.reourceId)),
stringResource(ConnectivityType.WIFI_ONLY.reourceId), TitleExplainer(stringResource(ConnectivityType.WIFI_ONLY.reourceId)),
stringResource(ConnectivityType.NEVER.reourceId) TitleExplainer(stringResource(ConnectivityType.NEVER.reourceId))
) )
val themeItens = persistentListOf( val themeItens = persistentListOf(
stringResource(R.string.system), TitleExplainer(stringResource(R.string.system)),
stringResource(R.string.light), TitleExplainer(stringResource(R.string.light)),
stringResource(R.string.dark) TitleExplainer(stringResource(R.string.dark))
) )
val settings = accountViewModel.account.settings val settings = accountViewModel.account.settings
@@ -132,7 +132,7 @@ fun SettingsScreen(
val context = LocalContext.current val context = LocalContext.current
val languageEntries = context.getLangPreferenceDropdownEntries() val languageEntries = context.getLangPreferenceDropdownEntries()
val languageList = languageEntries.keys.toImmutableList() val languageList = languageEntries.keys.map { TitleExplainer(it) }.toImmutableList()
val languageIndex = getLanguageIndex(languageEntries) val languageIndex = getLanguageIndex(languageEntries)
Column( Column(
@@ -149,12 +149,12 @@ fun SettingsScreen(
) { ) {
GlobalScope.launch(Dispatchers.Main) { GlobalScope.launch(Dispatchers.Main) {
val job = scope.launch(Dispatchers.IO) { val job = scope.launch(Dispatchers.IO) {
val locale = languageEntries[languageList[it]] val locale = languageEntries[languageList[it].title]
accountViewModel.account.settings.preferredLanguage = locale accountViewModel.account.settings.preferredLanguage = locale
LocalPreferences.saveToEncryptedStorage(accountViewModel.account) LocalPreferences.saveToEncryptedStorage(accountViewModel.account)
} }
job.join() job.join()
val appLocale: LocaleListCompat = LocaleListCompat.forLanguageTags(languageEntries[languageList[it]]) val appLocale: LocaleListCompat = LocaleListCompat.forLanguageTags(languageEntries[languageList[it].title])
AppCompatDelegate.setApplicationLocales(appLocale) AppCompatDelegate.setApplicationLocales(appLocale)
} }
} }
@@ -227,7 +227,7 @@ fun SettingsScreen(
fun SettingsRow( fun SettingsRow(
name: Int, name: Int,
description: Int, description: Int,
selectedItens: ImmutableList<String>, selectedItens: ImmutableList<TitleExplainer>,
selectedIndex: Int, selectedIndex: Int,
onSelect: (Int) -> Unit onSelect: (Int) -> Unit
) { ) {
@@ -255,7 +255,7 @@ fun SettingsRow(
TextSpinner( TextSpinner(
label = "", label = "",
placeholder = selectedItens[selectedIndex], placeholder = selectedItens[selectedIndex].title,
options = selectedItens, options = selectedItens,
onSelect = onSelect, onSelect = onSelect,
modifier = Modifier modifier = Modifier