Merge branch 'vitorpamplona:main' into main
This commit is contained in:
+1
-1
@@ -115,7 +115,7 @@ dependencies {
|
|||||||
implementation "androidx.biometric:biometric-ktx:1.2.0-alpha05"
|
implementation "androidx.biometric:biometric-ktx:1.2.0-alpha05"
|
||||||
|
|
||||||
// Bitcoin secp256k1 bindings to Android
|
// Bitcoin secp256k1 bindings to Android
|
||||||
implementation 'fr.acinq.secp256k1:secp256k1-kmp-jni-android:0.9.0'
|
implementation 'fr.acinq.secp256k1:secp256k1-kmp-jni-android:0.10.0'
|
||||||
|
|
||||||
// Nostr Base Protocol
|
// Nostr Base Protocol
|
||||||
implementation('com.github.vitorpamplona.NostrPostr:nostrpostrlib:master-SNAPSHOT') {
|
implementation('com.github.vitorpamplona.NostrPostr:nostrpostrlib:master-SNAPSHOT') {
|
||||||
|
|||||||
@@ -877,23 +877,28 @@ class Account(
|
|||||||
null
|
null
|
||||||
}
|
}
|
||||||
|
|
||||||
if (altPrivateKeyToUse != null && altPubkeyToUse != null) {
|
try {
|
||||||
val altPubKeyFromPrivate = Utils.pubkeyCreate(altPrivateKeyToUse).toHexKey()
|
if (altPrivateKeyToUse != null && altPubkeyToUse != null) {
|
||||||
|
val altPubKeyFromPrivate = Utils.pubkeyCreate(altPrivateKeyToUse).toHexKey()
|
||||||
|
|
||||||
if (altPubKeyFromPrivate == event.pubKey) {
|
if (altPubKeyFromPrivate == event.pubKey) {
|
||||||
val result = event.getPrivateZapEvent(altPrivateKeyToUse, altPubkeyToUse)
|
val result = event.getPrivateZapEvent(altPrivateKeyToUse, altPubkeyToUse)
|
||||||
|
|
||||||
if (result == null) {
|
if (result == null) {
|
||||||
Log.w(
|
Log.w(
|
||||||
"Private ZAP Decrypt",
|
"Private ZAP Decrypt",
|
||||||
"Fail to decrypt Zap from ${note.author?.toBestDisplayName()} ${note.idNote()}"
|
"Fail to decrypt Zap from ${note.author?.toBestDisplayName()} ${note.idNote()}"
|
||||||
)
|
)
|
||||||
|
}
|
||||||
|
result
|
||||||
|
} else {
|
||||||
|
null
|
||||||
}
|
}
|
||||||
result
|
|
||||||
} else {
|
} else {
|
||||||
null
|
null
|
||||||
}
|
}
|
||||||
} else {
|
} catch (e: Exception) {
|
||||||
|
Log.e("Account", "Failed to create pubkey for ZapRequest ${event.id}", e)
|
||||||
null
|
null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,7 +2,6 @@ package com.vitorpamplona.amethyst.model
|
|||||||
|
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
import androidx.core.net.toUri
|
import androidx.core.net.toUri
|
||||||
import androidx.lifecycle.LiveData
|
|
||||||
import com.fasterxml.jackson.databind.DeserializationFeature
|
import com.fasterxml.jackson.databind.DeserializationFeature
|
||||||
import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper
|
import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper
|
||||||
import com.vitorpamplona.amethyst.Amethyst
|
import com.vitorpamplona.amethyst.Amethyst
|
||||||
@@ -11,6 +10,8 @@ import com.vitorpamplona.amethyst.service.relays.Relay
|
|||||||
import com.vitorpamplona.amethyst.ui.components.BundledInsert
|
import com.vitorpamplona.amethyst.ui.components.BundledInsert
|
||||||
import fr.acinq.secp256k1.Hex
|
import fr.acinq.secp256k1.Hex
|
||||||
import kotlinx.coroutines.*
|
import kotlinx.coroutines.*
|
||||||
|
import kotlinx.coroutines.flow.MutableSharedFlow
|
||||||
|
import kotlinx.coroutines.flow.asSharedFlow
|
||||||
import nostr.postr.toNpub
|
import nostr.postr.toNpub
|
||||||
import java.io.ByteArrayInputStream
|
import java.io.ByteArrayInputStream
|
||||||
import java.io.File
|
import java.io.File
|
||||||
@@ -969,16 +970,16 @@ object LocalCache {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class LocalCacheLiveData : LiveData<Set<Note>>(setOf<Note>()) {
|
class LocalCacheLiveData {
|
||||||
|
private val _newEventBundles = MutableSharedFlow<Set<Note>>()
|
||||||
|
val newEventBundles = _newEventBundles.asSharedFlow() // read-only public view
|
||||||
|
|
||||||
// Refreshes observers in batches.
|
// Refreshes observers in batches.
|
||||||
private val bundler = BundledInsert<Note>(300, Dispatchers.IO)
|
private val bundler = BundledInsert<Note>(300, Dispatchers.IO)
|
||||||
|
|
||||||
fun invalidateData(newNote: Note) {
|
fun invalidateData(newNote: Note) {
|
||||||
bundler.invalidateList(newNote) { bundledNewNotes ->
|
bundler.invalidateList(newNote) { bundledNewNotes ->
|
||||||
if (hasActiveObservers()) {
|
_newEventBundles.emit(bundledNewNotes)
|
||||||
postValue(bundledNewNotes)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -137,9 +137,6 @@ private fun RenderSeach(
|
|||||||
|
|
||||||
val onlineSearch = NostrSearchEventOrUserDataSource
|
val onlineSearch = NostrSearchEventOrUserDataSource
|
||||||
|
|
||||||
val dbState = LocalCache.live.observeAsState()
|
|
||||||
val db = dbState.value ?: return
|
|
||||||
|
|
||||||
val lifeCycleOwner = LocalLifecycleOwner.current
|
val lifeCycleOwner = LocalLifecycleOwner.current
|
||||||
|
|
||||||
// Create a channel for processing search queries.
|
// Create a channel for processing search queries.
|
||||||
@@ -147,10 +144,12 @@ private fun RenderSeach(
|
|||||||
Channel<String>(Channel.CONFLATED)
|
Channel<String>(Channel.CONFLATED)
|
||||||
}
|
}
|
||||||
|
|
||||||
LaunchedEffect(db) {
|
LaunchedEffect(Unit) {
|
||||||
withContext(Dispatchers.IO) {
|
scope.launch(Dispatchers.IO) {
|
||||||
if (searchBarViewModel.isSearching()) {
|
LocalCache.live.newEventBundles.collect {
|
||||||
searchBarViewModel.invalidateData()
|
if (searchBarViewModel.isSearching()) {
|
||||||
|
searchBarViewModel.invalidateData()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ package com.vitorpamplona.amethyst.ui.actions
|
|||||||
import android.graphics.Bitmap
|
import android.graphics.Bitmap
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
|
import android.util.Log
|
||||||
import android.util.Size
|
import android.util.Size
|
||||||
import android.widget.Toast
|
import android.widget.Toast
|
||||||
import androidx.compose.foundation.BorderStroke
|
import androidx.compose.foundation.BorderStroke
|
||||||
@@ -82,6 +83,7 @@ fun NewPostView(onClose: () -> Unit, baseReplyTo: Note? = null, quote: Note? = n
|
|||||||
val keyboardController = LocalSoftwareKeyboardController.current
|
val keyboardController = LocalSoftwareKeyboardController.current
|
||||||
|
|
||||||
val scroolState = rememberScrollState()
|
val scroolState = rememberScrollState()
|
||||||
|
val scope = rememberCoroutineScope()
|
||||||
|
|
||||||
LaunchedEffect(Unit) {
|
LaunchedEffect(Unit) {
|
||||||
postViewModel.load(account, baseReplyTo, quote)
|
postViewModel.load(account, baseReplyTo, quote)
|
||||||
@@ -218,6 +220,11 @@ fun NewPostView(onClose: () -> Unit, baseReplyTo: Note? = null, quote: Note? = n
|
|||||||
},
|
},
|
||||||
onCancel = {
|
onCancel = {
|
||||||
postViewModel.contentToAddUrl = null
|
postViewModel.contentToAddUrl = null
|
||||||
|
},
|
||||||
|
onError = {
|
||||||
|
scope.launch {
|
||||||
|
postViewModel.imageUploadingError.emit(it)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -641,7 +648,8 @@ fun ImageVideoDescription(
|
|||||||
uri: Uri,
|
uri: Uri,
|
||||||
defaultServer: ServersAvailable,
|
defaultServer: ServersAvailable,
|
||||||
onAdd: (String, ServersAvailable) -> Unit,
|
onAdd: (String, ServersAvailable) -> Unit,
|
||||||
onCancel: () -> Unit
|
onCancel: () -> Unit,
|
||||||
|
onError: (String) -> Unit
|
||||||
) {
|
) {
|
||||||
val resolver = LocalContext.current.contentResolver
|
val resolver = LocalContext.current.contentResolver
|
||||||
val mediaType = resolver.getType(uri) ?: ""
|
val mediaType = resolver.getType(uri) ?: ""
|
||||||
@@ -749,7 +757,12 @@ fun ImageVideoDescription(
|
|||||||
|
|
||||||
LaunchedEffect(key1 = uri) {
|
LaunchedEffect(key1 = uri) {
|
||||||
scope.launch(Dispatchers.IO) {
|
scope.launch(Dispatchers.IO) {
|
||||||
bitmap = resolver.loadThumbnail(uri, Size(1200, 1000), null)
|
try {
|
||||||
|
bitmap = resolver.loadThumbnail(uri, Size(1200, 1000), null)
|
||||||
|
} catch (e: Exception) {
|
||||||
|
onError("Unable to load file")
|
||||||
|
Log.e("NewPostView", "Couldn't create thumbnail for $uri")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -8,8 +8,8 @@ import kotlinx.coroutines.NonCancellable
|
|||||||
import kotlinx.coroutines.delay
|
import kotlinx.coroutines.delay
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
|
import java.util.concurrent.LinkedBlockingQueue
|
||||||
import java.util.concurrent.atomic.AtomicBoolean
|
import java.util.concurrent.atomic.AtomicBoolean
|
||||||
import java.util.concurrent.atomic.AtomicReference
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* This class is designed to have a waiting time between two calls of invalidate
|
* This class is designed to have a waiting time between two calls of invalidate
|
||||||
@@ -54,23 +54,26 @@ class BundledInsert<T>(
|
|||||||
val dispatcher: CoroutineDispatcher = Dispatchers.Default
|
val dispatcher: CoroutineDispatcher = Dispatchers.Default
|
||||||
) {
|
) {
|
||||||
private var onlyOneInBlock = AtomicBoolean()
|
private var onlyOneInBlock = AtomicBoolean()
|
||||||
private var atomicSet = AtomicReference<Set<T>>(setOf<T>())
|
private var queue = LinkedBlockingQueue<T>()
|
||||||
|
|
||||||
fun invalidateList(newObject: T, onUpdate: (Set<T>) -> Unit) {
|
|
||||||
atomicSet.updateAndGet() {
|
|
||||||
it + newObject
|
|
||||||
}
|
|
||||||
|
|
||||||
|
fun invalidateList(newObject: T, onUpdate: suspend (Set<T>) -> Unit) {
|
||||||
|
queue.put(newObject)
|
||||||
if (onlyOneInBlock.getAndSet(true)) {
|
if (onlyOneInBlock.getAndSet(true)) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
val scope = CoroutineScope(Job() + dispatcher)
|
val scope = CoroutineScope(Job() + dispatcher)
|
||||||
scope.launch {
|
scope.launch(Dispatchers.IO) {
|
||||||
try {
|
try {
|
||||||
onUpdate(atomicSet.getAndSet(emptySet()))
|
val mySet = mutableSetOf<T>()
|
||||||
|
queue.drainTo(mySet)
|
||||||
|
onUpdate(mySet)
|
||||||
|
|
||||||
delay(delay)
|
delay(delay)
|
||||||
onUpdate(atomicSet.getAndSet(emptySet()))
|
|
||||||
|
mySet.clear()
|
||||||
|
queue.drainTo(mySet)
|
||||||
|
onUpdate(mySet)
|
||||||
} finally {
|
} finally {
|
||||||
withContext(NonCancellable) {
|
withContext(NonCancellable) {
|
||||||
onlyOneInBlock.set(false)
|
onlyOneInBlock.set(false)
|
||||||
|
|||||||
@@ -44,11 +44,13 @@ import com.vitorpamplona.amethyst.service.nip19.Nip19
|
|||||||
import com.vitorpamplona.amethyst.ui.note.NoteCompose
|
import com.vitorpamplona.amethyst.ui.note.NoteCompose
|
||||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
import java.net.MalformedURLException
|
import java.net.MalformedURLException
|
||||||
import java.net.URISyntaxException
|
import java.net.URISyntaxException
|
||||||
import java.net.URL
|
import java.net.URL
|
||||||
import java.util.regex.Pattern
|
import java.util.regex.Pattern
|
||||||
|
import kotlin.time.ExperimentalTime
|
||||||
|
|
||||||
val imageExtensions = listOf("png", "jpg", "gif", "bmp", "jpeg", "webp", "svg")
|
val imageExtensions = listOf("png", "jpg", "gif", "bmp", "jpeg", "webp", "svg")
|
||||||
val videoExtensions = listOf("mp4", "avi", "wmv", "mpg", "amv", "webm", "mov", "mp3")
|
val videoExtensions = listOf("mp4", "avi", "wmv", "mpg", "amv", "webm", "mov", "mp3")
|
||||||
@@ -106,11 +108,12 @@ fun RichTextViewer(
|
|||||||
|
|
||||||
class RichTextViewerState(
|
class RichTextViewerState(
|
||||||
val content: String,
|
val content: String,
|
||||||
val urlSet: LinkedHashSet<String>,
|
val urlSet: Set<String>,
|
||||||
val imagesForPager: Map<String, ZoomableUrlContent>,
|
val imagesForPager: Map<String, ZoomableUrlContent>,
|
||||||
val imageList: List<ZoomableUrlContent>
|
val imageList: List<ZoomableUrlContent>
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@OptIn(ExperimentalTime::class)
|
||||||
@Composable
|
@Composable
|
||||||
private fun RenderRegular(
|
private fun RenderRegular(
|
||||||
content: String,
|
content: String,
|
||||||
@@ -121,11 +124,13 @@ private fun RenderRegular(
|
|||||||
navController: NavController
|
navController: NavController
|
||||||
) {
|
) {
|
||||||
var processedState by remember {
|
var processedState by remember {
|
||||||
mutableStateOf<RichTextViewerState?>(null)
|
mutableStateOf<RichTextViewerState?>(RichTextViewerState(content, emptySet(), emptyMap(), emptyList()))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
val scope = rememberCoroutineScope()
|
||||||
|
|
||||||
LaunchedEffect(key1 = content) {
|
LaunchedEffect(key1 = content) {
|
||||||
withContext(Dispatchers.IO) {
|
scope.launch(Dispatchers.IO) {
|
||||||
val urls = UrlDetector(content, UrlDetectorOptions.Default).detect()
|
val urls = UrlDetector(content, UrlDetectorOptions.Default).detect()
|
||||||
val urlSet = urls.mapTo(LinkedHashSet(urls.size)) { it.originalUrl }
|
val urlSet = urls.mapTo(LinkedHashSet(urls.size)) { it.originalUrl }
|
||||||
val imagesForPager = urlSet.mapNotNull { fullUrl ->
|
val imagesForPager = urlSet.mapNotNull { fullUrl ->
|
||||||
@@ -140,7 +145,9 @@ private fun RenderRegular(
|
|||||||
}.associateBy { it.url }
|
}.associateBy { it.url }
|
||||||
val imageList = imagesForPager.values.toList()
|
val imageList = imagesForPager.values.toList()
|
||||||
|
|
||||||
processedState = RichTextViewerState(content, urlSet, imagesForPager, imageList)
|
if (urlSet.isNotEmpty()) {
|
||||||
|
processedState = RichTextViewerState(content, urlSet, imagesForPager, imageList)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -271,10 +271,6 @@ private fun UrlImageView(
|
|||||||
mutableStateOf<Boolean?>(null)
|
mutableStateOf<Boolean?>(null)
|
||||||
}
|
}
|
||||||
|
|
||||||
val ratio = remember {
|
|
||||||
aspectRatio(content.dim)
|
|
||||||
}
|
|
||||||
|
|
||||||
LaunchedEffect(key1 = content.url, key2 = imageState) {
|
LaunchedEffect(key1 = content.url, key2 = imageState) {
|
||||||
if (imageState is AsyncImagePainter.State.Success) {
|
if (imageState is AsyncImagePainter.State.Success) {
|
||||||
scope.launch(Dispatchers.IO) {
|
scope.launch(Dispatchers.IO) {
|
||||||
@@ -284,10 +280,10 @@ private fun UrlImageView(
|
|||||||
}
|
}
|
||||||
|
|
||||||
BoxWithConstraints(contentAlignment = Alignment.Center) {
|
BoxWithConstraints(contentAlignment = Alignment.Center) {
|
||||||
val myModifier = mainImageModifier.also {
|
val myModifier = mainImageModifier.run {
|
||||||
if (ratio != null) {
|
aspectRatio(content.dim)?.let { ratio ->
|
||||||
it.aspectRatio(ratio, maxHeight.isFinite)
|
this.aspectRatio(ratio, maxHeight.isFinite)
|
||||||
}
|
} ?: this
|
||||||
}
|
}
|
||||||
val contentScale = if (maxHeight.isFinite) ContentScale.Fit else ContentScale.FillWidth
|
val contentScale = if (maxHeight.isFinite) ContentScale.Fit else ContentScale.FillWidth
|
||||||
|
|
||||||
|
|||||||
@@ -41,7 +41,6 @@ import com.vitorpamplona.amethyst.model.LocalCache
|
|||||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import kotlin.time.ExperimentalTime
|
|
||||||
|
|
||||||
val bottomNavigationItems = listOf(
|
val bottomNavigationItems = listOf(
|
||||||
Route.Home,
|
Route.Home,
|
||||||
@@ -136,9 +135,10 @@ fun AppBottomBar(navController: NavHostController, accountViewModel: AccountView
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@OptIn(ExperimentalTime::class)
|
|
||||||
@Composable
|
@Composable
|
||||||
private fun NotifiableIcon(route: Route, selected: Boolean, accountViewModel: AccountViewModel) {
|
private fun NotifiableIcon(route: Route, selected: Boolean, accountViewModel: AccountViewModel) {
|
||||||
|
val scope = rememberCoroutineScope()
|
||||||
|
|
||||||
Box(Modifier.size(if ("Home" == route.base) 25.dp else 23.dp)) {
|
Box(Modifier.size(if ("Home" == route.base) 25.dp else 23.dp)) {
|
||||||
Icon(
|
Icon(
|
||||||
painter = painterResource(id = route.icon),
|
painter = painterResource(id = route.icon),
|
||||||
@@ -150,15 +150,10 @@ private fun NotifiableIcon(route: Route, selected: Boolean, accountViewModel: Ac
|
|||||||
val accountState by accountViewModel.accountLiveData.observeAsState()
|
val accountState by accountViewModel.accountLiveData.observeAsState()
|
||||||
val account = accountState?.account ?: return
|
val account = accountState?.account ?: return
|
||||||
|
|
||||||
// Notification
|
|
||||||
val dbState = LocalCache.live.observeAsState()
|
|
||||||
val db = dbState.value ?: return
|
|
||||||
|
|
||||||
val notifState = NotificationCache.live.observeAsState()
|
val notifState = NotificationCache.live.observeAsState()
|
||||||
val notif = notifState.value ?: return
|
val notif = notifState.value ?: return
|
||||||
|
|
||||||
var hasNewItems by remember { mutableStateOf<Boolean>(false) }
|
var hasNewItems by remember { mutableStateOf<Boolean>(false) }
|
||||||
val scope = rememberCoroutineScope()
|
|
||||||
|
|
||||||
LaunchedEffect(key1 = notif) {
|
LaunchedEffect(key1 = notif) {
|
||||||
scope.launch(Dispatchers.IO) {
|
scope.launch(Dispatchers.IO) {
|
||||||
@@ -166,9 +161,11 @@ private fun NotifiableIcon(route: Route, selected: Boolean, accountViewModel: Ac
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
LaunchedEffect(key1 = db) {
|
LaunchedEffect(Unit) {
|
||||||
scope.launch(Dispatchers.IO) {
|
scope.launch(Dispatchers.IO) {
|
||||||
hasNewItems = route.hasNewItems(account, notif.cache, db)
|
LocalCache.live.newEventBundles.collect {
|
||||||
|
hasNewItems = route.hasNewItems(account, notif.cache, it)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -24,7 +24,6 @@ import androidx.compose.material.TopAppBar
|
|||||||
import androidx.compose.material.icons.Icons
|
import androidx.compose.material.icons.Icons
|
||||||
import androidx.compose.material.icons.filled.ArrowBack
|
import androidx.compose.material.icons.filled.ArrowBack
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.LaunchedEffect
|
|
||||||
import androidx.compose.runtime.derivedStateOf
|
import androidx.compose.runtime.derivedStateOf
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.runtime.livedata.observeAsState
|
import androidx.compose.runtime.livedata.observeAsState
|
||||||
@@ -40,6 +39,8 @@ import androidx.compose.ui.platform.LocalContext
|
|||||||
import androidx.compose.ui.res.painterResource
|
import androidx.compose.ui.res.painterResource
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
|
import androidx.lifecycle.ViewModel
|
||||||
|
import androidx.lifecycle.viewModelScope
|
||||||
import androidx.lifecycle.viewmodel.compose.viewModel
|
import androidx.lifecycle.viewmodel.compose.viewModel
|
||||||
import androidx.navigation.NavHostController
|
import androidx.navigation.NavHostController
|
||||||
import coil.Coil
|
import coil.Coil
|
||||||
@@ -71,43 +72,45 @@ import com.vitorpamplona.amethyst.ui.components.RobohashAsyncImageProxy
|
|||||||
import com.vitorpamplona.amethyst.ui.screen.RelayPoolViewModel
|
import com.vitorpamplona.amethyst.ui.screen.RelayPoolViewModel
|
||||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
||||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.SpinnerSelectionDialog
|
import com.vitorpamplona.amethyst.ui.screen.loggedIn.SpinnerSelectionDialog
|
||||||
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.Job
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun AppTopBar(navController: NavHostController, scaffoldState: ScaffoldState, accountViewModel: AccountViewModel) {
|
fun AppTopBar(followLists: FollowListViewModel, navController: NavHostController, scaffoldState: ScaffoldState, accountViewModel: AccountViewModel) {
|
||||||
when (currentRoute(navController)?.substringBefore("?")) {
|
when (currentRoute(navController)?.substringBefore("?")) {
|
||||||
// Route.Profile.route -> TopBarWithBackButton(navController)
|
// Route.Profile.route -> TopBarWithBackButton(navController)
|
||||||
Route.Home.base -> HomeTopBar(scaffoldState, accountViewModel)
|
Route.Home.base -> HomeTopBar(followLists, scaffoldState, accountViewModel)
|
||||||
Route.Video.base -> StoriesTopBar(scaffoldState, accountViewModel)
|
Route.Video.base -> StoriesTopBar(followLists, scaffoldState, accountViewModel)
|
||||||
Route.Notification.base -> NotificationTopBar(scaffoldState, accountViewModel)
|
Route.Notification.base -> NotificationTopBar(followLists, scaffoldState, accountViewModel)
|
||||||
else -> MainTopBar(scaffoldState, accountViewModel)
|
else -> MainTopBar(scaffoldState, accountViewModel)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun StoriesTopBar(scaffoldState: ScaffoldState, accountViewModel: AccountViewModel) {
|
fun StoriesTopBar(followLists: FollowListViewModel, scaffoldState: ScaffoldState, accountViewModel: AccountViewModel) {
|
||||||
GenericTopBar(scaffoldState, accountViewModel) { account ->
|
GenericTopBar(scaffoldState, accountViewModel) { account ->
|
||||||
FollowList(account.defaultStoriesFollowList, account.userProfile(), true) { listName ->
|
FollowList(followLists, account.defaultStoriesFollowList, account.userProfile(), true) { listName ->
|
||||||
account.changeDefaultStoriesFollowList(listName)
|
account.changeDefaultStoriesFollowList(listName)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun HomeTopBar(scaffoldState: ScaffoldState, accountViewModel: AccountViewModel) {
|
fun HomeTopBar(followLists: FollowListViewModel, scaffoldState: ScaffoldState, accountViewModel: AccountViewModel) {
|
||||||
GenericTopBar(scaffoldState, accountViewModel) { account ->
|
GenericTopBar(scaffoldState, accountViewModel) { account ->
|
||||||
FollowList(account.defaultHomeFollowList, account.userProfile(), false) { listName ->
|
FollowList(followLists, account.defaultHomeFollowList, account.userProfile(), false) { listName ->
|
||||||
account.changeDefaultHomeFollowList(listName)
|
account.changeDefaultHomeFollowList(listName)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun NotificationTopBar(scaffoldState: ScaffoldState, accountViewModel: AccountViewModel) {
|
fun NotificationTopBar(followLists: FollowListViewModel, scaffoldState: ScaffoldState, accountViewModel: AccountViewModel) {
|
||||||
GenericTopBar(scaffoldState, accountViewModel) { account ->
|
GenericTopBar(scaffoldState, accountViewModel) { account ->
|
||||||
FollowList(account.defaultNotificationFollowList, account.userProfile(), true) { listName ->
|
FollowList(followLists, account.defaultNotificationFollowList, account.userProfile(), true) { listName ->
|
||||||
account.changeDefaultNotificationFollowList(listName)
|
account.changeDefaultNotificationFollowList(listName)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -237,30 +240,19 @@ private fun LoggedInUserPictureDrawer(
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun FollowList(listName: String, loggedIn: User, withGlobal: Boolean, onChange: (String) -> Unit) {
|
fun FollowList(followListsModel: FollowListViewModel, listName: String, loggedIn: User, withGlobal: Boolean, onChange: (String) -> Unit) {
|
||||||
// Notification
|
|
||||||
val dbState = LocalCache.live.observeAsState()
|
|
||||||
val db = dbState.value ?: return
|
|
||||||
|
|
||||||
val kind3Follow = Pair(KIND3_FOLLOWS, stringResource(id = R.string.follow_list_kind3follows))
|
val kind3Follow = Pair(KIND3_FOLLOWS, stringResource(id = R.string.follow_list_kind3follows))
|
||||||
val globalFollow = Pair(GLOBAL_FOLLOWS, stringResource(id = R.string.follow_list_global))
|
val globalFollow = Pair(GLOBAL_FOLLOWS, stringResource(id = R.string.follow_list_global))
|
||||||
|
|
||||||
val defaultOptions = if (withGlobal) listOf(kind3Follow, globalFollow) else listOf(kind3Follow)
|
val defaultOptions = if (withGlobal) listOf(kind3Follow, globalFollow) else listOf(kind3Follow)
|
||||||
|
|
||||||
var followLists by remember { mutableStateOf(defaultOptions) }
|
val followLists = remember(followListsModel.followLists) {
|
||||||
val followNames = remember { derivedStateOf { followLists.map { it.second } } }
|
(defaultOptions + followListsModel.followLists)
|
||||||
|
}
|
||||||
|
|
||||||
LaunchedEffect(key1 = db) {
|
val followNames = remember(followLists) {
|
||||||
withContext(Dispatchers.IO) {
|
derivedStateOf {
|
||||||
followLists = defaultOptions + LocalCache.addressables.mapNotNull {
|
followLists.map { it.second }
|
||||||
val event = (it.value.event as? PeopleListEvent)
|
|
||||||
// Has to have an list
|
|
||||||
if (event != null && event.pubKey == loggedIn.pubkeyHex && (event.tags.size > 1 || event.content.length > 50)) {
|
|
||||||
Pair(event.dTag(), event.dTag())
|
|
||||||
} else {
|
|
||||||
null
|
|
||||||
}
|
|
||||||
}.sortedBy { it.second }
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -273,6 +265,62 @@ fun FollowList(listName: String, loggedIn: User, withGlobal: Boolean, onChange:
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class FollowListViewModel : ViewModel() {
|
||||||
|
var followLists by mutableStateOf<List<Pair<String, String>>>(emptyList())
|
||||||
|
var account: Account? = null
|
||||||
|
|
||||||
|
fun load(account: Account?) {
|
||||||
|
this.account = account
|
||||||
|
refresh()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun refresh() {
|
||||||
|
val scope = CoroutineScope(Job() + Dispatchers.Default)
|
||||||
|
scope.launch {
|
||||||
|
refreshFollows()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private suspend fun refreshFollows() {
|
||||||
|
val myAccount = account ?: return
|
||||||
|
|
||||||
|
val newFollowLists = LocalCache.addressables.mapNotNull {
|
||||||
|
val event = (it.value.event as? PeopleListEvent)
|
||||||
|
// Has to have an list
|
||||||
|
if (event != null && event.pubKey == myAccount.userProfile().pubkeyHex && (event.tags.size > 1 || event.content.length > 50)) {
|
||||||
|
Pair(event.dTag(), event.dTag())
|
||||||
|
} else {
|
||||||
|
null
|
||||||
|
}
|
||||||
|
}.sortedBy { it.second }
|
||||||
|
|
||||||
|
withContext(Dispatchers.Main) {
|
||||||
|
if (followLists != newFollowLists) {
|
||||||
|
followLists = newFollowLists
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var collectorJob: Job? = null
|
||||||
|
|
||||||
|
init {
|
||||||
|
collectorJob = viewModelScope.launch(Dispatchers.IO) {
|
||||||
|
LocalCache.live.newEventBundles.collect { newNotes ->
|
||||||
|
newNotes.forEach {
|
||||||
|
if (it.event is PeopleListEvent) {
|
||||||
|
refresh()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onCleared() {
|
||||||
|
collectorJob?.cancel()
|
||||||
|
super.onCleared()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun SimpleTextSpinner(
|
fun SimpleTextSpinner(
|
||||||
placeholder: String,
|
placeholder: String,
|
||||||
|
|||||||
@@ -276,8 +276,8 @@ private fun AuthorPictureAndComment(
|
|||||||
(zapRequest.event as? LnZapRequestEvent)?.let {
|
(zapRequest.event as? LnZapRequestEvent)?.let {
|
||||||
val decryptedContent = accountViewModel.decryptZap(zapRequest)
|
val decryptedContent = accountViewModel.decryptZap(zapRequest)
|
||||||
if (decryptedContent != null) {
|
if (decryptedContent != null) {
|
||||||
val author = LocalCache.getOrCreateUser(decryptedContent.pubKey)
|
val newAuthor = LocalCache.getOrCreateUser(decryptedContent.pubKey)
|
||||||
content = Pair(author, decryptedContent.content)
|
content = Pair(newAuthor, decryptedContent.content)
|
||||||
} else {
|
} else {
|
||||||
if (!zapRequest.event?.content().isNullOrBlank()) {
|
if (!zapRequest.event?.content().isNullOrBlank()) {
|
||||||
content = Pair(author, zapRequest.event?.content())
|
content = Pair(author, zapRequest.event?.content())
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ package com.vitorpamplona.amethyst.ui.screen
|
|||||||
import android.util.Log
|
import android.util.Log
|
||||||
import androidx.compose.runtime.mutableStateOf
|
import androidx.compose.runtime.mutableStateOf
|
||||||
import androidx.lifecycle.ViewModel
|
import androidx.lifecycle.ViewModel
|
||||||
|
import androidx.lifecycle.viewModelScope
|
||||||
import com.vitorpamplona.amethyst.model.Account
|
import com.vitorpamplona.amethyst.model.Account
|
||||||
import com.vitorpamplona.amethyst.model.LocalCache
|
import com.vitorpamplona.amethyst.model.LocalCache
|
||||||
import com.vitorpamplona.amethyst.model.Note
|
import com.vitorpamplona.amethyst.model.Note
|
||||||
@@ -222,17 +223,19 @@ open class CardFeedViewModel(val localFilter: FeedFilter<Note>) : ViewModel() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private val cacheListener: (Set<Note>) -> Unit = { newNotes ->
|
var collectorJob: Job? = null
|
||||||
if (localFilter is AdditiveFeedFilter && _feedContent.value is CardFeedState.Loaded) {
|
|
||||||
invalidateInsertData(newNotes)
|
|
||||||
} else {
|
|
||||||
// Refresh Everything
|
|
||||||
invalidateData()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
init {
|
init {
|
||||||
LocalCache.live.observeForever(cacheListener)
|
collectorJob = viewModelScope.launch(Dispatchers.IO) {
|
||||||
|
LocalCache.live.newEventBundles.collect { newNotes ->
|
||||||
|
if (localFilter is AdditiveFeedFilter && _feedContent.value is CardFeedState.Loaded) {
|
||||||
|
invalidateInsertData(newNotes)
|
||||||
|
} else {
|
||||||
|
// Refresh Everything
|
||||||
|
invalidateData()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun clear() {
|
fun clear() {
|
||||||
@@ -242,7 +245,7 @@ open class CardFeedViewModel(val localFilter: FeedFilter<Note>) : ViewModel() {
|
|||||||
|
|
||||||
override fun onCleared() {
|
override fun onCleared() {
|
||||||
clear()
|
clear()
|
||||||
LocalCache.live.removeObserver(cacheListener)
|
collectorJob?.cancel()
|
||||||
super.onCleared()
|
super.onCleared()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ package com.vitorpamplona.amethyst.ui.screen
|
|||||||
|
|
||||||
import androidx.compose.runtime.mutableStateOf
|
import androidx.compose.runtime.mutableStateOf
|
||||||
import androidx.lifecycle.ViewModel
|
import androidx.lifecycle.ViewModel
|
||||||
|
import androidx.lifecycle.viewModelScope
|
||||||
import com.vitorpamplona.amethyst.model.LocalCache
|
import com.vitorpamplona.amethyst.model.LocalCache
|
||||||
import com.vitorpamplona.amethyst.model.Note
|
import com.vitorpamplona.amethyst.model.Note
|
||||||
import com.vitorpamplona.amethyst.ui.components.BundledInsert
|
import com.vitorpamplona.amethyst.ui.components.BundledInsert
|
||||||
@@ -124,21 +125,23 @@ abstract class FeedViewModel(val localFilter: FeedFilter<Note>) : ViewModel() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private val cacheListener: (Set<Note>) -> Unit = { newNotes ->
|
var collectorJob: Job? = null
|
||||||
if (localFilter is AdditiveFeedFilter && _feedContent.value is FeedState.Loaded) {
|
|
||||||
invalidateInsertData(newNotes)
|
init {
|
||||||
} else {
|
collectorJob = viewModelScope.launch(Dispatchers.IO) {
|
||||||
// Refresh Everything
|
LocalCache.live.newEventBundles.collect { newNotes ->
|
||||||
invalidateData()
|
if (localFilter is AdditiveFeedFilter && _feedContent.value is FeedState.Loaded) {
|
||||||
|
invalidateInsertData(newNotes)
|
||||||
|
} else {
|
||||||
|
// Refresh Everything
|
||||||
|
invalidateData()
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
init {
|
|
||||||
LocalCache.live.observeForever(cacheListener)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onCleared() {
|
override fun onCleared() {
|
||||||
LocalCache.live.removeObserver(cacheListener)
|
collectorJob?.cancel()
|
||||||
super.onCleared()
|
super.onCleared()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ package com.vitorpamplona.amethyst.ui.screen
|
|||||||
|
|
||||||
import androidx.compose.runtime.mutableStateOf
|
import androidx.compose.runtime.mutableStateOf
|
||||||
import androidx.lifecycle.ViewModel
|
import androidx.lifecycle.ViewModel
|
||||||
|
import androidx.lifecycle.viewModelScope
|
||||||
import com.vitorpamplona.amethyst.model.LocalCache
|
import com.vitorpamplona.amethyst.model.LocalCache
|
||||||
import com.vitorpamplona.amethyst.model.Note
|
import com.vitorpamplona.amethyst.model.Note
|
||||||
import com.vitorpamplona.amethyst.ui.components.BundledUpdate
|
import com.vitorpamplona.amethyst.ui.components.BundledUpdate
|
||||||
@@ -67,16 +68,18 @@ open class LnZapFeedViewModel(val dataSource: FeedFilter<Pair<Note, Note>>) : Vi
|
|||||||
bundler.invalidate()
|
bundler.invalidate()
|
||||||
}
|
}
|
||||||
|
|
||||||
private val cacheListener: (Set<Note>) -> Unit = { newNotes ->
|
var collectorJob: Job? = null
|
||||||
invalidateData()
|
|
||||||
}
|
|
||||||
|
|
||||||
init {
|
init {
|
||||||
LocalCache.live.observeForever(cacheListener)
|
collectorJob = viewModelScope.launch(Dispatchers.IO) {
|
||||||
|
LocalCache.live.newEventBundles.collect { newNotes ->
|
||||||
|
invalidateData()
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onCleared() {
|
override fun onCleared() {
|
||||||
LocalCache.live.removeObserver(cacheListener)
|
collectorJob?.cancel()
|
||||||
super.onCleared()
|
super.onCleared()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,8 +2,8 @@ package com.vitorpamplona.amethyst.ui.screen
|
|||||||
|
|
||||||
import androidx.compose.runtime.mutableStateOf
|
import androidx.compose.runtime.mutableStateOf
|
||||||
import androidx.lifecycle.ViewModel
|
import androidx.lifecycle.ViewModel
|
||||||
|
import androidx.lifecycle.viewModelScope
|
||||||
import com.vitorpamplona.amethyst.model.LocalCache
|
import com.vitorpamplona.amethyst.model.LocalCache
|
||||||
import com.vitorpamplona.amethyst.model.Note
|
|
||||||
import com.vitorpamplona.amethyst.model.User
|
import com.vitorpamplona.amethyst.model.User
|
||||||
import com.vitorpamplona.amethyst.ui.components.BundledUpdate
|
import com.vitorpamplona.amethyst.ui.components.BundledUpdate
|
||||||
import com.vitorpamplona.amethyst.ui.dal.FeedFilter
|
import com.vitorpamplona.amethyst.ui.dal.FeedFilter
|
||||||
@@ -72,16 +72,18 @@ open class UserFeedViewModel(val dataSource: FeedFilter<User>) : ViewModel() {
|
|||||||
bundler.invalidate()
|
bundler.invalidate()
|
||||||
}
|
}
|
||||||
|
|
||||||
private val cacheListener: (Set<Note>) -> Unit = { newNotes ->
|
var collectorJob: Job? = null
|
||||||
invalidateData()
|
|
||||||
}
|
|
||||||
|
|
||||||
init {
|
init {
|
||||||
LocalCache.live.observeForever(cacheListener)
|
collectorJob = viewModelScope.launch(Dispatchers.IO) {
|
||||||
|
LocalCache.live.newEventBundles.collect { newNotes ->
|
||||||
|
invalidateData()
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onCleared() {
|
override fun onCleared() {
|
||||||
LocalCache.live.removeObserver(cacheListener)
|
collectorJob?.cancel()
|
||||||
super.onCleared()
|
super.onCleared()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -20,8 +20,11 @@ import androidx.compose.material.rememberScaffoldState
|
|||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.collectAsState
|
import androidx.compose.runtime.collectAsState
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.runtime.livedata.observeAsState
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
import androidx.compose.runtime.rememberCoroutineScope
|
import androidx.compose.runtime.rememberCoroutineScope
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.lifecycle.viewmodel.compose.viewModel
|
||||||
import androidx.navigation.NavHostController
|
import androidx.navigation.NavHostController
|
||||||
import androidx.navigation.compose.rememberNavController
|
import androidx.navigation.compose.rememberNavController
|
||||||
import com.vitorpamplona.amethyst.ui.buttons.ChannelFabColumn
|
import com.vitorpamplona.amethyst.ui.buttons.ChannelFabColumn
|
||||||
@@ -50,6 +53,12 @@ fun MainScreen(accountViewModel: AccountViewModel, accountStateViewModel: Accoun
|
|||||||
skipHalfExpanded = true
|
skipHalfExpanded = true
|
||||||
)
|
)
|
||||||
|
|
||||||
|
val accountState by accountViewModel.accountLiveData.observeAsState()
|
||||||
|
val account = remember(accountState) { accountState?.account }
|
||||||
|
|
||||||
|
val followLists: FollowListViewModel = viewModel()
|
||||||
|
followLists.load(account)
|
||||||
|
|
||||||
ModalBottomSheetLayout(
|
ModalBottomSheetLayout(
|
||||||
sheetState = sheetState,
|
sheetState = sheetState,
|
||||||
sheetContent = {
|
sheetContent = {
|
||||||
@@ -64,7 +73,7 @@ fun MainScreen(accountViewModel: AccountViewModel, accountStateViewModel: Accoun
|
|||||||
AppBottomBar(navController, accountViewModel)
|
AppBottomBar(navController, accountViewModel)
|
||||||
},
|
},
|
||||||
topBar = {
|
topBar = {
|
||||||
AppTopBar(navController, scaffoldState, accountViewModel)
|
AppTopBar(followLists, navController, scaffoldState, accountViewModel)
|
||||||
},
|
},
|
||||||
drawerContent = {
|
drawerContent = {
|
||||||
DrawerContent(navController, scaffoldState, sheetState, accountViewModel)
|
DrawerContent(navController, scaffoldState, sheetState, accountViewModel)
|
||||||
|
|||||||
@@ -191,19 +191,17 @@ private fun SearchBar(accountViewModel: AccountViewModel, navController: NavCont
|
|||||||
|
|
||||||
val onlineSearch = NostrSearchEventOrUserDataSource
|
val onlineSearch = NostrSearchEventOrUserDataSource
|
||||||
|
|
||||||
val dbState = LocalCache.live.observeAsState()
|
|
||||||
val db = dbState.value ?: return
|
|
||||||
|
|
||||||
// Create a channel for processing search queries.
|
// Create a channel for processing search queries.
|
||||||
val searchTextChanges = remember {
|
val searchTextChanges = remember {
|
||||||
CoroutineChannel<String>(CoroutineChannel.CONFLATED)
|
CoroutineChannel<String>(CoroutineChannel.CONFLATED)
|
||||||
}
|
}
|
||||||
|
|
||||||
LaunchedEffect(db) {
|
LaunchedEffect(Unit) {
|
||||||
withContext(Dispatchers.IO) {
|
scope.launch(Dispatchers.IO) {
|
||||||
if (searchBarViewModel.isSearching()) {
|
LocalCache.live.newEventBundles.collect {
|
||||||
println("Search Active")
|
if (searchBarViewModel.isSearching()) {
|
||||||
searchBarViewModel.invalidateData()
|
searchBarViewModel.invalidateData()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -333,11 +333,45 @@
|
|||||||
|
|
||||||
<string name="upload_server_relays_nip95">Saját csomópontjaid (NIP-95)</string>
|
<string name="upload_server_relays_nip95">Saját csomópontjaid (NIP-95)</string>
|
||||||
<string name="upload_server_relays_nip95_explainer">A fájlokat a csomópontokra töltik fel és ott tárolják. Rögzített URL-től mentesek (harmadik féltől való függőség). Győződj meg róla, hogy legalább egy NIP-95 csomópont a csomópontlistában szerepel</string>
|
<string name="upload_server_relays_nip95_explainer">A fájlokat a csomópontokra töltik fel és ott tárolják. Rögzített URL-től mentesek (harmadik féltől való függőség). Győződj meg róla, hogy legalább egy NIP-95 csomópont a csomópontlistában szerepel</string>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
<string name="follow_list_selection">Követek Lista</string>
|
<string name="follow_list_selection">Követek Lista</string>
|
||||||
<string name="follow_list_kind3follows">Mindenki akit követek</string>
|
<string name="follow_list_kind3follows">Mindenki akit követek</string>
|
||||||
<string name="follow_list_global">Globális</string>
|
<string name="follow_list_global">Globális</string>
|
||||||
|
<string name="connect_through_your_orbot_setup_markdown">
|
||||||
|
## Kapcsolódj a TOR-on keresztül az Orbot segítségével
|
||||||
|
\n\n1. Telepítsd az [Orbot-ot](https://play.google.com/store/apps/details?id=org.torproject.android)
|
||||||
|
\n2. Indítsd el az Orbot-ot
|
||||||
|
\n3. Az Orbot-ban ellenőrizd a Socks portokat. Az alapbeállítás 9050
|
||||||
|
\n4. Ha szükséges az Orbot-ban változtasd meg a portot
|
||||||
|
\n5. Ezen a felületen állítsd be a Socks portot
|
||||||
|
\n6. Kattíts az Aktiválás gombra, hogy az Orbot-ot átjátszóként használd
|
||||||
|
</string>
|
||||||
|
<string name="orbot_socks_port">Orbot Socks portja</string>
|
||||||
|
<string name="invalid_port_number">Érvénytelen Port szám</string>
|
||||||
|
<string name="use_orbot">Használd az Orbot-ot</string>
|
||||||
|
<string name="disconnect_from_your_orbot_setup">TOR/Orbot Lekapcsolása</string>
|
||||||
|
|
||||||
|
<string name="app_notification_channel_id" translatable="false">DefaultChannelID</string>
|
||||||
|
<string name="app_notification_private_message" translatable="false">New notification arrived</string>
|
||||||
|
|
||||||
|
<string name="app_notification_dms_channel_id" translatable="false">PrivateMessagesID</string>
|
||||||
|
<string name="app_notification_dms_channel_name">Privát üzenetek</string>
|
||||||
|
<string name="app_notification_dms_channel_description">Értesítést küld, amikor privát üzeneted érkezik</string>
|
||||||
|
|
||||||
|
<string name="app_notification_zaps_channel_id" translatable="false">ZapsID</string>
|
||||||
|
<string name="app_notification_zaps_channel_name">Zap-et kaptál</string>
|
||||||
|
<string name="app_notification_zaps_channel_description">Értesítést küld, amikor valaki neked Zap-et küldött</string>
|
||||||
|
<string name="app_notification_zaps_channel_message">%1$s sats</string>
|
||||||
|
<string name="app_notification_zaps_channel_message_from">%1$s -tól</string>
|
||||||
|
<string name="app_notification_zaps_channel_message_for">%1$s -ért</string>
|
||||||
|
|
||||||
|
<string name="reply_notify">Értesítés: </string>
|
||||||
|
|
||||||
|
<string name="channel_list_join_conversation">Csatlakozz a beszélgetéshez</string>
|
||||||
|
<string name="channel_list_user_or_group_id">Felhasználó vagy Csoport azonosító</string>
|
||||||
|
<string name="channel_list_user_or_group_id_demo">npub, nevent vagy hex</string>
|
||||||
|
<string name="channel_list_create_channel">Létrehoz</string>
|
||||||
|
<string name="channel_list_join_channel">Csatlakozás</string>
|
||||||
</resources>
|
</resources>
|
||||||
|
|||||||
@@ -0,0 +1 @@
|
|||||||
|
<p><i>Amethyst</i> apporte le meilleur réseau social sur votre téléphone Android. Insérez simplement votre clé privée Nostr et commencez à poster.</p><p><a href='https://github.com/nostr-protocol/nostr' target='_blank' rel='nofollow noopener'>Nostr</a> est un protocole ouvert qui permet de créer un réseau "social" mondial résistant à la censure, en transférant des notes et d'autres éléments à l'aide de relais. Il ne repose pas sur un serveur central de confiance, ce qui le rend résistant; il est basé sur des clés et des signatures cryptographiques, ce qui le rend inviolable; il ne repose pas sur des techniques P2P, ce qui le rend efficace.
|
||||||
Binary file not shown.
|
After Width: | Height: | Size: 30 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 737 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 310 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 212 KiB |
@@ -0,0 +1 @@
|
|||||||
|
Nostr Client pour Android
|
||||||
Reference in New Issue
Block a user