mirror of
https://github.com/vitorpamplona/amethyst.git
synced 2026-07-22 15:52:22 +00:00
Compare commits
32 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c58c20cc54 | ||
|
|
89ddb7f4b7 | ||
|
|
cb92a51191 | ||
|
|
213b172c32 | ||
|
|
cfdd470626 | ||
|
|
a5a3c62edd | ||
|
|
c9bcfa1c17 | ||
|
|
02ad85a740 | ||
|
|
446273de80 | ||
|
|
b513877552 | ||
|
|
013eedd892 | ||
|
|
d2cbb27756 | ||
|
|
1a477ea1c8 | ||
|
|
83df639aff | ||
|
|
f298cc6f13 | ||
|
|
300a70ce18 | ||
|
|
0f6bf1cc59 | ||
|
|
6bc1b6b28a | ||
|
|
1c381f64f9 | ||
|
|
5e3703e849 | ||
|
|
acbd41ce61 | ||
|
|
056d00b73b | ||
|
|
72fc21cdc6 | ||
|
|
3e983cdf1c | ||
|
|
d81af9a194 | ||
|
|
7f6b44cd0b | ||
|
|
88bad1e308 | ||
|
|
aa0a4d1aac | ||
|
|
b0a61cce42 | ||
|
|
459e579bcb | ||
|
|
3c069d574e | ||
|
|
d2accf7622 |
@@ -13,8 +13,8 @@ android {
|
||||
applicationId "com.vitorpamplona.amethyst"
|
||||
minSdk 26
|
||||
targetSdk 33
|
||||
versionCode 182
|
||||
versionName "0.52.2"
|
||||
versionCode 184
|
||||
versionName "0.53.0"
|
||||
|
||||
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
||||
vectorDrawables {
|
||||
|
||||
@@ -1154,6 +1154,7 @@ class Account(
|
||||
val userToBlock = LocalCache.getOrCreateUser(it.pubkeyHex)
|
||||
if (userToBlock != userProfile() && userToBlock.pubkeyHex !in followingKeySet()) {
|
||||
transientHiddenUsers = transientHiddenUsers + it.pubkeyHex
|
||||
live.invalidateData()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -75,14 +75,17 @@ class Nip05Verifier() {
|
||||
fetchNip05Json(
|
||||
nip05,
|
||||
onSuccess = {
|
||||
// NIP05 usernames are case insensitive, but JSON properties are not
|
||||
// converts the json to lowercase and then tries to access the username via a
|
||||
// lowercase version of the username.
|
||||
val nip05url = try {
|
||||
mapper.readTree(it)
|
||||
mapper.readTree(it.lowercase())
|
||||
} catch (t: Throwable) {
|
||||
onError("Error Parsing JSON from Lightning Address. Check the user's lightning setup")
|
||||
null
|
||||
}
|
||||
|
||||
val user = nip05.split("@")[0]
|
||||
val user = nip05.split("@")[0].lowercase()
|
||||
|
||||
val hexKey = nip05url?.get("names")?.get(user)?.asText()
|
||||
|
||||
|
||||
@@ -28,7 +28,7 @@ import com.vitorpamplona.amethyst.service.model.PrivateDmEvent
|
||||
import com.vitorpamplona.amethyst.service.nip19.Nip19
|
||||
import com.vitorpamplona.amethyst.service.notifications.PushNotificationUtils
|
||||
import com.vitorpamplona.amethyst.service.relays.Client
|
||||
import com.vitorpamplona.amethyst.ui.components.muted
|
||||
import com.vitorpamplona.amethyst.ui.components.DefaultMutedSetting
|
||||
import com.vitorpamplona.amethyst.ui.navigation.Route
|
||||
import com.vitorpamplona.amethyst.ui.note.Nip47
|
||||
import com.vitorpamplona.amethyst.ui.screen.AccountScreen
|
||||
@@ -88,7 +88,7 @@ class MainActivity : FragmentActivity() {
|
||||
override fun onResume() {
|
||||
super.onResume()
|
||||
// starts muted every time
|
||||
muted.value = true
|
||||
DefaultMutedSetting.value = true
|
||||
|
||||
// Only starts after login
|
||||
GlobalScope.launch(Dispatchers.IO) {
|
||||
|
||||
@@ -63,7 +63,6 @@ import androidx.compose.ui.window.DialogProperties
|
||||
import androidx.lifecycle.viewmodel.compose.viewModel
|
||||
import coil.compose.AsyncImage
|
||||
import com.vitorpamplona.amethyst.R
|
||||
import com.vitorpamplona.amethyst.model.Account
|
||||
import com.vitorpamplona.amethyst.model.Note
|
||||
import com.vitorpamplona.amethyst.model.User
|
||||
import com.vitorpamplona.amethyst.service.NostrSearchEventOrUserDataSource
|
||||
@@ -78,7 +77,10 @@ import kotlinx.coroutines.launch
|
||||
|
||||
@OptIn(ExperimentalComposeUiApi::class)
|
||||
@Composable
|
||||
fun NewPostView(onClose: () -> Unit, baseReplyTo: Note? = null, quote: Note? = null, account: Account, accountViewModel: AccountViewModel, nav: (String) -> Unit) {
|
||||
fun NewPostView(onClose: () -> Unit, baseReplyTo: Note? = null, quote: Note? = null, accountViewModel: AccountViewModel, nav: (String) -> Unit) {
|
||||
val accountState by accountViewModel.accountLiveData.observeAsState()
|
||||
val account = remember(accountState) { accountState?.account } ?: return
|
||||
|
||||
val postViewModel: NewPostViewModel = viewModel()
|
||||
|
||||
val context = LocalContext.current
|
||||
|
||||
@@ -17,18 +17,17 @@ import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.vitorpamplona.amethyst.R
|
||||
import com.vitorpamplona.amethyst.model.Account
|
||||
import com.vitorpamplona.amethyst.ui.actions.NewPostView
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
||||
|
||||
@Composable
|
||||
fun NewNoteButton(account: Account, accountViewModel: AccountViewModel, nav: (String) -> Unit) {
|
||||
fun NewNoteButton(accountViewModel: AccountViewModel, nav: (String) -> Unit) {
|
||||
var wantsToPost by remember {
|
||||
mutableStateOf(false)
|
||||
}
|
||||
|
||||
if (wantsToPost) {
|
||||
NewPostView({ wantsToPost = false }, account = account, accountViewModel = accountViewModel, nav = nav)
|
||||
NewPostView({ wantsToPost = false }, accountViewModel = accountViewModel, nav = nav)
|
||||
}
|
||||
|
||||
OutlinedButton(
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package com.vitorpamplona.amethyst.ui.components
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.Immutable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.ColorFilter
|
||||
@@ -14,6 +15,7 @@ import coil.compose.AsyncImage
|
||||
import coil.compose.AsyncImagePainter
|
||||
import java.util.Base64
|
||||
|
||||
@Immutable
|
||||
data class ResizeImage(val url: String?, val size: Dp) {
|
||||
fun proxyUrl(): String? {
|
||||
if (url == null) return null
|
||||
|
||||
@@ -64,22 +64,26 @@ import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import java.io.File
|
||||
|
||||
public var muted = mutableStateOf(true)
|
||||
public var DefaultMutedSetting = mutableStateOf(true)
|
||||
|
||||
@Composable
|
||||
fun VideoView(localFile: File?, description: String? = null, onDialog: ((Boolean) -> Unit)? = null) {
|
||||
if (localFile != null) {
|
||||
VideoView(localFile.toUri(), description, null, onDialog)
|
||||
val video = remember(localFile) { localFile.toUri() }
|
||||
|
||||
VideoView(video, description, null, onDialog)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun VideoView(videoUri: String, description: String? = null, onDialog: ((Boolean) -> Unit)? = null) {
|
||||
VideoView(Uri.parse(videoUri), description, null, onDialog)
|
||||
val video = remember(videoUri) { Uri.parse(videoUri) }
|
||||
|
||||
VideoView(video, description, null, onDialog)
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun VideoView(videoUri: String, description: String? = null, thumbUri: String, onDialog: ((Boolean) -> Unit)? = null) {
|
||||
fun LoadThumbAndThenVideoView(videoUri: String, description: String? = null, thumbUri: String, onDialog: ((Boolean) -> Unit)? = null) {
|
||||
var loadingFinished by remember { mutableStateOf<Pair<Boolean, Drawable?>>(Pair(false, null)) }
|
||||
|
||||
val context = LocalContext.current
|
||||
@@ -104,7 +108,9 @@ fun VideoView(videoUri: String, description: String? = null, thumbUri: String, o
|
||||
|
||||
if (loadingFinished.first) {
|
||||
if (loadingFinished.second != null) {
|
||||
VideoView(Uri.parse(videoUri), description, loadingFinished.second, onDialog)
|
||||
val video = remember(videoUri) { Uri.parse(videoUri) }
|
||||
|
||||
VideoView(video, description, loadingFinished.second, onDialog)
|
||||
} else {
|
||||
VideoView(videoUri, description, onDialog)
|
||||
}
|
||||
@@ -116,7 +122,7 @@ fun VideoView(videoUri: Uri, description: String? = null, thumb: Drawable? = nul
|
||||
val context = LocalContext.current
|
||||
val lifecycleOwner = rememberUpdatedState(LocalLifecycleOwner.current)
|
||||
|
||||
println("loading audio with artwork $thumb")
|
||||
val mutedInstance = remember { mutableStateOf(DefaultMutedSetting.value) }
|
||||
|
||||
val exoPlayer = remember(videoUri) {
|
||||
val mediaBuilder = MediaItem.Builder().setUri(videoUri)
|
||||
@@ -146,10 +152,6 @@ fun VideoView(videoUri: Uri, description: String? = null, thumb: Drawable? = nul
|
||||
}
|
||||
}
|
||||
|
||||
LaunchedEffect(key1 = muted.value) {
|
||||
exoPlayer.volume = if (muted.value) 0f else 1f
|
||||
}
|
||||
|
||||
DisposableEffect(
|
||||
BoxWithConstraints() {
|
||||
AndroidView(
|
||||
@@ -185,8 +187,11 @@ fun VideoView(videoUri: Uri, description: String? = null, thumb: Drawable? = nul
|
||||
}
|
||||
)
|
||||
|
||||
MuteButton(muted, Modifier) {
|
||||
muted.value = !muted.value
|
||||
MuteButton(mutedInstance) {
|
||||
mutedInstance.value = !mutedInstance.value
|
||||
DefaultMutedSetting.value = mutedInstance.value
|
||||
|
||||
exoPlayer.volume = if (mutedInstance.value) 0f else 1f
|
||||
}
|
||||
}
|
||||
) {
|
||||
@@ -241,12 +246,14 @@ fun LayoutCoordinates.isCompletelyVisible(view: View): Boolean {
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun MuteButton(muted: MutableState<Boolean>, modifier: Modifier, toggle: () -> Unit) {
|
||||
private fun MuteButton(muted: MutableState<Boolean>, toggle: () -> Unit) {
|
||||
Box(
|
||||
modifier
|
||||
.width(70.dp)
|
||||
.height(70.dp)
|
||||
.padding(10.dp)
|
||||
remember {
|
||||
Modifier
|
||||
.width(70.dp)
|
||||
.height(70.dp)
|
||||
.padding(10.dp)
|
||||
}
|
||||
) {
|
||||
Box(
|
||||
Modifier
|
||||
@@ -256,23 +263,18 @@ private fun MuteButton(muted: MutableState<Boolean>, modifier: Modifier, toggle:
|
||||
.background(MaterialTheme.colors.background)
|
||||
)
|
||||
|
||||
if (muted.value) {
|
||||
IconButton(
|
||||
onClick = toggle,
|
||||
modifier = Modifier.size(50.dp)
|
||||
) {
|
||||
IconButton(
|
||||
onClick = toggle,
|
||||
modifier = Modifier.size(50.dp)
|
||||
) {
|
||||
if (muted.value) {
|
||||
Icon(
|
||||
imageVector = Icons.Default.VolumeOff,
|
||||
"Hash Verified",
|
||||
tint = MaterialTheme.colors.onBackground,
|
||||
modifier = Modifier.size(30.dp)
|
||||
)
|
||||
}
|
||||
} else {
|
||||
IconButton(
|
||||
onClick = toggle,
|
||||
modifier = Modifier.size(50.dp)
|
||||
) {
|
||||
} else {
|
||||
Icon(
|
||||
imageVector = Icons.Default.VolumeUp,
|
||||
"Hash Verified",
|
||||
|
||||
@@ -1,9 +1,16 @@
|
||||
package com.vitorpamplona.amethyst.ui.dal
|
||||
|
||||
import android.util.Log
|
||||
import com.vitorpamplona.amethyst.model.Account
|
||||
import com.vitorpamplona.amethyst.model.LocalCache
|
||||
import com.vitorpamplona.amethyst.model.Note
|
||||
import com.vitorpamplona.amethyst.service.model.ChannelMessageEvent
|
||||
import com.vitorpamplona.amethyst.service.model.PrivateDmEvent
|
||||
import com.vitorpamplona.amethyst.ui.actions.updated
|
||||
import kotlin.time.ExperimentalTime
|
||||
import kotlin.time.measureTimedValue
|
||||
|
||||
object ChatroomListKnownFeedFilter : FeedFilter<Note>() {
|
||||
object ChatroomListKnownFeedFilter : AdditiveFeedFilter<Note>() {
|
||||
lateinit var account: Account
|
||||
|
||||
// returns the last Note of each user.
|
||||
@@ -35,4 +42,119 @@ object ChatroomListKnownFeedFilter : FeedFilter<Note>() {
|
||||
.sortedWith(compareBy({ it.createdAt() }, { it.idHex }))
|
||||
.reversed()
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalTime::class)
|
||||
override fun updateListWith(oldList: List<Note>, newItems: Set<Note>): List<Note> {
|
||||
val (feed, elapsed) = measureTimedValue {
|
||||
val me = account.userProfile()
|
||||
|
||||
// Gets the latest message by channel from the new items.
|
||||
val newRelevantPublicMessages = filterRelevantPublicMessages(newItems, account)
|
||||
|
||||
// Gets the latest message by room from the new items.
|
||||
val newRelevantPrivateMessages = filterRelevantPrivateMessages(newItems, account)
|
||||
|
||||
if (newRelevantPrivateMessages.isEmpty() && newRelevantPublicMessages.isEmpty()) {
|
||||
return oldList
|
||||
}
|
||||
|
||||
var myNewList = oldList
|
||||
|
||||
newRelevantPublicMessages.forEach { newNotePair ->
|
||||
oldList.forEach { oldNote ->
|
||||
if (
|
||||
(newNotePair.key == oldNote.channelHex()) && (newNotePair.value.createdAt() ?: 0) > (oldNote.createdAt() ?: 0)
|
||||
) {
|
||||
myNewList = myNewList.updated(oldNote, newNotePair.value)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
newRelevantPrivateMessages.forEach { newNotePair ->
|
||||
oldList.forEach { oldNote ->
|
||||
val oldAuthor = oldNote.author?.pubkeyHex
|
||||
val oldRecipient = (oldNote.event as? PrivateDmEvent)?.verifiedRecipientPubKey()
|
||||
|
||||
val oldRoom = if (oldAuthor == me.pubkeyHex) oldRecipient else oldAuthor
|
||||
|
||||
if (
|
||||
(newNotePair.key == oldRoom) && (newNotePair.value.createdAt() ?: 0) > (oldNote.createdAt() ?: 0)
|
||||
) {
|
||||
myNewList = myNewList.updated(oldNote, newNotePair.value)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
myNewList
|
||||
}
|
||||
|
||||
Log.d("Time", "${this.javaClass.simpleName} Modified Additive Feed in $elapsed with ${feed.size} objects")
|
||||
return feed
|
||||
}
|
||||
|
||||
override fun applyFilter(newItems: Set<Note>): Set<Note> {
|
||||
// Gets the latest message by channel from the new items.
|
||||
val newRelevantPublicMessages = filterRelevantPublicMessages(newItems, account)
|
||||
|
||||
// Gets the latest message by room from the new items.
|
||||
val newRelevantPrivateMessages = filterRelevantPrivateMessages(newItems, account)
|
||||
|
||||
return if (newRelevantPrivateMessages.isEmpty() && newRelevantPublicMessages.isEmpty()) {
|
||||
emptySet()
|
||||
} else {
|
||||
(newRelevantPrivateMessages.values + newRelevantPublicMessages.values).toSet()
|
||||
}
|
||||
}
|
||||
|
||||
private fun filterRelevantPublicMessages(newItems: Set<Note>, account: Account): MutableMap<String, Note> {
|
||||
val followingChannels = account.followingChannels
|
||||
val newRelevantPublicMessages = mutableMapOf<String, Note>()
|
||||
newItems.filter { it.event is ChannelMessageEvent }.forEach { newNote ->
|
||||
newNote.channelHex()?.let { channelHex ->
|
||||
if (channelHex in followingChannels && account.isAcceptable(newNote)) {
|
||||
val lastNote = newRelevantPublicMessages.get(channelHex)
|
||||
if (lastNote != null) {
|
||||
if ((newNote.createdAt() ?: 0) > (lastNote.createdAt() ?: 0)) {
|
||||
newRelevantPublicMessages.put(channelHex, newNote)
|
||||
}
|
||||
} else {
|
||||
newRelevantPublicMessages.put(channelHex, newNote)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return newRelevantPublicMessages
|
||||
}
|
||||
|
||||
private fun filterRelevantPrivateMessages(newItems: Set<Note>, account: Account): MutableMap<String, Note> {
|
||||
val me = account.userProfile()
|
||||
val followingKeySet = account.followingKeySet()
|
||||
|
||||
val newRelevantPrivateMessages = mutableMapOf<String, Note>()
|
||||
newItems.filter { it.event is PrivateDmEvent }.forEach { newNote ->
|
||||
val newAuthor = newNote.author?.pubkeyHex
|
||||
val newRecipient = (newNote.event as? PrivateDmEvent)?.verifiedRecipientPubKey()
|
||||
|
||||
val roomUserHex = if (newAuthor == me.pubkeyHex) newRecipient else newAuthor
|
||||
val roomUser = roomUserHex?.let { LocalCache.users[it] }
|
||||
|
||||
if (roomUserHex != null && (newAuthor == me.pubkeyHex || roomUserHex in followingKeySet || me.hasSentMessagesTo(roomUser)) && !account.isHidden(roomUserHex)) {
|
||||
val lastNote = newRelevantPrivateMessages.get(roomUserHex)
|
||||
if (lastNote != null) {
|
||||
if ((newNote.createdAt() ?: 0) > (lastNote.createdAt() ?: 0)) {
|
||||
newRelevantPrivateMessages.put(roomUserHex, newNote)
|
||||
}
|
||||
} else {
|
||||
newRelevantPrivateMessages.put(roomUserHex, newNote)
|
||||
}
|
||||
}
|
||||
}
|
||||
return newRelevantPrivateMessages
|
||||
}
|
||||
|
||||
override fun sort(collection: Set<Note>): List<Note> {
|
||||
return collection
|
||||
.sortedWith(compareBy({ it.createdAt() }, { it.idHex }))
|
||||
.reversed()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,7 +11,7 @@ abstract class FeedFilter<T> {
|
||||
feed()
|
||||
}
|
||||
|
||||
Log.d("Time", "${this.javaClass.simpleName} Feed in $elapsed with ${feed.size} objects")
|
||||
Log.d("Time", "${this.javaClass.simpleName} Full Feed in $elapsed with ${feed.size} objects")
|
||||
return feed.take(1000)
|
||||
}
|
||||
|
||||
@@ -23,7 +23,7 @@ abstract class AdditiveFeedFilter<T> : FeedFilter<T>() {
|
||||
abstract fun sort(collection: Set<T>): List<T>
|
||||
|
||||
@OptIn(ExperimentalTime::class)
|
||||
fun updateListWith(oldList: List<T>, newItems: Set<T>): List<T> {
|
||||
open fun updateListWith(oldList: List<T>, newItems: Set<T>): List<T> {
|
||||
val (feed, elapsed) = measureTimedValue {
|
||||
val newItemsToBeAdded = applyFilter(newItems)
|
||||
if (newItemsToBeAdded.isNotEmpty()) {
|
||||
@@ -34,7 +34,7 @@ abstract class AdditiveFeedFilter<T> : FeedFilter<T>() {
|
||||
}
|
||||
}
|
||||
|
||||
Log.d("Time", "${this.javaClass.simpleName} Feed in $elapsed with ${feed.size} objects")
|
||||
Log.d("Time", "${this.javaClass.simpleName} Additive Feed in $elapsed with ${feed.size} objects")
|
||||
return feed
|
||||
}
|
||||
}
|
||||
|
||||
@@ -170,7 +170,7 @@ object NotificationLatestItem : LatestItem() {
|
||||
}
|
||||
}
|
||||
|
||||
object MessagesLatestItem {
|
||||
object MessagesLatestItem : LatestItem() {
|
||||
fun hasNewItems(
|
||||
account: Account,
|
||||
cache: NotificationCache,
|
||||
@@ -178,13 +178,11 @@ object MessagesLatestItem {
|
||||
): Boolean {
|
||||
ChatroomListKnownFeedFilter.account = account
|
||||
|
||||
val note = ChatroomListKnownFeedFilter.loadTop().firstOrNull {
|
||||
it.createdAt() != null && it.channel() == null && it.author != account.userProfile()
|
||||
} ?: return false
|
||||
val newestItem = updateNewestItem(newNotes, account, ChatroomListKnownFeedFilter)
|
||||
|
||||
val lastTime = cache.load("Room/${note.author?.pubkeyHex}")
|
||||
val lastTime = cache.load("Room/${newestItem?.author?.pubkeyHex}")
|
||||
|
||||
return (note.createdAt() ?: 0) > lastTime
|
||||
return (newestItem?.createdAt() ?: 0) > lastTime
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -36,6 +36,7 @@ import com.vitorpamplona.amethyst.NotificationCache
|
||||
import com.vitorpamplona.amethyst.R
|
||||
import com.vitorpamplona.amethyst.ui.screen.BadgeCard
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
||||
import com.vitorpamplona.amethyst.ui.theme.newItemBackgroundColor
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
@@ -64,7 +65,7 @@ fun BadgeCompose(likeSetCard: BadgeCard, isInnerNote: Boolean = false, routeForL
|
||||
}
|
||||
|
||||
val backgroundColor = if (isNew) {
|
||||
MaterialTheme.colors.primary.copy(0.12f).compositeOver(MaterialTheme.colors.background)
|
||||
MaterialTheme.colors.newItemBackgroundColor.compositeOver(MaterialTheme.colors.background)
|
||||
} else {
|
||||
MaterialTheme.colors.background
|
||||
}
|
||||
|
||||
@@ -30,6 +30,7 @@ import com.vitorpamplona.amethyst.NotificationCache
|
||||
import com.vitorpamplona.amethyst.R
|
||||
import com.vitorpamplona.amethyst.ui.screen.BoostSetCard
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
||||
import com.vitorpamplona.amethyst.ui.theme.newItemBackgroundColor
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
@@ -62,7 +63,7 @@ fun BoostSetCompose(boostSetCard: BoostSetCard, isInnerNote: Boolean = false, ro
|
||||
}
|
||||
|
||||
val backgroundColor = if (isNew) {
|
||||
MaterialTheme.colors.primary.copy(0.12f).compositeOver(MaterialTheme.colors.background)
|
||||
MaterialTheme.colors.newItemBackgroundColor.compositeOver(MaterialTheme.colors.background)
|
||||
} else {
|
||||
MaterialTheme.colors.background
|
||||
}
|
||||
|
||||
@@ -30,6 +30,7 @@ import com.vitorpamplona.amethyst.NotificationCache
|
||||
import com.vitorpamplona.amethyst.R
|
||||
import com.vitorpamplona.amethyst.ui.screen.LikeSetCard
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
||||
import com.vitorpamplona.amethyst.ui.theme.newItemBackgroundColor
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
@@ -61,7 +62,7 @@ fun LikeSetCompose(likeSetCard: LikeSetCard, isInnerNote: Boolean = false, route
|
||||
}
|
||||
|
||||
val backgroundColor = if (isNew) {
|
||||
MaterialTheme.colors.primary.copy(0.12f).compositeOver(MaterialTheme.colors.background)
|
||||
MaterialTheme.colors.newItemBackgroundColor.compositeOver(MaterialTheme.colors.background)
|
||||
} else {
|
||||
MaterialTheme.colors.background
|
||||
}
|
||||
|
||||
@@ -29,6 +29,7 @@ import com.vitorpamplona.amethyst.NotificationCache
|
||||
import com.vitorpamplona.amethyst.R
|
||||
import com.vitorpamplona.amethyst.ui.screen.MessageSetCard
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
||||
import com.vitorpamplona.amethyst.ui.theme.newItemBackgroundColor
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
@@ -66,7 +67,7 @@ fun MessageSetCompose(messageSetCard: MessageSetCard, routeForLastRead: String,
|
||||
}
|
||||
|
||||
val backgroundColor = if (isNew) {
|
||||
MaterialTheme.colors.primary.copy(0.12f).compositeOver(MaterialTheme.colors.background)
|
||||
MaterialTheme.colors.newItemBackgroundColor.compositeOver(MaterialTheme.colors.background)
|
||||
} else {
|
||||
MaterialTheme.colors.background
|
||||
}
|
||||
|
||||
@@ -51,6 +51,7 @@ import com.vitorpamplona.amethyst.ui.screen.MultiSetCard
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.showAmountAxis
|
||||
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.coroutines.Dispatchers
|
||||
@@ -88,7 +89,7 @@ fun MultiSetCompose(multiSetCard: MultiSetCard, routeForLastRead: String, accoun
|
||||
}
|
||||
}
|
||||
|
||||
val primaryColor = MaterialTheme.colors.primary.copy(0.12f)
|
||||
val primaryColor = MaterialTheme.colors.newItemBackgroundColor
|
||||
val defaultBackgroundColor = MaterialTheme.colors.background
|
||||
|
||||
val backgroundColor = if (isNew) {
|
||||
|
||||
@@ -83,7 +83,6 @@ import com.vitorpamplona.amethyst.model.Account
|
||||
import com.vitorpamplona.amethyst.model.Channel
|
||||
import com.vitorpamplona.amethyst.model.LocalCache
|
||||
import com.vitorpamplona.amethyst.model.Note
|
||||
import com.vitorpamplona.amethyst.model.NoteState
|
||||
import com.vitorpamplona.amethyst.model.User
|
||||
import com.vitorpamplona.amethyst.service.model.AudioTrackEvent
|
||||
import com.vitorpamplona.amethyst.service.model.BadgeAwardEvent
|
||||
@@ -108,6 +107,7 @@ import com.vitorpamplona.amethyst.service.model.RepostEvent
|
||||
import com.vitorpamplona.amethyst.service.model.TextNoteEvent
|
||||
import com.vitorpamplona.amethyst.ui.components.ClickableUrl
|
||||
import com.vitorpamplona.amethyst.ui.components.CreateClickableTextWithEmoji
|
||||
import com.vitorpamplona.amethyst.ui.components.LoadThumbAndThenVideoView
|
||||
import com.vitorpamplona.amethyst.ui.components.ObserveDisplayNip05Status
|
||||
import com.vitorpamplona.amethyst.ui.components.ResizeImage
|
||||
import com.vitorpamplona.amethyst.ui.components.RobohashAsyncImage
|
||||
@@ -127,6 +127,7 @@ import com.vitorpamplona.amethyst.ui.screen.loggedIn.ChannelHeader
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.ReportNoteDialog
|
||||
import com.vitorpamplona.amethyst.ui.theme.BitcoinOrange
|
||||
import com.vitorpamplona.amethyst.ui.theme.Following
|
||||
import com.vitorpamplona.amethyst.ui.theme.newItemBackgroundColor
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
@@ -153,7 +154,6 @@ fun NoteCompose(
|
||||
) {
|
||||
val accountState by accountViewModel.accountLiveData.observeAsState()
|
||||
val account = remember(accountState) { accountState?.account } ?: return
|
||||
val loggedIn = remember(accountState) { accountState?.account?.userProfile() } ?: return
|
||||
|
||||
val noteState by baseNote.live().metadata.observeAsState()
|
||||
val note = remember(noteState) { noteState?.note } ?: return
|
||||
@@ -161,13 +161,11 @@ fun NoteCompose(
|
||||
val noteReportsState by baseNote.live().reports.observeAsState()
|
||||
val noteForReports = remember(noteReportsState) { noteReportsState?.note } ?: return
|
||||
|
||||
val noteEvent = remember(noteState) { note.event }
|
||||
val baseChannel = remember(noteState) { note.channel() }
|
||||
val isSensitive = remember(noteState) { note.event?.isSensitive() ?: false }
|
||||
|
||||
var popupExpanded by remember { mutableStateOf(false) }
|
||||
if (note.event == null) {
|
||||
var popupExpanded by remember { mutableStateOf(false) }
|
||||
|
||||
if (noteEvent == null) {
|
||||
BlankNote(
|
||||
remember {
|
||||
modifier.combinedClickable(
|
||||
@@ -178,13 +176,11 @@ fun NoteCompose(
|
||||
isBoostedNote
|
||||
)
|
||||
|
||||
note.let {
|
||||
NoteQuickActionMenu(it, popupExpanded, { popupExpanded = false }, accountViewModel)
|
||||
}
|
||||
NoteQuickActionMenu(note, popupExpanded, { popupExpanded = false }, accountViewModel)
|
||||
} else if (account.isHidden(noteForReports.author!!) || (isSensitive && account.showSensitiveContent == false)) {
|
||||
// Does nothing
|
||||
} else {
|
||||
var showHiddenNote by remember { mutableStateOf(false) }
|
||||
var showReportedNote by remember { mutableStateOf(false) }
|
||||
var isAcceptableAndCanPreview by remember { mutableStateOf(Pair(true, true)) }
|
||||
|
||||
LaunchedEffect(key1 = noteReportsState, key2 = accountState) {
|
||||
@@ -203,192 +199,235 @@ fun NoteCompose(
|
||||
}
|
||||
}
|
||||
|
||||
if (!isAcceptableAndCanPreview.first && !showHiddenNote) {
|
||||
if (!isAcceptableAndCanPreview.first && !showReportedNote) {
|
||||
HiddenNote(
|
||||
account.getRelevantReports(noteForReports),
|
||||
account.userProfile(),
|
||||
modifier,
|
||||
isBoostedNote,
|
||||
nav,
|
||||
onClick = { showHiddenNote = true }
|
||||
onClick = { showReportedNote = true }
|
||||
)
|
||||
} else if ((noteEvent is ChannelCreateEvent || noteEvent is ChannelMetadataEvent) && baseChannel != null) {
|
||||
ChannelHeader(baseChannel = baseChannel, account = account, nav = nav)
|
||||
} else if (noteEvent is BadgeDefinitionEvent) {
|
||||
BadgeDisplay(baseNote = note)
|
||||
} else if (noteEvent is FileHeaderEvent) {
|
||||
FileHeaderDisplay(note)
|
||||
} else if (noteEvent is FileStorageHeaderEvent) {
|
||||
FileStorageHeaderDisplay(note)
|
||||
} else {
|
||||
var isNew by remember { mutableStateOf<Boolean>(false) }
|
||||
NormalNote(
|
||||
baseNote,
|
||||
routeForLastRead,
|
||||
modifier,
|
||||
isBoostedNote,
|
||||
isQuotedNote,
|
||||
unPackReply,
|
||||
makeItShort,
|
||||
addMarginTop,
|
||||
isAcceptableAndCanPreview.second,
|
||||
parentBackgroundColor,
|
||||
accountViewModel,
|
||||
nav
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
val scope = rememberCoroutineScope()
|
||||
@OptIn(ExperimentalFoundationApi::class)
|
||||
@Composable
|
||||
fun NormalNote(
|
||||
baseNote: Note,
|
||||
routeForLastRead: String? = null,
|
||||
modifier: Modifier = Modifier,
|
||||
isBoostedNote: Boolean = false,
|
||||
isQuotedNote: Boolean = false,
|
||||
unPackReply: Boolean = true,
|
||||
makeItShort: Boolean = false,
|
||||
addMarginTop: Boolean = true,
|
||||
canPreview: Boolean = true,
|
||||
parentBackgroundColor: Color? = null,
|
||||
accountViewModel: AccountViewModel,
|
||||
nav: (String) -> Unit
|
||||
) {
|
||||
val accountState by accountViewModel.accountLiveData.observeAsState()
|
||||
val account = remember(accountState) { accountState?.account } ?: return
|
||||
val loggedIn = remember(accountState) { accountState?.account?.userProfile() } ?: return
|
||||
|
||||
LaunchedEffect(key1 = routeForLastRead) {
|
||||
scope.launch(Dispatchers.IO) {
|
||||
routeForLastRead?.let {
|
||||
val lastTime = NotificationCache.load(it)
|
||||
val noteEvent = remember { baseNote.event }
|
||||
val channelHex = remember { baseNote.channelHex() }
|
||||
|
||||
val createdAt = note.createdAt()
|
||||
if (createdAt != null) {
|
||||
NotificationCache.markAsRead(it, createdAt)
|
||||
var popupExpanded by remember { mutableStateOf(false) }
|
||||
|
||||
val newIsNew = createdAt > lastTime
|
||||
if (newIsNew != isNew) {
|
||||
isNew = newIsNew
|
||||
}
|
||||
if ((noteEvent is ChannelCreateEvent || noteEvent is ChannelMetadataEvent) && channelHex != null) {
|
||||
ChannelHeader(channelHex = channelHex, account = account, nav = nav)
|
||||
} else if (noteEvent is BadgeDefinitionEvent) {
|
||||
BadgeDisplay(baseNote = baseNote)
|
||||
} else if (noteEvent is FileHeaderEvent) {
|
||||
FileHeaderDisplay(baseNote)
|
||||
} else if (noteEvent is FileStorageHeaderEvent) {
|
||||
FileStorageHeaderDisplay(baseNote)
|
||||
} else {
|
||||
var isNew by remember { mutableStateOf<Boolean>(false) }
|
||||
|
||||
val scope = rememberCoroutineScope()
|
||||
|
||||
LaunchedEffect(key1 = routeForLastRead) {
|
||||
scope.launch(Dispatchers.IO) {
|
||||
routeForLastRead?.let {
|
||||
val lastTime = NotificationCache.load(it)
|
||||
|
||||
val createdAt = baseNote.createdAt()
|
||||
if (createdAt != null) {
|
||||
NotificationCache.markAsRead(it, createdAt)
|
||||
|
||||
val newIsNew = createdAt > lastTime
|
||||
if (newIsNew != isNew) {
|
||||
isNew = newIsNew
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
val primaryColor = MaterialTheme.colors.primary.copy(0.12f)
|
||||
val defaultBackgroundColor = MaterialTheme.colors.background
|
||||
val primaryColor = MaterialTheme.colors.newItemBackgroundColor
|
||||
val defaultBackgroundColor = MaterialTheme.colors.background
|
||||
|
||||
val backgroundColor = remember(isNew, parentBackgroundColor) {
|
||||
if (isNew) {
|
||||
if (parentBackgroundColor != null) {
|
||||
primaryColor.compositeOver(parentBackgroundColor)
|
||||
} else {
|
||||
primaryColor.compositeOver(defaultBackgroundColor)
|
||||
}
|
||||
val backgroundColor = remember(isNew, parentBackgroundColor) {
|
||||
if (isNew) {
|
||||
if (parentBackgroundColor != null) {
|
||||
primaryColor.compositeOver(parentBackgroundColor)
|
||||
} else {
|
||||
parentBackgroundColor ?: defaultBackgroundColor
|
||||
primaryColor.compositeOver(defaultBackgroundColor)
|
||||
}
|
||||
} else {
|
||||
parentBackgroundColor ?: defaultBackgroundColor
|
||||
}
|
||||
}
|
||||
|
||||
val columnModifier = remember(backgroundColor) {
|
||||
modifier
|
||||
.combinedClickable(
|
||||
onClick = {
|
||||
scope.launch {
|
||||
routeFor(note, loggedIn)?.let {
|
||||
nav(it)
|
||||
}
|
||||
val columnModifier = remember(backgroundColor) {
|
||||
modifier
|
||||
.combinedClickable(
|
||||
onClick = {
|
||||
scope.launch {
|
||||
routeFor(baseNote, loggedIn)?.let {
|
||||
nav(it)
|
||||
}
|
||||
},
|
||||
onLongClick = { popupExpanded = true }
|
||||
)
|
||||
.background(backgroundColor)
|
||||
}
|
||||
}
|
||||
},
|
||||
onLongClick = { popupExpanded = true }
|
||||
)
|
||||
.background(backgroundColor)
|
||||
}
|
||||
|
||||
Column(modifier = columnModifier) {
|
||||
Row(
|
||||
Column(modifier = columnModifier) {
|
||||
Row(
|
||||
modifier = remember {
|
||||
Modifier
|
||||
.padding(
|
||||
start = if (!isBoostedNote) 12.dp else 0.dp,
|
||||
end = if (!isBoostedNote) 12.dp else 0.dp,
|
||||
top = if (addMarginTop && !isBoostedNote) 10.dp else 0.dp
|
||||
)
|
||||
}
|
||||
) {
|
||||
if (!isBoostedNote && !isQuotedNote) {
|
||||
DrawAuthorImages(baseNote, loggedIn, nav)
|
||||
}
|
||||
|
||||
Column(
|
||||
modifier = remember {
|
||||
Modifier
|
||||
.padding(
|
||||
start = if (!isBoostedNote) 12.dp else 0.dp,
|
||||
end = if (!isBoostedNote) 12.dp else 0.dp,
|
||||
top = if (addMarginTop && !isBoostedNote) 10.dp else 0.dp
|
||||
)
|
||||
.padding(start = if (!isBoostedNote && !isQuotedNote) 10.dp else 0.dp)
|
||||
}
|
||||
) {
|
||||
if (!isBoostedNote && !isQuotedNote) {
|
||||
DrawAuthorImages(baseNote, loggedIn, nav)
|
||||
FirstUserInfoRow(
|
||||
baseNote = baseNote,
|
||||
showAuthorPicture = isQuotedNote,
|
||||
account = account,
|
||||
accountViewModel = accountViewModel,
|
||||
nav = nav
|
||||
)
|
||||
|
||||
if (noteEvent !is RepostEvent && !makeItShort && !isQuotedNote) {
|
||||
SecondUserInfoRow(
|
||||
baseNote,
|
||||
account,
|
||||
nav
|
||||
)
|
||||
}
|
||||
|
||||
Column(
|
||||
modifier = remember {
|
||||
Modifier
|
||||
.padding(start = if (!isBoostedNote && !isQuotedNote) 10.dp else 0.dp)
|
||||
}
|
||||
) {
|
||||
FirstUserInfoRow(
|
||||
baseNote = baseNote,
|
||||
showAuthorPicture = isQuotedNote,
|
||||
account = account,
|
||||
accountViewModel = accountViewModel,
|
||||
nav = nav
|
||||
Spacer(modifier = Modifier.height(2.dp))
|
||||
|
||||
if (!makeItShort) {
|
||||
ReplyRow(
|
||||
baseNote,
|
||||
unPackReply,
|
||||
backgroundColor,
|
||||
account,
|
||||
accountViewModel,
|
||||
nav
|
||||
)
|
||||
}
|
||||
|
||||
if (noteEvent !is RepostEvent && !makeItShort && !isQuotedNote) {
|
||||
SecondUserInfoRow(
|
||||
note,
|
||||
account,
|
||||
nav
|
||||
)
|
||||
when (noteEvent) {
|
||||
is ReactionEvent -> {
|
||||
RenderReaction(baseNote, backgroundColor, accountViewModel, nav)
|
||||
}
|
||||
|
||||
Spacer(modifier = Modifier.height(2.dp))
|
||||
is RepostEvent -> {
|
||||
RenderRepost(baseNote, backgroundColor, accountViewModel, nav)
|
||||
}
|
||||
|
||||
if (!makeItShort) {
|
||||
ReplyRow(
|
||||
note,
|
||||
unPackReply,
|
||||
is ReportEvent -> {
|
||||
RenderReport(baseNote)
|
||||
}
|
||||
|
||||
is LongTextNoteEvent -> {
|
||||
RenderLongFormContent(baseNote, loggedIn, accountViewModel, nav)
|
||||
}
|
||||
|
||||
is BadgeAwardEvent -> {
|
||||
RenderBadgeAward(baseNote, backgroundColor, accountViewModel, nav)
|
||||
}
|
||||
|
||||
is PeopleListEvent -> {
|
||||
RenderPeopleList(baseNote, backgroundColor, accountViewModel, nav)
|
||||
}
|
||||
|
||||
is AudioTrackEvent -> {
|
||||
RenderAudioTrack(baseNote, loggedIn, accountViewModel, nav)
|
||||
}
|
||||
|
||||
is PinListEvent -> {
|
||||
RenderPinListEvent(baseNote, backgroundColor, accountViewModel, nav)
|
||||
}
|
||||
|
||||
is PrivateDmEvent -> {
|
||||
RenderPrivateMessage(baseNote, makeItShort, canPreview, backgroundColor, accountViewModel, nav)
|
||||
}
|
||||
|
||||
is HighlightEvent -> {
|
||||
RenderHighlight(baseNote, makeItShort, canPreview, backgroundColor, accountViewModel, nav)
|
||||
}
|
||||
|
||||
is PollNoteEvent -> {
|
||||
RenderPoll(
|
||||
baseNote,
|
||||
makeItShort,
|
||||
canPreview,
|
||||
backgroundColor,
|
||||
account,
|
||||
accountViewModel,
|
||||
nav
|
||||
)
|
||||
}
|
||||
|
||||
when (noteEvent) {
|
||||
is ReactionEvent -> {
|
||||
RenderReaction(note, backgroundColor, accountViewModel, nav)
|
||||
}
|
||||
|
||||
is RepostEvent -> {
|
||||
RenderRepost(note, backgroundColor, accountViewModel, nav)
|
||||
}
|
||||
|
||||
is ReportEvent -> {
|
||||
RenderReport(note)
|
||||
}
|
||||
|
||||
is LongTextNoteEvent -> {
|
||||
RenderLongFormContent(note, loggedIn, accountViewModel, nav)
|
||||
}
|
||||
|
||||
is BadgeAwardEvent -> {
|
||||
RenderBadgeAward(note, backgroundColor, accountViewModel, nav)
|
||||
}
|
||||
|
||||
is PeopleListEvent -> {
|
||||
RenderPeopleList(noteState, backgroundColor, accountViewModel, nav)
|
||||
}
|
||||
|
||||
is AudioTrackEvent -> {
|
||||
RenderAudioTrack(note, loggedIn, accountViewModel, nav)
|
||||
}
|
||||
|
||||
is PinListEvent -> {
|
||||
RenderPinListEvent(noteState, backgroundColor, accountViewModel, nav)
|
||||
}
|
||||
|
||||
is PrivateDmEvent -> {
|
||||
RenderPrivateMessage(note, makeItShort, isAcceptableAndCanPreview.second, backgroundColor, accountViewModel, nav)
|
||||
}
|
||||
|
||||
is HighlightEvent -> {
|
||||
RenderHighlight(note, makeItShort, isAcceptableAndCanPreview.second, backgroundColor, accountViewModel, nav)
|
||||
}
|
||||
|
||||
is PollNoteEvent -> {
|
||||
RenderPoll(
|
||||
note,
|
||||
makeItShort,
|
||||
isAcceptableAndCanPreview.second,
|
||||
backgroundColor,
|
||||
accountViewModel,
|
||||
nav
|
||||
)
|
||||
}
|
||||
|
||||
else -> {
|
||||
RenderTextEvent(
|
||||
note,
|
||||
makeItShort,
|
||||
isAcceptableAndCanPreview.second,
|
||||
backgroundColor,
|
||||
accountViewModel,
|
||||
nav
|
||||
)
|
||||
}
|
||||
else -> {
|
||||
RenderTextEvent(
|
||||
baseNote,
|
||||
makeItShort,
|
||||
canPreview,
|
||||
backgroundColor,
|
||||
accountViewModel,
|
||||
nav
|
||||
)
|
||||
}
|
||||
|
||||
NoteQuickActionMenu(note, popupExpanded, { popupExpanded = false }, accountViewModel)
|
||||
}
|
||||
|
||||
NoteQuickActionMenu(baseNote, popupExpanded, { popupExpanded = false }, accountViewModel)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -581,14 +620,15 @@ private fun RenderPrivateMessage(
|
||||
val noteEvent = note.event as? PrivateDmEvent ?: return
|
||||
|
||||
val withMe = remember { noteEvent.with(accountViewModel.userProfile().pubkeyHex) }
|
||||
val tags = remember(note.event?.id()) { note.event?.tags() }
|
||||
val hashtags = remember(note.event?.id()) { note.event?.hashtags() ?: emptyList() }
|
||||
val modifier = remember(note.event?.id()) { Modifier.fillMaxWidth() }
|
||||
val isAuthorTheLoggedUser = remember(note.event?.id()) { accountViewModel.isLoggedUser(note.author) }
|
||||
|
||||
if (withMe) {
|
||||
val eventContent = remember { accountViewModel.decrypt(note) }
|
||||
|
||||
val hashtags = remember(note.event?.id()) { note.event?.hashtags() ?: emptyList() }
|
||||
val modifier = remember(note.event?.id()) { Modifier.fillMaxWidth() }
|
||||
val isAuthorTheLoggedUser = remember(note.event?.id()) { accountViewModel.isLoggedUser(note.author) }
|
||||
val tags = remember(note.event?.id()) { note.event?.tags() }
|
||||
|
||||
if (eventContent != null) {
|
||||
if (makeItShort && isAuthorTheLoggedUser) {
|
||||
Text(
|
||||
@@ -628,7 +668,7 @@ private fun RenderPrivateMessage(
|
||||
),
|
||||
canPreview = !makeItShort,
|
||||
Modifier.fillMaxWidth(),
|
||||
noteEvent.tags(),
|
||||
emptyList(),
|
||||
backgroundColor,
|
||||
accountViewModel,
|
||||
nav
|
||||
@@ -647,16 +687,14 @@ private fun RenderPrivateMessage(
|
||||
|
||||
@Composable
|
||||
fun RenderPeopleList(
|
||||
noteState: NoteState?,
|
||||
baseNote: Note,
|
||||
backgroundColor: Color,
|
||||
accountViewModel: AccountViewModel,
|
||||
nav: (String) -> Unit
|
||||
) {
|
||||
DisplayPeopleList(noteState, backgroundColor, accountViewModel, nav)
|
||||
DisplayPeopleList(baseNote, backgroundColor, accountViewModel, nav)
|
||||
|
||||
noteState?.note?.let {
|
||||
ReactionsRow(it, accountViewModel, nav)
|
||||
}
|
||||
ReactionsRow(baseNote, accountViewModel, nav)
|
||||
|
||||
Divider(
|
||||
modifier = Modifier.padding(top = 10.dp),
|
||||
@@ -666,13 +704,12 @@ fun RenderPeopleList(
|
||||
|
||||
@Composable
|
||||
fun DisplayPeopleList(
|
||||
noteState: NoteState?,
|
||||
baseNote: Note,
|
||||
backgroundColor: Color,
|
||||
accountViewModel: AccountViewModel,
|
||||
nav: (String) -> Unit
|
||||
) {
|
||||
val note = remember(noteState) { noteState?.note } ?: return
|
||||
val noteEvent = note.event as? PeopleListEvent ?: return
|
||||
val noteEvent = baseNote.event as? PeopleListEvent ?: return
|
||||
|
||||
var members by remember { mutableStateOf<List<User>>(listOf()) }
|
||||
|
||||
@@ -698,7 +735,7 @@ fun DisplayPeopleList(
|
||||
textAlign = TextAlign.Center
|
||||
)
|
||||
|
||||
LaunchedEffect(key1 = noteState) {
|
||||
LaunchedEffect(Unit) {
|
||||
withContext(Dispatchers.IO) {
|
||||
members = noteEvent.bookmarkedPeople().mapNotNull { hex ->
|
||||
LocalCache.checkGetOrCreateUser(hex)
|
||||
@@ -875,16 +912,16 @@ private fun RenderRepost(
|
||||
|
||||
@Composable
|
||||
private fun RenderPinListEvent(
|
||||
noteState: NoteState?,
|
||||
baseNote: Note,
|
||||
backgroundColor: Color,
|
||||
accountViewModel: AccountViewModel,
|
||||
nav: (String) -> Unit
|
||||
) {
|
||||
val noteEvent = noteState?.note?.event as? PinListEvent ?: return
|
||||
val noteEvent = baseNote.event as? PinListEvent ?: return
|
||||
|
||||
PinListHeader(noteState, backgroundColor, accountViewModel, nav)
|
||||
PinListHeader(baseNote, backgroundColor, accountViewModel, nav)
|
||||
|
||||
ReactionsRow(noteState?.note, accountViewModel, nav)
|
||||
ReactionsRow(baseNote, accountViewModel, nav)
|
||||
|
||||
Divider(
|
||||
modifier = Modifier.padding(top = 10.dp),
|
||||
@@ -894,15 +931,14 @@ private fun RenderPinListEvent(
|
||||
|
||||
@Composable
|
||||
fun PinListHeader(
|
||||
noteState: NoteState?,
|
||||
baseNote: Note,
|
||||
backgroundColor: Color,
|
||||
accountViewModel: AccountViewModel,
|
||||
nav: (String) -> Unit
|
||||
) {
|
||||
val note = remember(noteState) { noteState?.note } ?: return
|
||||
val noteEvent = note.event as? PinListEvent ?: return
|
||||
val noteEvent = baseNote.event as? PinListEvent ?: return
|
||||
|
||||
var pins by remember { mutableStateOf<List<String>>(noteEvent.pins()) }
|
||||
var pins by remember { mutableStateOf(noteEvent.pins()) }
|
||||
|
||||
var expanded by remember {
|
||||
mutableStateOf(false)
|
||||
@@ -1754,7 +1790,7 @@ fun AudioTrackHeader(noteEvent: AudioTrackEvent, note: Note, loggedIn: User, nav
|
||||
modifier = Modifier.padding(10.dp)
|
||||
) {
|
||||
cover?.let { cover ->
|
||||
VideoView(
|
||||
LoadThumbAndThenVideoView(
|
||||
videoUri = media,
|
||||
description = noteEvent.subject(),
|
||||
thumbUri = cover
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package com.vitorpamplona.amethyst.ui.note
|
||||
|
||||
import android.widget.Toast
|
||||
import androidx.compose.animation.core.animateFloatAsState
|
||||
import androidx.compose.foundation.ExperimentalFoundationApi
|
||||
import androidx.compose.foundation.combinedClickable
|
||||
import androidx.compose.foundation.interaction.MutableInteractionSource
|
||||
@@ -20,6 +21,7 @@ import androidx.compose.material.CircularProgressIndicator
|
||||
import androidx.compose.material.Icon
|
||||
import androidx.compose.material.IconButton
|
||||
import androidx.compose.material.MaterialTheme
|
||||
import androidx.compose.material.ProgressIndicatorDefaults
|
||||
import androidx.compose.material.Text
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.Bolt
|
||||
@@ -56,6 +58,7 @@ import coil.request.CachePolicy
|
||||
import coil.request.ImageRequest
|
||||
import com.vitorpamplona.amethyst.R
|
||||
import com.vitorpamplona.amethyst.model.Note
|
||||
import com.vitorpamplona.amethyst.model.User
|
||||
import com.vitorpamplona.amethyst.ui.actions.NewPostView
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
||||
import com.vitorpamplona.amethyst.ui.theme.BitcoinOrange
|
||||
@@ -68,9 +71,6 @@ import kotlin.math.roundToInt
|
||||
|
||||
@Composable
|
||||
fun ReactionsRow(baseNote: Note, accountViewModel: AccountViewModel, nav: (String) -> Unit) {
|
||||
val accountState by accountViewModel.accountLiveData.observeAsState()
|
||||
val account = remember(accountState) { accountState?.account } ?: return
|
||||
|
||||
val grayTint = MaterialTheme.colors.onSurface.copy(alpha = 0.32f)
|
||||
|
||||
var wantsToReplyTo by remember {
|
||||
@@ -82,17 +82,17 @@ fun ReactionsRow(baseNote: Note, accountViewModel: AccountViewModel, nav: (Strin
|
||||
}
|
||||
|
||||
if (wantsToReplyTo != null) {
|
||||
NewPostView({ wantsToReplyTo = null }, wantsToReplyTo, null, account, accountViewModel, nav)
|
||||
NewPostView({ wantsToReplyTo = null }, wantsToReplyTo, null, accountViewModel, nav)
|
||||
}
|
||||
|
||||
if (wantsToQuote != null) {
|
||||
NewPostView({ wantsToQuote = null }, null, wantsToQuote, account, accountViewModel, nav)
|
||||
NewPostView({ wantsToQuote = null }, null, wantsToQuote, accountViewModel, nav)
|
||||
}
|
||||
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
|
||||
Row(verticalAlignment = CenterVertically) {
|
||||
Row(verticalAlignment = CenterVertically, modifier = Modifier.weight(1f)) {
|
||||
Row(verticalAlignment = CenterVertically, modifier = remember { Modifier.weight(1f) }) {
|
||||
ReplyReaction(baseNote, grayTint, accountViewModel) {
|
||||
wantsToReplyTo = baseNote
|
||||
}
|
||||
@@ -123,17 +123,6 @@ fun ReplyReaction(
|
||||
iconSize: Dp = 20.dp,
|
||||
onPress: () -> Unit
|
||||
) {
|
||||
val repliesState by baseNote.live().replies.observeAsState()
|
||||
val replies = remember(repliesState) { repliesState?.note?.replies } ?: emptySet()
|
||||
|
||||
val isWriteable = remember { accountViewModel.isWriteable() }
|
||||
|
||||
val replyCount by remember(repliesState) {
|
||||
derivedStateOf {
|
||||
showCount(replies.size)
|
||||
}
|
||||
}
|
||||
|
||||
val context = LocalContext.current
|
||||
val scope = rememberCoroutineScope()
|
||||
|
||||
@@ -148,7 +137,7 @@ fun ReplyReaction(
|
||||
IconButton(
|
||||
modifier = iconButtonModifier,
|
||||
onClick = {
|
||||
if (isWriteable) {
|
||||
if (accountViewModel.isWriteable()) {
|
||||
onPress()
|
||||
} else {
|
||||
scope.launch {
|
||||
@@ -170,14 +159,26 @@ fun ReplyReaction(
|
||||
}
|
||||
|
||||
if (showCounter) {
|
||||
Text(
|
||||
" $replyCount",
|
||||
fontSize = 14.sp,
|
||||
color = grayTint
|
||||
)
|
||||
ReplyCounter(baseNote, grayTint)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun ReplyCounter(baseNote: Note, textColor: Color) {
|
||||
val repliesState by baseNote.live().replies.observeAsState()
|
||||
val replyCount by remember(repliesState) {
|
||||
derivedStateOf {
|
||||
" " + showCount(repliesState?.note?.replies?.size)
|
||||
}
|
||||
}
|
||||
|
||||
Text(
|
||||
text = replyCount,
|
||||
fontSize = 14.sp,
|
||||
color = textColor
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun BoostReaction(
|
||||
baseNote: Note,
|
||||
@@ -186,29 +187,6 @@ fun BoostReaction(
|
||||
iconSize: Dp = 20.dp,
|
||||
onQuotePress: () -> Unit
|
||||
) {
|
||||
val boostsState by baseNote.live().boosts.observeAsState()
|
||||
val boostedNote = remember(boostsState) { boostsState?.note } ?: return
|
||||
|
||||
val hasBoosted by remember(boostsState) {
|
||||
derivedStateOf {
|
||||
accountViewModel.hasBoosted(baseNote)
|
||||
}
|
||||
}
|
||||
|
||||
val wasBoostedByLoggedIn by remember(boostsState) {
|
||||
derivedStateOf {
|
||||
boostedNote.isBoostedBy(accountViewModel.userProfile())
|
||||
}
|
||||
}
|
||||
|
||||
val isWriteable = remember { accountViewModel.isWriteable() }
|
||||
|
||||
val boostCount by remember(boostsState) {
|
||||
derivedStateOf {
|
||||
showCount(boostedNote.boosts.size)
|
||||
}
|
||||
}
|
||||
|
||||
val context = LocalContext.current
|
||||
val scope = rememberCoroutineScope()
|
||||
|
||||
@@ -218,15 +196,11 @@ fun BoostReaction(
|
||||
Modifier.size(iconSize)
|
||||
}
|
||||
|
||||
val iconModifier = remember {
|
||||
Modifier.size(iconSize)
|
||||
}
|
||||
|
||||
IconButton(
|
||||
modifier = iconButtonModifier,
|
||||
onClick = {
|
||||
if (isWriteable) {
|
||||
if (hasBoosted) {
|
||||
if (accountViewModel.isWriteable()) {
|
||||
if (accountViewModel.hasBoosted(baseNote)) {
|
||||
accountViewModel.deleteBoostsTo(baseNote)
|
||||
} else {
|
||||
wantsToBoost = true
|
||||
@@ -256,16 +230,45 @@ fun BoostReaction(
|
||||
)
|
||||
}
|
||||
|
||||
Icon(
|
||||
painter = painterResource(R.drawable.ic_retweeted),
|
||||
null,
|
||||
modifier = iconModifier,
|
||||
tint = if (wasBoostedByLoggedIn) Color.Unspecified else grayTint
|
||||
)
|
||||
BoostIcon(baseNote, iconSize, grayTint, accountViewModel.userProfile())
|
||||
}
|
||||
|
||||
BoostText(baseNote, grayTint)
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun BoostIcon(baseNote: Note, iconSize: Dp = 20.dp, grayTint: Color, loggedIn: User) {
|
||||
val boostsState by baseNote.live().boosts.observeAsState()
|
||||
|
||||
val iconTint by remember(boostsState) {
|
||||
derivedStateOf {
|
||||
if (boostsState?.note?.isBoostedBy(loggedIn) == true) Color.Unspecified else grayTint
|
||||
}
|
||||
}
|
||||
|
||||
val iconModifier = remember {
|
||||
Modifier.size(iconSize)
|
||||
}
|
||||
|
||||
Icon(
|
||||
painter = painterResource(R.drawable.ic_retweeted),
|
||||
null,
|
||||
modifier = iconModifier,
|
||||
tint = iconTint
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun BoostText(baseNote: Note, grayTint: Color) {
|
||||
val boostsState by baseNote.live().boosts.observeAsState()
|
||||
val boostCount by remember(boostsState) {
|
||||
derivedStateOf {
|
||||
" " + showCount(boostsState?.note?.boosts?.size)
|
||||
}
|
||||
}
|
||||
|
||||
Text(
|
||||
" $boostCount",
|
||||
boostCount,
|
||||
fontSize = 14.sp,
|
||||
color = grayTint
|
||||
)
|
||||
@@ -279,29 +282,6 @@ fun LikeReaction(
|
||||
iconSize: Dp = 20.dp,
|
||||
heartSize: Dp = 16.dp
|
||||
) {
|
||||
val reactionsState by baseNote.live().reactions.observeAsState()
|
||||
val reactedNote = remember(reactionsState) { reactionsState?.note } ?: return
|
||||
|
||||
val hasReacted by remember(reactionsState) {
|
||||
derivedStateOf {
|
||||
accountViewModel.hasReactedTo(baseNote)
|
||||
}
|
||||
}
|
||||
|
||||
val wasReactedByLoggedIn by remember(reactionsState) {
|
||||
derivedStateOf {
|
||||
reactedNote.isReactedBy(accountViewModel.userProfile())
|
||||
}
|
||||
}
|
||||
|
||||
val isWriteable = remember { accountViewModel.isWriteable() }
|
||||
|
||||
val reactionCount by remember(reactionsState) {
|
||||
derivedStateOf {
|
||||
showCount(reactedNote.reactions.size)
|
||||
}
|
||||
}
|
||||
|
||||
val context = LocalContext.current
|
||||
val scope = rememberCoroutineScope()
|
||||
|
||||
@@ -309,15 +289,11 @@ fun LikeReaction(
|
||||
Modifier.size(iconSize)
|
||||
}
|
||||
|
||||
val iconModifier = remember {
|
||||
Modifier.size(heartSize)
|
||||
}
|
||||
|
||||
IconButton(
|
||||
modifier = iconButtonModifier,
|
||||
onClick = {
|
||||
if (isWriteable) {
|
||||
if (hasReacted) {
|
||||
if (accountViewModel.isWriteable()) {
|
||||
if (accountViewModel.hasReactedTo(baseNote)) {
|
||||
accountViewModel.deleteReactionTo(baseNote)
|
||||
} else {
|
||||
accountViewModel.reactTo(baseNote)
|
||||
@@ -333,25 +309,55 @@ fun LikeReaction(
|
||||
}
|
||||
}
|
||||
) {
|
||||
if (wasReactedByLoggedIn) {
|
||||
Icon(
|
||||
painter = painterResource(R.drawable.ic_liked),
|
||||
null,
|
||||
modifier = iconModifier,
|
||||
tint = Color.Unspecified
|
||||
)
|
||||
} else {
|
||||
Icon(
|
||||
painter = painterResource(R.drawable.ic_like),
|
||||
null,
|
||||
modifier = iconModifier,
|
||||
tint = grayTint
|
||||
)
|
||||
LikeIcon(baseNote, heartSize, grayTint, accountViewModel.userProfile())
|
||||
}
|
||||
|
||||
LikeText(baseNote, grayTint)
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun LikeIcon(baseNote: Note, iconSize: Dp = 20.dp, grayTint: Color, loggedIn: User) {
|
||||
val reactionsState by baseNote.live().reactions.observeAsState()
|
||||
|
||||
val wasReactedByLoggedIn by remember(reactionsState) {
|
||||
derivedStateOf {
|
||||
reactionsState?.note?.isReactedBy(loggedIn) == true
|
||||
}
|
||||
}
|
||||
|
||||
val iconModifier = remember {
|
||||
Modifier.size(iconSize)
|
||||
}
|
||||
|
||||
if (wasReactedByLoggedIn) {
|
||||
Icon(
|
||||
painter = painterResource(R.drawable.ic_liked),
|
||||
null,
|
||||
modifier = iconModifier,
|
||||
tint = Color.Unspecified
|
||||
)
|
||||
} else {
|
||||
Icon(
|
||||
painter = painterResource(R.drawable.ic_like),
|
||||
null,
|
||||
modifier = iconModifier,
|
||||
tint = grayTint
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun LikeText(baseNote: Note, grayTint: Color) {
|
||||
val reactionsState by baseNote.live().reactions.observeAsState()
|
||||
|
||||
val reactionsCount by remember(reactionsState) {
|
||||
derivedStateOf {
|
||||
" " + showCount(reactionsState?.note?.reactions?.size)
|
||||
}
|
||||
}
|
||||
|
||||
Text(
|
||||
" $reactionCount",
|
||||
reactionsCount,
|
||||
fontSize = 14.sp,
|
||||
color = grayTint
|
||||
)
|
||||
@@ -363,16 +369,12 @@ fun ZapReaction(
|
||||
baseNote: Note,
|
||||
grayTint: Color,
|
||||
accountViewModel: AccountViewModel,
|
||||
textModifier: Modifier = Modifier,
|
||||
iconSize: Dp = 20.dp,
|
||||
animationSize: Dp = 14.dp
|
||||
) {
|
||||
val accountState by accountViewModel.accountLiveData.observeAsState()
|
||||
val account = remember(accountState) { accountState?.account } ?: return
|
||||
|
||||
val zapsState by baseNote.live().zaps.observeAsState()
|
||||
val zappedNote = remember(zapsState) { zapsState?.note } ?: return
|
||||
|
||||
var wantsToZap by remember { mutableStateOf(false) }
|
||||
var wantsToChangeZapAmount by remember { mutableStateOf(false) }
|
||||
var wantsToSetCustomZap by remember { mutableStateOf(false) }
|
||||
@@ -382,32 +384,6 @@ fun ZapReaction(
|
||||
|
||||
var zappingProgress by remember { mutableStateOf(0f) }
|
||||
|
||||
var wasZappedByLoggedInUser by remember { mutableStateOf(false) }
|
||||
var zapAmountTxt by remember { mutableStateOf<String>("") }
|
||||
|
||||
LaunchedEffect(key1 = zapsState) {
|
||||
scope.launch(Dispatchers.IO) {
|
||||
if (!wasZappedByLoggedInUser) {
|
||||
val newWasZapped = accountViewModel.calculateIfNoteWasZappedByAccount(zappedNote)
|
||||
|
||||
if (wasZappedByLoggedInUser != newWasZapped) {
|
||||
wasZappedByLoggedInUser = newWasZapped
|
||||
}
|
||||
}
|
||||
|
||||
val newZapAmount = showAmount(account.calculateZappedAmount(zappedNote))
|
||||
if (newZapAmount != zapAmountTxt) {
|
||||
zapAmountTxt = newZapAmount
|
||||
}
|
||||
|
||||
if (wasZappedByLoggedInUser) {
|
||||
if (abs(zappingProgress - 1) < 0.001) {
|
||||
zappingProgress = 1f
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Row(
|
||||
verticalAlignment = CenterVertically,
|
||||
modifier = Modifier
|
||||
@@ -498,6 +474,7 @@ fun ZapReaction(
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
if (wantsToChangeZapAmount) {
|
||||
UpdateZapAmountDialog({ wantsToChangeZapAmount = false }, account = account)
|
||||
}
|
||||
@@ -506,28 +483,93 @@ fun ZapReaction(
|
||||
ZapCustomDialog({ wantsToSetCustomZap = false }, account = account, accountViewModel, baseNote)
|
||||
}
|
||||
|
||||
if (wasZappedByLoggedInUser) {
|
||||
Icon(
|
||||
imageVector = Icons.Default.Bolt,
|
||||
contentDescription = stringResource(R.string.zaps),
|
||||
modifier = Modifier.size(iconSize),
|
||||
tint = BitcoinOrange
|
||||
if (zappingProgress > 0.00001 && zappingProgress < 0.99999) {
|
||||
Spacer(Modifier.width(3.dp))
|
||||
|
||||
val animatedProgress = animateFloatAsState(
|
||||
targetValue = zappingProgress,
|
||||
animationSpec = ProgressIndicatorDefaults.ProgressAnimationSpec
|
||||
).value
|
||||
|
||||
CircularProgressIndicator(
|
||||
progress = animatedProgress,
|
||||
modifier = Modifier.size(animationSize),
|
||||
strokeWidth = 2.dp
|
||||
)
|
||||
} else {
|
||||
if (zappingProgress < 0.1 || zappingProgress > 0.99) {
|
||||
Icon(
|
||||
imageVector = Icons.Outlined.Bolt,
|
||||
contentDescription = stringResource(id = R.string.zaps),
|
||||
modifier = Modifier.size(iconSize),
|
||||
tint = grayTint
|
||||
)
|
||||
} else {
|
||||
Spacer(Modifier.width(3.dp))
|
||||
CircularProgressIndicator(
|
||||
progress = zappingProgress,
|
||||
modifier = Modifier.size(animationSize),
|
||||
strokeWidth = 2.dp
|
||||
)
|
||||
ZapIcon(
|
||||
baseNote,
|
||||
iconSize,
|
||||
grayTint,
|
||||
accountViewModel
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
ZapAmountText(baseNote, grayTint, accountViewModel)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun ZapIcon(
|
||||
baseNote: Note,
|
||||
iconSize: Dp,
|
||||
grayTint: Color,
|
||||
accountViewModel: AccountViewModel
|
||||
) {
|
||||
var wasZappedByLoggedInUser by remember { mutableStateOf(false) }
|
||||
val zapsState by baseNote.live().zaps.observeAsState()
|
||||
|
||||
val scope = rememberCoroutineScope()
|
||||
|
||||
LaunchedEffect(key1 = zapsState) {
|
||||
scope.launch(Dispatchers.IO) {
|
||||
zapsState?.note?.let {
|
||||
if (!wasZappedByLoggedInUser) {
|
||||
val newWasZapped = accountViewModel.calculateIfNoteWasZappedByAccount(it)
|
||||
|
||||
if (wasZappedByLoggedInUser != newWasZapped) {
|
||||
wasZappedByLoggedInUser = newWasZapped
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (wasZappedByLoggedInUser) {
|
||||
Icon(
|
||||
imageVector = Icons.Default.Bolt,
|
||||
contentDescription = stringResource(R.string.zaps),
|
||||
modifier = remember { Modifier.size(iconSize) },
|
||||
tint = BitcoinOrange
|
||||
)
|
||||
} else {
|
||||
Icon(
|
||||
imageVector = Icons.Outlined.Bolt,
|
||||
contentDescription = stringResource(id = R.string.zaps),
|
||||
modifier = remember { Modifier.size(iconSize) },
|
||||
tint = grayTint
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun ZapAmountText(
|
||||
baseNote: Note,
|
||||
grayTint: Color,
|
||||
accountViewModel: AccountViewModel
|
||||
) {
|
||||
val zapsState by baseNote.live().zaps.observeAsState()
|
||||
val zappedNote = remember(zapsState) { zapsState?.note } ?: return
|
||||
|
||||
val scope = rememberCoroutineScope()
|
||||
|
||||
var zapAmountTxt by remember { mutableStateOf("") }
|
||||
|
||||
LaunchedEffect(key1 = zapsState) {
|
||||
scope.launch(Dispatchers.IO) {
|
||||
val newZapAmount = showAmount(accountViewModel.calculateZapAmount(zappedNote))
|
||||
if (newZapAmount != zapAmountTxt) {
|
||||
zapAmountTxt = newZapAmount
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -535,8 +577,7 @@ fun ZapReaction(
|
||||
Text(
|
||||
zapAmountTxt,
|
||||
fontSize = 14.sp,
|
||||
color = grayTint,
|
||||
modifier = textModifier
|
||||
color = grayTint
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -19,14 +19,14 @@ import com.vitorpamplona.amethyst.R
|
||||
import com.vitorpamplona.amethyst.model.*
|
||||
import com.vitorpamplona.amethyst.ui.components.CreateClickableTextWithEmoji
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.withContext
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
@Composable
|
||||
fun ReplyInformation(replyTo: List<Note>?, mentions: List<String>, account: Account, nav: (String) -> Unit) {
|
||||
var dupMentions by remember { mutableStateOf<List<User>?>(null) }
|
||||
|
||||
LaunchedEffect(Unit) {
|
||||
withContext(Dispatchers.IO) {
|
||||
launch(Dispatchers.IO) {
|
||||
dupMentions = mentions.mapNotNull { LocalCache.checkGetOrCreateUser(it) }
|
||||
}
|
||||
}
|
||||
@@ -119,7 +119,7 @@ fun ReplyInformationChannel(replyTo: List<Note>?, mentions: List<String>, channe
|
||||
var sortedMentions by remember { mutableStateOf<List<User>?>(null) }
|
||||
|
||||
LaunchedEffect(Unit) {
|
||||
withContext(Dispatchers.IO) {
|
||||
launch(Dispatchers.IO) {
|
||||
sortedMentions = mentions
|
||||
.mapNotNull { LocalCache.checkGetOrCreateUser(it) }
|
||||
.toSet()
|
||||
|
||||
@@ -32,6 +32,7 @@ import com.vitorpamplona.amethyst.R
|
||||
import com.vitorpamplona.amethyst.ui.screen.ZapSetCard
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
||||
import com.vitorpamplona.amethyst.ui.theme.BitcoinOrange
|
||||
import com.vitorpamplona.amethyst.ui.theme.newItemBackgroundColor
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
@@ -63,7 +64,7 @@ fun ZapSetCompose(zapSetCard: ZapSetCard, isInnerNote: Boolean = false, routeFor
|
||||
}
|
||||
|
||||
var backgroundColor = if (isNew) {
|
||||
MaterialTheme.colors.primary.copy(0.12f).compositeOver(MaterialTheme.colors.background)
|
||||
MaterialTheme.colors.newItemBackgroundColor.compositeOver(MaterialTheme.colors.background)
|
||||
} else {
|
||||
MaterialTheme.colors.background
|
||||
}
|
||||
|
||||
@@ -31,6 +31,7 @@ import com.vitorpamplona.amethyst.R
|
||||
import com.vitorpamplona.amethyst.ui.screen.ZapUserSetCard
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
||||
import com.vitorpamplona.amethyst.ui.theme.BitcoinOrange
|
||||
import com.vitorpamplona.amethyst.ui.theme.newItemBackgroundColor
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.withContext
|
||||
|
||||
@@ -51,7 +52,7 @@ fun ZapUserSetCompose(zapSetCard: ZapUserSetCard, isInnerNote: Boolean = false,
|
||||
}
|
||||
|
||||
var backgroundColor = if (isNew) {
|
||||
MaterialTheme.colors.primary.copy(0.12f).compositeOver(MaterialTheme.colors.background)
|
||||
MaterialTheme.colors.newItemBackgroundColor.compositeOver(MaterialTheme.colors.background)
|
||||
} else {
|
||||
MaterialTheme.colors.background
|
||||
}
|
||||
|
||||
@@ -290,6 +290,7 @@ open class CardFeedViewModel(val localFilter: FeedFilter<Note>) : ViewModel() {
|
||||
}
|
||||
|
||||
fun <T> equalImmutableLists(list1: ImmutableList<T>, list2: ImmutableList<T>): Boolean {
|
||||
if (list1 === list2) return true
|
||||
if (list1.size != list2.size) return false
|
||||
for (i in 0 until list1.size) {
|
||||
if (list1[i] !== list2[i]) {
|
||||
|
||||
@@ -2,10 +2,11 @@ package com.vitorpamplona.amethyst.ui.screen
|
||||
|
||||
import androidx.compose.runtime.MutableState
|
||||
import com.vitorpamplona.amethyst.model.Note
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
|
||||
sealed class FeedState {
|
||||
object Loading : FeedState()
|
||||
class Loaded(val feed: MutableState<List<Note>>) : FeedState()
|
||||
class Loaded(val feed: MutableState<ImmutableList<Note>>) : FeedState()
|
||||
object Empty : FeedState()
|
||||
class FeedError(val errorMessage: String) : FeedState()
|
||||
}
|
||||
|
||||
@@ -25,6 +25,8 @@ import com.vitorpamplona.amethyst.ui.dal.UserProfileConversationsFeedFilter
|
||||
import com.vitorpamplona.amethyst.ui.dal.UserProfileNewThreadFeedFilter
|
||||
import com.vitorpamplona.amethyst.ui.dal.UserProfileReportsFeedFilter
|
||||
import com.vitorpamplona.amethyst.ui.dal.VideoFeedFilter
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
import kotlinx.collections.immutable.toImmutableList
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.Job
|
||||
@@ -55,8 +57,8 @@ abstract class FeedViewModel(val localFilter: FeedFilter<Note>) : ViewModel() {
|
||||
private val _feedContent = MutableStateFlow<FeedState>(FeedState.Loading)
|
||||
val feedContent = _feedContent.asStateFlow()
|
||||
|
||||
fun newListFromDataSource(): List<Note> {
|
||||
return localFilter.loadTop()
|
||||
fun newListFromDataSource(): ImmutableList<Note> {
|
||||
return localFilter.loadTop().toImmutableList()
|
||||
}
|
||||
|
||||
private fun refresh() {
|
||||
@@ -71,8 +73,7 @@ abstract class FeedViewModel(val localFilter: FeedFilter<Note>) : ViewModel() {
|
||||
|
||||
val oldNotesState = _feedContent.value
|
||||
if (oldNotesState is FeedState.Loaded) {
|
||||
// Using size as a proxy for has changed.
|
||||
if (notes != oldNotesState.feed.value) {
|
||||
if (!equalImmutableLists(notes, oldNotesState.feed.value)) {
|
||||
updateFeed(notes)
|
||||
}
|
||||
} else {
|
||||
@@ -80,7 +81,7 @@ abstract class FeedViewModel(val localFilter: FeedFilter<Note>) : ViewModel() {
|
||||
}
|
||||
}
|
||||
|
||||
private fun updateFeed(notes: List<Note>) {
|
||||
private fun updateFeed(notes: ImmutableList<Note>) {
|
||||
val scope = CoroutineScope(Job() + Dispatchers.Main)
|
||||
scope.launch {
|
||||
val currentState = _feedContent.value
|
||||
@@ -97,10 +98,20 @@ abstract class FeedViewModel(val localFilter: FeedFilter<Note>) : ViewModel() {
|
||||
|
||||
fun refreshFromOldState(newItems: Set<Note>) {
|
||||
val oldNotesState = _feedContent.value
|
||||
if (localFilter is AdditiveFeedFilter && oldNotesState is FeedState.Loaded) {
|
||||
val newList = localFilter.updateListWith(oldNotesState.feed.value, newItems.toSet())
|
||||
if (newList !== oldNotesState.feed.value) {
|
||||
updateFeed(newList)
|
||||
if (localFilter is AdditiveFeedFilter) {
|
||||
if (oldNotesState is FeedState.Loaded) {
|
||||
val newList = localFilter.updateListWith(oldNotesState.feed.value, newItems.toSet()).toImmutableList()
|
||||
if (!equalImmutableLists(newList, oldNotesState.feed.value)) {
|
||||
updateFeed(newList)
|
||||
}
|
||||
} else if (oldNotesState is FeedState.Empty) {
|
||||
val newList = localFilter.updateListWith(emptyList(), newItems.toSet()).toImmutableList()
|
||||
if (newList.isNotEmpty()) {
|
||||
updateFeed(newList)
|
||||
}
|
||||
} else {
|
||||
// Refresh Everything
|
||||
refreshSuspended()
|
||||
}
|
||||
} else {
|
||||
// Refresh Everything
|
||||
@@ -130,7 +141,9 @@ abstract class FeedViewModel(val localFilter: FeedFilter<Note>) : ViewModel() {
|
||||
init {
|
||||
collectorJob = viewModelScope.launch(Dispatchers.IO) {
|
||||
LocalCache.live.newEventBundles.collect { newNotes ->
|
||||
if (localFilter is AdditiveFeedFilter && _feedContent.value is FeedState.Loaded) {
|
||||
if (localFilter is AdditiveFeedFilter &&
|
||||
(_feedContent.value is FeedState.Loaded || _feedContent.value is FeedState.Empty)
|
||||
) {
|
||||
invalidateInsertData(newNotes)
|
||||
} else {
|
||||
// Refresh Everything
|
||||
|
||||
@@ -78,6 +78,7 @@ import com.vitorpamplona.amethyst.ui.note.NoteUsernameDisplay
|
||||
import com.vitorpamplona.amethyst.ui.note.ReactionsRow
|
||||
import com.vitorpamplona.amethyst.ui.note.timeAgo
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
||||
import com.vitorpamplona.amethyst.ui.theme.newItemBackgroundColor
|
||||
import kotlinx.coroutines.delay
|
||||
|
||||
@OptIn(ExperimentalMaterialApi::class)
|
||||
@@ -151,7 +152,7 @@ fun ThreadFeedView(noteId: String, viewModel: FeedViewModel, accountViewModel: A
|
||||
MaterialTheme.colors.onSurface.copy(alpha = 0.32f),
|
||||
if (item.idHex == noteId) MaterialTheme.colors.primary.copy(alpha = 0.52f) else MaterialTheme.colors.onSurface.copy(alpha = 0.32f)
|
||||
),
|
||||
parentBackgroundColor = if (item.idHex == noteId) MaterialTheme.colors.primary.copy(0.12f).compositeOver(MaterialTheme.colors.background) else null,
|
||||
parentBackgroundColor = if (item.idHex == noteId) MaterialTheme.colors.newItemBackgroundColor.compositeOver(MaterialTheme.colors.background) else null,
|
||||
isBoostedNote = false,
|
||||
unPackReply = false,
|
||||
accountViewModel = accountViewModel,
|
||||
@@ -353,11 +354,11 @@ fun NoteMaster(
|
||||
) {
|
||||
Column() {
|
||||
if (noteEvent is PeopleListEvent) {
|
||||
DisplayPeopleList(noteState, MaterialTheme.colors.background, accountViewModel, nav)
|
||||
DisplayPeopleList(baseNote, MaterialTheme.colors.background, accountViewModel, nav)
|
||||
} else if (noteEvent is AudioTrackEvent) {
|
||||
AudioTrackHeader(noteEvent, note, account.userProfile(), nav)
|
||||
} else if (noteEvent is PinListEvent) {
|
||||
PinListHeader(noteState, MaterialTheme.colors.background, accountViewModel, nav)
|
||||
PinListHeader(baseNote, MaterialTheme.colors.background, accountViewModel, nav)
|
||||
} else if (noteEvent is HighlightEvent) {
|
||||
DisplayHighlight(
|
||||
noteEvent.quote(),
|
||||
|
||||
@@ -23,6 +23,7 @@ import com.vitorpamplona.amethyst.service.model.ReportEvent
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.launch
|
||||
import java.math.BigDecimal
|
||||
import java.util.Locale
|
||||
|
||||
@Immutable
|
||||
@@ -66,6 +67,10 @@ class AccountViewModel(private val account: Account) : ViewModel() {
|
||||
return account.calculateIfNoteWasZappedByAccount(zappedNote)
|
||||
}
|
||||
|
||||
fun calculateZapAmount(zappedNote: Note): BigDecimal {
|
||||
return account.calculateZappedAmount(zappedNote)
|
||||
}
|
||||
|
||||
fun zap(note: Note, amount: Long, pollOption: Int?, message: String, context: Context, onError: (String) -> Unit, onProgress: (percent: Float) -> Unit, zapType: LnZapEvent.ZapType) {
|
||||
val lud16 = note.event?.zapAddress() ?: note.author?.info?.lud16?.trim() ?: note.author?.info?.lud06?.trim()
|
||||
|
||||
@@ -105,7 +110,7 @@ class AccountViewModel(private val account: Account) : ViewModel() {
|
||||
?: "Error parsing error message"
|
||||
)
|
||||
} else {
|
||||
onProgress(0.99f)
|
||||
onProgress(1f)
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
@@ -40,6 +40,7 @@ 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
|
||||
@@ -63,6 +64,7 @@ import androidx.lifecycle.viewmodel.compose.viewModel
|
||||
import com.vitorpamplona.amethyst.R
|
||||
import com.vitorpamplona.amethyst.model.Account
|
||||
import com.vitorpamplona.amethyst.model.Channel
|
||||
import com.vitorpamplona.amethyst.model.LocalCache
|
||||
import com.vitorpamplona.amethyst.model.Note
|
||||
import com.vitorpamplona.amethyst.service.NostrChannelDataSource
|
||||
import com.vitorpamplona.amethyst.ui.actions.NewChannelView
|
||||
@@ -77,6 +79,8 @@ import com.vitorpamplona.amethyst.ui.navigation.Route
|
||||
import com.vitorpamplona.amethyst.ui.note.ChatroomMessageCompose
|
||||
import com.vitorpamplona.amethyst.ui.screen.ChatroomFeedView
|
||||
import com.vitorpamplona.amethyst.ui.screen.NostrChannelFeedViewModel
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
@Composable
|
||||
fun ChannelScreen(
|
||||
@@ -158,7 +162,12 @@ fun ChannelScreen(
|
||||
|
||||
Spacer(modifier = Modifier.height(10.dp))
|
||||
|
||||
Row(Modifier.padding(horizontal = 10.dp).animateContentSize(), verticalAlignment = Alignment.CenterVertically) {
|
||||
Row(
|
||||
Modifier
|
||||
.padding(horizontal = 10.dp)
|
||||
.animateContentSize(),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
val replyingNote = replyTo.value
|
||||
if (replyingNote != null) {
|
||||
Column(Modifier.weight(1f)) {
|
||||
@@ -182,7 +191,9 @@ fun ChannelScreen(
|
||||
Icon(
|
||||
imageVector = Icons.Default.Cancel,
|
||||
null,
|
||||
modifier = Modifier.padding(end = 5.dp).size(30.dp),
|
||||
modifier = Modifier
|
||||
.padding(end = 5.dp)
|
||||
.size(30.dp),
|
||||
tint = MaterialTheme.colors.onSurface.copy(alpha = 0.32f)
|
||||
)
|
||||
}
|
||||
@@ -192,7 +203,9 @@ fun ChannelScreen(
|
||||
|
||||
// LAST ROW
|
||||
Row(
|
||||
modifier = Modifier.padding(start = 10.dp, end = 10.dp, bottom = 10.dp, top = 5.dp).fillMaxWidth(),
|
||||
modifier = Modifier
|
||||
.padding(start = 10.dp, end = 10.dp, bottom = 10.dp, top = 5.dp)
|
||||
.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.SpaceBetween,
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
@@ -246,10 +259,26 @@ fun ChannelScreen(
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun ChannelHeader(channelHex: String, account: Account, nav: (String) -> Unit) {
|
||||
var baseChannel by remember { mutableStateOf<Channel?>(null) }
|
||||
val scope = rememberCoroutineScope()
|
||||
|
||||
LaunchedEffect(key1 = channelHex) {
|
||||
scope.launch(Dispatchers.IO) {
|
||||
baseChannel = LocalCache.checkGetOrCreateChannel(channelHex)
|
||||
}
|
||||
}
|
||||
|
||||
baseChannel?.let {
|
||||
ChannelHeader(it, account, nav)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun ChannelHeader(baseChannel: Channel, account: Account, nav: (String) -> Unit) {
|
||||
val channelState by baseChannel.live.observeAsState()
|
||||
val channel = channelState?.channel ?: return
|
||||
val channel = remember(channelState) { channelState?.channel } ?: return
|
||||
|
||||
val context = LocalContext.current.applicationContext
|
||||
|
||||
|
||||
@@ -123,7 +123,7 @@ fun FloatingButtons(navController: NavHostController, accountViewModel: AccountV
|
||||
}
|
||||
is AccountState.LoggedIn -> {
|
||||
if (currentRoute(navController)?.substringBefore("?") == Route.Home.base) {
|
||||
NewNoteButton(state.account, accountViewModel, nav)
|
||||
NewNoteButton(accountViewModel, nav)
|
||||
}
|
||||
if (currentRoute(navController) == Route.Message.base) {
|
||||
ChannelFabColumn(state.account, nav)
|
||||
|
||||
@@ -439,20 +439,39 @@ private fun ProfileActions(
|
||||
coroutineScope: CoroutineScope
|
||||
) {
|
||||
val accountUserState by account.userProfile().live().follows.observeAsState()
|
||||
val accountLocalUserState by account.live.observeAsState()
|
||||
val accountUser = remember(accountUserState) { accountUserState?.user } ?: return
|
||||
|
||||
val isHidden by remember(accountUserState, accountLocalUserState) {
|
||||
derivedStateOf {
|
||||
account.isHidden(baseUser)
|
||||
}
|
||||
}
|
||||
|
||||
val isLoggedInFollowingUser by remember(accountUserState, accountLocalUserState) {
|
||||
derivedStateOf {
|
||||
accountUser.isFollowingCached(baseUser)
|
||||
}
|
||||
}
|
||||
|
||||
val isUserFollowingLoggedIn by remember(accountUserState, accountLocalUserState) {
|
||||
derivedStateOf {
|
||||
baseUser.isFollowingCached(accountUser)
|
||||
}
|
||||
}
|
||||
|
||||
if (accountUser == baseUser) {
|
||||
EditButton(account)
|
||||
}
|
||||
|
||||
if (account.isHidden(baseUser)) {
|
||||
if (isHidden) {
|
||||
ShowUserButton {
|
||||
account.showUser(baseUser.pubkeyHex)
|
||||
}
|
||||
} else if (accountUser.isFollowingCached(baseUser)) {
|
||||
} else if (isLoggedInFollowingUser) {
|
||||
UnfollowButton { coroutineScope.launch(Dispatchers.IO) { account.unfollow(baseUser) } }
|
||||
} else {
|
||||
if (baseUser.isFollowingCached(accountUser)) {
|
||||
if (isUserFollowingLoggedIn) {
|
||||
FollowButton(
|
||||
{ coroutineScope.launch(Dispatchers.IO) { account.follow(baseUser) } },
|
||||
R.string.follow_back
|
||||
|
||||
@@ -86,6 +86,7 @@ import com.vitorpamplona.amethyst.ui.screen.LoadingFeed
|
||||
import com.vitorpamplona.amethyst.ui.screen.NostrVideoFeedViewModel
|
||||
import com.vitorpamplona.amethyst.ui.screen.ScrollStateKeys
|
||||
import com.vitorpamplona.amethyst.ui.screen.rememberForeverPagerState
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.launch
|
||||
@@ -198,7 +199,7 @@ fun FeedView(
|
||||
@OptIn(ExperimentalFoundationApi::class)
|
||||
@Composable
|
||||
fun SlidingCarousel(
|
||||
feed: MutableState<List<Note>>,
|
||||
feed: MutableState<ImmutableList<Note>>,
|
||||
accountViewModel: AccountViewModel,
|
||||
nav: (String) -> Unit,
|
||||
scrollStateKey: String? = null,
|
||||
@@ -325,9 +326,6 @@ private fun RelayBadges(baseNote: Note) {
|
||||
|
||||
@Composable
|
||||
fun ReactionsColumn(baseNote: Note, accountViewModel: AccountViewModel, nav: (String) -> Unit) {
|
||||
val accountState by accountViewModel.accountLiveData.observeAsState()
|
||||
val account = accountState?.account ?: return
|
||||
|
||||
var wantsToReplyTo by remember {
|
||||
mutableStateOf<Note?>(null)
|
||||
}
|
||||
@@ -337,11 +335,11 @@ fun ReactionsColumn(baseNote: Note, accountViewModel: AccountViewModel, nav: (St
|
||||
}
|
||||
|
||||
if (wantsToReplyTo != null) {
|
||||
NewPostView({ wantsToReplyTo = null }, wantsToReplyTo, null, account, accountViewModel, nav)
|
||||
NewPostView({ wantsToReplyTo = null }, wantsToReplyTo, null, accountViewModel, nav)
|
||||
}
|
||||
|
||||
if (wantsToQuote != null) {
|
||||
NewPostView({ wantsToQuote = null }, null, wantsToQuote, account, accountViewModel, nav)
|
||||
NewPostView({ wantsToQuote = null }, null, wantsToQuote, accountViewModel, nav)
|
||||
}
|
||||
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
|
||||
@@ -2,6 +2,7 @@ package com.vitorpamplona.amethyst.ui.theme
|
||||
|
||||
import android.app.Activity
|
||||
import androidx.compose.foundation.isSystemInDarkTheme
|
||||
import androidx.compose.material.Colors
|
||||
import androidx.compose.material.MaterialTheme
|
||||
import androidx.compose.material.darkColors
|
||||
import androidx.compose.material.lightColors
|
||||
@@ -34,6 +35,10 @@ private val LightColorPalette = lightColors(
|
||||
*/
|
||||
)
|
||||
|
||||
val Colors.newItemBackgroundColor: Color
|
||||
@Composable
|
||||
get() = if (isLight) primary.copy(0.05f) else primary.copy(0.12f)
|
||||
|
||||
@Composable
|
||||
fun AmethystTheme(darkTheme: Boolean = isSystemInDarkTheme(), content: @Composable () -> Unit) {
|
||||
val colors = if (darkTheme) {
|
||||
|
||||
390
app/src/main/res/values-cs/strings.xml
Normal file
390
app/src/main/res/values-cs/strings.xml
Normal file
@@ -0,0 +1,390 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources xmlns:tools="http://schemas.android.com/tools">
|
||||
<string name="point_to_the_qr_code">Nasměrujte na QR kód</string>
|
||||
<string name="show_qr">Zobrazit QR kód</string>
|
||||
<string name="profile_image">Profilový obrázek</string>
|
||||
<string name="scan_qr">Skenovat QR kód</string>
|
||||
<string name="show_anyway">Zobrazit přesto</string>
|
||||
<string name="post_was_flagged_as_inappropriate_by">Příspěvek byl označen jako nevhodný uživatelem</string>
|
||||
<string name="post_not_found">Příspěvek nenalezen</string>
|
||||
<string name="channel_image">Obrázek kanálu</string>
|
||||
<string name="referenced_event_not_found">Odkazovaná událost nebyla nalezena</string>
|
||||
<string name="could_not_decrypt_the_message">Nepodařilo se dešifrovat zprávu</string>
|
||||
<string name="group_picture">Obrázek skupiny</string>
|
||||
<string name="explicit_content">Explicitní obsah</string>
|
||||
<string name="spam">Spam</string>
|
||||
<string name="impersonation">Zneužívání identity</string>
|
||||
<string name="illegal_behavior">Nezákonné jednání</string>
|
||||
<string name="unknown">Neznámý</string>
|
||||
<string name="relay_icon">Ikona předávání</string>
|
||||
<string name="unknown_author">Neznámý autor</string>
|
||||
<string name="copy_text">Kopírovat text</string>
|
||||
<string name="copy_user_pubkey">Kopírovat ID autora</string>
|
||||
<string name="copy_note_id">Kopírovat ID poznámky</string>
|
||||
<string name="broadcast">Vysílání</string>
|
||||
<string name="request_deletion">Požadovat smazání</string>
|
||||
<string name="block_hide_user"><![CDATA[Blokovat a skrýt uživatele]]></string>
|
||||
<string name="report_spam_scam">Nahlásit spam / podvod</string>
|
||||
<string name="report_impersonation">Nahlásit zneužívání identity</string>
|
||||
<string name="report_explicit_content">Nahlásit explicitní obsah</string>
|
||||
<string name="report_illegal_behaviour">Nahlásit nezákonné jednání</string>
|
||||
<string name="login_with_a_private_key_to_be_able_to_reply">Pro odpověď se přihlaste s privátním klíčem</string>
|
||||
<string name="login_with_a_private_key_to_be_able_to_boost_posts">Pro zvýšení příspěvků se přihlaste s privátním klíčem</string>
|
||||
<string name="login_with_a_private_key_to_like_posts">Přihlásit se pomocí soukromého klíče pro označení příspěvků jako líbivé</string>
|
||||
<string name="no_zap_amount_setup_long_press_to_change">Není nastaveno množství Zap. Dlouhým stisknutím změňte</string>
|
||||
<string name="login_with_a_private_key_to_be_able_to_send_zaps">Přihlaste se pomocí soukromého klíče, abyste mohli odesílat Zapy</string>
|
||||
<string name="zaps">Zapy</string>
|
||||
<string name="view_count">Počet zobrazení</string>
|
||||
<string name="boost">Boost</string>
|
||||
<string name="boosted">boostováno</string>
|
||||
<string name="quote">Citovat</string>
|
||||
<string name="new_amount_in_sats">Nová částka v sats</string>
|
||||
<string name="add">Přidat</string>
|
||||
<string name="replying_to">"odpovídá na "</string>
|
||||
<string name="and">"a "</string>
|
||||
<string name="in_channel">"v kanálu "</string>
|
||||
<string name="profile_banner">Banner profilu</string>
|
||||
<string name="following">"Sleduje"</string>
|
||||
<string name="followers">"Sledující"</string>
|
||||
<string name="profile">Profil</string>
|
||||
<string name="security_filters">Bezpečnostní filtry</string>
|
||||
<string name="log_out">Odhlásit</string>
|
||||
<string name="show_more">Zobrazit více</string>
|
||||
<string name="lightning_invoice">Blesková faktura</string>
|
||||
<string name="pay">Zaplatit</string>
|
||||
<string name="lightning_tips">Bleskové tipy</string>
|
||||
<string name="note_to_receiver">Poznámka pro příjemce</string>
|
||||
<string name="thank_you_so_much">Děkuji moc!</string>
|
||||
<string name="amount_in_sats">Částka v sats</string>
|
||||
<string name="send_sats">Poslat sats</string>
|
||||
<string name="error_parsing_preview_for">"Chyba při zpracování náhledu pro %1$s : %2$s"</string>
|
||||
<string name="preview_card_image_for">"Náhled obrázku karty pro %1$s"</string>
|
||||
<string name="new_channel">Nový kanál</string>
|
||||
<string name="channel_name">Název kanálu</string>
|
||||
<string name="my_awesome_group">Má skvělá skupina</string>
|
||||
<string name="picture_url">URL obrázku</string>
|
||||
<string name="description">Popis</string>
|
||||
<string name="about_us">"O nás.. "</string>
|
||||
<string name="what_s_on_your_mind">Co vás napadá?</string>
|
||||
<string name="post">Příspěvek</string>
|
||||
<string name="save">Uložit</string>
|
||||
<string name="create">Vytvořit</string>
|
||||
<string name="cancel">Zrušit</string>
|
||||
<string name="failed_to_upload_the_image">Selhalo nahrávání obrázku</string>
|
||||
<string name="relay_address">Adresa relé</string>
|
||||
<string name="posts">Příspěvky</string>
|
||||
<string name="bytes">Byty</string>
|
||||
<string name="errors">Chyby</string>
|
||||
<string name="home_feed">Domovský kanál</string>
|
||||
<string name="private_message_feed">Kanál soukromých zpráv</string>
|
||||
<string name="public_chat_feed">Kanál veřejného chatu</string>
|
||||
<string name="global_feed"> Globální kanál</string>
|
||||
<string name="search_feed">Kanál vyhledávání</string>
|
||||
<string name="add_a_relay">Přidat přeposílání</string>
|
||||
<string name="display_name">Zobrazované jméno</string>
|
||||
<string name="my_display_name">Moje zobrazované jméno</string>
|
||||
<string name="username">Uživatelské jméno</string>
|
||||
<string name="my_username">Moje uživatelské jméno</string>
|
||||
<string name="about_me">O mně</string>
|
||||
<string name="avatar_url">URL avatara</string>
|
||||
<string name="banner_url">URL banneru</string>
|
||||
<string name="website_url">URL webové stránky</string>
|
||||
<string name="ln_address">LN adresa</string>
|
||||
<string name="ln_url_outdated">LN URL (zastaralé)</string>
|
||||
<string name="image_saved_to_the_gallery">Obrázek uložen do galerie</string>
|
||||
<string name="failed_to_save_the_image">Chyba při ukládání obrázku</string>
|
||||
<string name="upload_image">Nahrát obrázek</string>
|
||||
<string name="uploading">Nahrávání…</string>
|
||||
<string name="user_does_not_have_a_lightning_address_setup_to_receive_sats">Uživatel nemá nastavenou LN adresu pro přijímání sats</string>
|
||||
<string name="reply_here">"Odpověď zde.. "</string>
|
||||
<string name="copies_the_note_id_to_the_clipboard_for_sharing">Zkopíruje ID poznámky do schránky pro sdílení</string>
|
||||
<string name="copy_channel_id_note_to_the_clipboard">Zkopírovat ID kanálu (poznámka) do schránky</string>
|
||||
<string name="edits_the_channel_metadata">Upravit metadata kanálu</string>
|
||||
<string name="join">Připojit se</string>
|
||||
<string name="known">Známé</string>
|
||||
<string name="new_requests">Nové žádosti</string>
|
||||
<string name="blocked_users">Blokovaní uživatelé</string>
|
||||
<string name="new_threads">Nové vlákna</string>
|
||||
<string name="conversations">Konverzace</string>
|
||||
<string name="notes">Poznámky</string>
|
||||
<string name="replies">Odpovědi</string>
|
||||
<string name="follows">"Sleduje"</string>
|
||||
<string name="reports">"Zprávy"</string>
|
||||
<string name="more_options">Více možností</string>
|
||||
<string name="relays">" Přeposílání"</string>
|
||||
<string name="website">Webová stránka</string>
|
||||
<string name="lightning_address">Lightning adresa</string>
|
||||
<string name="copies_the_nsec_id_your_password_to_the_clipboard_for_backup">Zkopíruje Nsec ID (vaše heslo) do schránky pro zálohování</string>
|
||||
<string name="copy_private_key_to_the_clipboard">Zkopírovat tajný klíč do schránky</string>
|
||||
<string name="copies_the_public_key_to_the_clipboard_for_sharing">Zkopíruje veřejný klíč do schránky pro sdílení</string>
|
||||
<string name="copy_public_key_npub_to_the_clipboard">Zkopírovat veřejný klíč (NPub) do schránky</string>
|
||||
<string name="send_a_direct_message">Poslat přímou zprávu</string>
|
||||
<string name="edits_the_user_s_metadata">Upravit uživatelova metadata</string>
|
||||
<string name="follow">Sledovat</string>
|
||||
<string name="follow_back">Následovat zpět</string>
|
||||
<string name="unblock">Odblokovat</string>
|
||||
<string name="copy_user_id">Zkopírovat ID uživatele</string>
|
||||
<string name="unblock_user">Odblokovat uživatele</string>
|
||||
<string name="npub_hex_username">"npub, hex, uživatelské jméno"</string>
|
||||
<string name="clear">Vymazat</string>
|
||||
<string name="app_logo">Logo aplikace</string>
|
||||
<string name="nsec_npub_hex_private_key">nsec / npub / hex privátní klíč</string>
|
||||
<string name="show_password">Zobrazit heslo</string>
|
||||
<string name="hide_password">Skrýt heslo</string>
|
||||
<string name="invalid_key">Neplatný klíč</string>
|
||||
<string name="i_accept_the">"Souhlasím s "</string>
|
||||
<string name="terms_of_use">podmínkami použití</string>
|
||||
<string name="acceptance_of_terms_is_required">Je vyžadováno přijetí podmínek</string>
|
||||
<string name="key_is_required">Klíč je povinný</string>
|
||||
<string name="login">Přihlásit se</string>
|
||||
<string name="generate_a_new_key">Vygenerovat nový klíč</string>
|
||||
<string name="loading_feed">Načítání zdroje</string>
|
||||
<string name="error_loading_replies">"Chyba při načítání odpovědí: "</string>
|
||||
<string name="try_again">Zkusit znovu</string>
|
||||
<string name="feed_is_empty">Zdroj je prázdný.</string>
|
||||
<string name="refresh">Obnovit</string>
|
||||
<string name="created">vytvořeno</string>
|
||||
<string name="with_description_of">s popisem</string>
|
||||
<string name="and_picture">a obrázkem</string>
|
||||
<string name="changed_chat_name_to">změnil název chatu na</string>
|
||||
<string name="description_to">popis na</string>
|
||||
<string name="and_picture_to">a obrázek na</string>
|
||||
<string name="leave">Opustit</string>
|
||||
<string name="unfollow">Přestat sledovat</string>
|
||||
<string name="channel_created">Kanál vytvořen</string>
|
||||
<string name="channel_information_changed_to">"Informace kanálu změněna na"</string>
|
||||
<string name="public_chat">Veřejný chat</string>
|
||||
<string name="posts_received">příspěvků přijato</string>
|
||||
<string name="remove">Odebrat</string>
|
||||
<string name="translations_auto">Automaticky</string>
|
||||
<string name="translations_translated_from">přeloženo z</string>
|
||||
<string name="translations_to">do</string>
|
||||
<string name="translations_show_in_lang_first">Zobrazit nejprve v %1$s</string>
|
||||
<string name="translations_always_translate_to_lang">Vždy překládat do %1$s</string>
|
||||
<string name="translations_never_translate_from_lang">Nikdy nepřekládat z %1$s</string>
|
||||
<string name="never">nikdy</string>
|
||||
<string name="now">nyní</string>
|
||||
<string name="h">h</string>
|
||||
<string name="m">m</string>
|
||||
<string name="d">d</string>
|
||||
<string name="nudity">Nahota</string>
|
||||
<string name="profanity_hateful_speech">Neslušnost / Nenávistná řeč</string>
|
||||
<string name="report_hateful_speech">Nahlásit nenávistnou řeč</string>
|
||||
<string name="report_nudity_porn">Nahlásit nahotu / pornografii</string>
|
||||
<string name="others">jiné</string>
|
||||
<string name="mark_all_known_as_read">Označit všechny známé jako přečtené</string>
|
||||
<string name="mark_all_new_as_read">Označit všechny nové jako přečtené</string>
|
||||
<string name="mark_all_as_read">Označit všechny jako přečtené</string>
|
||||
<string name="backup_keys">Zálohovat klíče</string>
|
||||
<string name="account_backup_tips_md" tools:ignore="Typos">
|
||||
## Tipy na zálohování a bezpečnost účtu
|
||||
\n\nVáš účet je zabezpečen tajným klíčem. Klíč je dlouhý náhodný řetězec začínající **nsec1**. Každý, kdo má přístup k vašemu tajnému klíči, může publikovat obsah pod vaší identitou.
|
||||
\n\n- **Nepoužívejte** svůj tajný klíč na webových stránkách nebo v softwaru, kterému nedůvěřujete.
|
||||
\n- Vývojáři aplikace Amethyst vás **nikdy** nebudou žádat o váš tajný klíč.
|
||||
\n- **Uchovávejte** si bezpečnou zálohu svého tajného klíče pro obnovení účtu. Doporučujeme používat správce hesel.
|
||||
</string>
|
||||
<string name="secret_key_copied_to_clipboard">Tajný klíč (nsec) zkopírován do schránky</string>
|
||||
<string name="copy_my_secret_key">Zkopírovat můj tajný klíč</string>
|
||||
<string name="biometric_authentication_failed">Autentizace se nezdařila</string>
|
||||
<string name="biometric_error">Chyba</string>
|
||||
<string name="badge_created_by">Vytvořeno uživatelem %1$s</string>
|
||||
<string name="badge_award_image_for">Obrázek ocenění od %1$s</string>
|
||||
<string name="new_badge_award_notif">Obdrželi jste nové ocenění</string>
|
||||
<string name="award_granted_to">Ocenění uděleno uživateli</string>
|
||||
<string name="copied_note_text_to_clipboard">Text poznámky zkopírován do schránky</string>
|
||||
<string name="copied_user_id_to_clipboard" tools:ignore="Typos">Uživatelovo ID (@npub) zkopírováno do schránky</string>
|
||||
<string name="copied_note_id_to_clipboard" tools:ignore="Typos">ID poznámky (@note1) zkopírováno do schránky</string>
|
||||
<string name="select_text_dialog_top">Vybrat text</string>
|
||||
<string name="private_conversation_notification">"<Nelze dešifrovat soukromou zprávu>\n\nByl jste zmíněn v soukromé/zašifrované konverzaci mezi %1$s a %2$s."</string>
|
||||
<string name="account_switch_add_account_dialog_title">Přidat nový účet</string>
|
||||
<string name="drawer_accounts">Účty</string>
|
||||
<string name="account_switch_select_account">Vybrat účet</string>
|
||||
<string name="account_switch_add_account_btn">Přidat nový účet</string>
|
||||
<string name="account_switch_active_account">Aktivní účet</string>
|
||||
<string name="account_switch_has_private_key">Má soukromý klíč</string>
|
||||
<string name="account_switch_pubkey_only">Pouze veřejný klíč, žádný soukromý klíč</string>
|
||||
<string name="back">Zpět</string>
|
||||
<string name="quick_action_select">Vybrat</string>
|
||||
<string name="quick_action_share_browser_link">Sdílet odkaz prohlížeče</string>
|
||||
<string name="quick_action_share">Sdílet</string>
|
||||
<string name="quick_action_copy_user_id">ID autora</string>
|
||||
<string name="quick_action_copy_note_id">ID poznámky</string>
|
||||
<string name="quick_action_copy_text">Kopírovat text</string>
|
||||
<string name="quick_action_delete">Smazat</string>
|
||||
<string name="quick_action_unfollow">Přestat sledovat</string>
|
||||
<string name="quick_action_follow">Sledovat</string>
|
||||
<string name="quick_action_request_deletion_alert_title">Požadavek na smazání</string>
|
||||
<string name="quick_action_request_deletion_alert_body">Amethyst požádá o smazání vaší poznámky ze spojek, ke kterým jste v současnosti připojeni. Není zaručeno, že vaše poznámka bude trvale smazána z těchto spojek nebo z dalších spojek, kde může být uložena.</string>
|
||||
<string name="quick_action_block_dialog_btn">Blokovat</string>
|
||||
<string name="quick_action_delete_dialog_btn">Smazat</string>
|
||||
<string name="quick_action_block">Blokovat</string>
|
||||
<string name="quick_action_report">Nahlásit</string>
|
||||
<string name="quick_action_delete_button">Smazat</string>
|
||||
<string name="quick_action_dont_show_again_button">Nepředvádět znovu</string>
|
||||
<string name="report_dialog_spam">Spam nebo podvody</string>
|
||||
<string name="report_dialog_profanity">Sprostota nebo nenávistné chování</string>
|
||||
<string name="report_dialog_impersonation">Zákeřná zneužití</string>
|
||||
<string name="report_dialog_nudity">Nahota nebo grafický obsah</string>
|
||||
<string name="report_dialog_illegal">Nelegální chování</string>
|
||||
<string name="report_dialog_blocking_a_user">Blokování uživatele skryje jejich obsah ve vaší aplikaci. Vaše poznámky jsou stále veřejně viditelné, včetně osob, které blokujete. Blokovaní uživatelé jsou uvedeni na obrazovce s bezpečnostními filtry.</string>
|
||||
<string name="report_dialog_block_hide_user_btn"><![CDATA[Blokovat a skrýt uživatele]]></string>
|
||||
<string name="report_dialog_report_btn">Nahlásit zneužití</string>
|
||||
<string name="report_dialog_reminder_public">Všechny zveřejněné zprávy budou veřejně viditelné.</string>
|
||||
<string name="report_dialog_additional_reason_placeholder">Volitelně poskytněte další kontext k vaší zprávě…</string>
|
||||
<string name="report_dialog_additional_reason_label">Další kontext</string>
|
||||
<string name="report_dialog_select_reason_label">Důvod</string>
|
||||
<string name="report_dialog_select_reason_placeholder">Vyberte důvod…</string>
|
||||
<string name="report_dialog_post_report_btn">Odeslat zprávu</string>
|
||||
<string name="report_dialog_title">Blokovat a nahlásit</string>
|
||||
<string name="block_only">Blokovat</string>
|
||||
<string name="bookmarks">Záložky</string>
|
||||
<string name="private_bookmarks">Soukromé záložky</string>
|
||||
<string name="public_bookmarks">Veřejné záložky</string>
|
||||
<string name="add_to_private_bookmarks">Přidat do soukromých záložek</string>
|
||||
<string name="add_to_public_bookmarks">Přidat do veřejných záložek</string>
|
||||
<string name="remove_from_private_bookmarks">Odebrat ze soukromých záložek</string>
|
||||
<string name="remove_from_public_bookmarks">Odebrat z veřejných záložek</string>
|
||||
|
||||
<string name="wallet_connect_service">Služba Wallet Connect</string>
|
||||
<string name="wallet_connect_service_explainer">Autorizuje Nostr Secret k provedení platby zapsům, aniž byste opustili aplikaci. Uchovávejte tajemství v bezpečí a pokud je to možné, používejte soukromý přepojovač.</string>
|
||||
<string name="wallet_connect_service_pubkey">Veřejný klíč Wallet Connect</string>
|
||||
<string name="wallet_connect_service_relay">Relé Wallet Connect</string>
|
||||
<string name="wallet_connect_service_secret">Tajemství Wallet Connect</string>
|
||||
<string name="wallet_connect_service_show_secret">Zobrazit tajný klíč</string>
|
||||
<string name="wallet_connect_service_secret_placeholder">nsec / hex soukromý klíč</string>
|
||||
|
||||
<string name="pledge_amount_in_sats">Částka záruky v sats</string>
|
||||
<string name="post_poll">Zveřejnit hlasování</string>
|
||||
<string name="poll_heading_required">Povinná pole:</string>
|
||||
<string name="poll_zap_recipients">Příjemci zapsů</string>
|
||||
<string name="poll_primary_description">Hlavní popis hlasování…</string>
|
||||
<string name="poll_option_index">Možnost %s</string>
|
||||
<string name="poll_option_description">Popis možnosti hlasování</string>
|
||||
<string name="poll_heading_optional">Volitelná pole:</string>
|
||||
<string name="poll_zap_value_min">Minimální zaps</string>
|
||||
<string name="poll_zap_value_max">Maximální zaps</string>
|
||||
<string name="poll_consensus_threshold">Konsensus</string>
|
||||
<string name="poll_consensus_threshold_percent">(0–100)%</string>
|
||||
<string name="poll_closing_time">Uzavřít po</string>
|
||||
<string name="poll_closing_time_days">dnech</string>
|
||||
<string name="poll_is_closed">Hlasování je uzavřeno pro nové hlasy</string>
|
||||
<string name="poll_zap_amount">Částka zapsu</string>
|
||||
<string name="one_vote_per_user_on_atomic_votes">U tohoto typu hlasování je povolen pouze jeden hlas na uživatele</string>
|
||||
|
||||
<string name="looking_for_event">"Hledá se událost %1$s"</string>
|
||||
|
||||
<string name="custom_zaps_add_a_message">Přidat veřejnou zprávu</string>
|
||||
<string name="custom_zaps_add_a_message_private">Přidat soukromou zprávu</string>
|
||||
<string name="custom_zaps_add_a_message_nonzap">Přidat fakturu</string>
|
||||
|
||||
<string name="custom_zaps_add_a_message_example">Děkuji za veškerou práci!</string>
|
||||
|
||||
<string name="lightning_create_and_add_invoice">Vytvořit a přidat</string>
|
||||
<string name="poll_author_no_vote">Autoři hlasování nemohou hlasovat ve vlastních hlasováních.</string>
|
||||
|
||||
<string name="hash_verification_passed">Obrázek je stejný jako v původním příspěvku</string>
|
||||
<string name="hash_verification_failed">Obrázek se změnil. Autor možná změnu nezaznamenal</string>
|
||||
|
||||
<string name="content_description_add_image">Přidat obrázek</string>
|
||||
<string name="content_description_add_video">Přidat video</string>
|
||||
<string name="content_description_add_document">Přidat dokument</string>
|
||||
|
||||
<string name="add_content">Přidat do zprávy</string>
|
||||
<string name="content_description">Popis obsahu</string>
|
||||
<string name="content_description_example">Modrá loď na bílé písčité pláži při západu slunce</string>
|
||||
|
||||
<string name="zap_type">Typ zapsu</string>
|
||||
<string name="zap_type_explainer">Typ zapsu pro všechny možnosti</string>
|
||||
|
||||
<string name="zap_type_public">Veřejný</string>
|
||||
<string name="zap_type_public_explainer">Všichni mohou vidět transakci a zprávu</string>
|
||||
|
||||
<string name="zap_type_private">Soukromý</string>
|
||||
<string name="zap_type_private_explainer">Odesílatel a příjemce si mohou navzájem vidět a číst zprávu</string>
|
||||
|
||||
<string name="zap_type_anonymous">Anonymní</string>
|
||||
<string name="zap_type_anonymous_explainer">Příjemce a veřejnost neví, kdo platbu poslal</string>
|
||||
|
||||
<string name="zap_type_nonzap">Non-Zap</string>
|
||||
<string name="zap_type_nonzap_explainer">Žádná stopa v Nostr, pouze v Lightning</string>
|
||||
|
||||
<string name="file_server">Souborový server</string>
|
||||
<string name="zap_forward_lnAddress">LnAddress nebo @Uživatel</string>
|
||||
|
||||
<string name="upload_server_imgur">imgur.com – důvěryhodný</string>
|
||||
<string name="upload_server_imgur_explainer">Imgur může upravit soubor</string>
|
||||
|
||||
<string name="upload_server_nostrimg">nostrimg.com – důvěryhodný</string>
|
||||
<string name="upload_server_nostrimg_explainer">NostrImg může upravit soubor</string>
|
||||
|
||||
<string name="upload_server_nostrbuild">nostr.build – důvěryhodný</string>
|
||||
<string name="upload_server_nostrbuild_explainer">Nostr.build může upravit soubor</string>
|
||||
|
||||
<string name="upload_server_nostrfilesdev">nostrfiles.dev – důvěryhodný</string>
|
||||
<string name="upload_server_nostrfilesdev_explainer">Nostrfiles.dev může upravit soubor</string>
|
||||
|
||||
<string name="upload_server_imgur_nip94">Ověřitelný Imgur (NIP-94)</string>
|
||||
<string name="upload_server_imgur_nip94_explainer">Ověří, zda Imgur upravil soubor. Nový NIP: jiní klienti ho možná neuvidí</string>
|
||||
|
||||
<string name="upload_server_nostrimg_nip94">Ověřitelný NostrImg (NIP-94)</string>
|
||||
<string name="upload_server_nostrimg_nip94_explainer">Ověří, zda NostrImg upravil soubor. Nový NIP: jiní klienti ho možná neuvidí</string>
|
||||
|
||||
<string name="upload_server_nostrbuild_nip94">Ověřitelný Nostr.build (NIP-94)</string>
|
||||
<string name="upload_server_nostrbuild_nip94_explainer">Ověří, zda Nostr.build upravil soubor. Nový NIP: jiní klienti ho možná neuvidí</string>
|
||||
|
||||
<string name="upload_server_nostrfilesdev_nip94">Ověřitelný Nostrfiles.dev (NIP-94)</string>
|
||||
<string name="upload_server_nostrfilesdev_nip94_explainer">Ověří, zda Nostrfiles.dev upravil soubor. Nový NIP: jiní klienti ho možná neuvidí</string>
|
||||
|
||||
<string name="upload_server_relays_nip95">Vaše relé (NIP-95)</string>
|
||||
<string name="upload_server_relays_nip95_explainer">Soubory jsou hostovány vašimi relé. Nový NIP: zkontrolujte, zda je podporují</string>
|
||||
|
||||
<string name="connect_via_tor_short">Nastavení Tor/Orbot</string>
|
||||
<string name="connect_via_tor">Připojit se přes váš Orbot</string>
|
||||
|
||||
<string name="do_you_really_want_to_disable_tor_title">Odpojit se od Orbotu/Toru?</string>
|
||||
<string name="do_you_really_want_to_disable_tor_text">Vaše data budou okamžitě přenesena do běžné sítě</string>
|
||||
<string name="yes">Ano</string>
|
||||
<string name="no">Ne</string>
|
||||
|
||||
<string name="follow_list_selection">Seznam sledovaných</string>
|
||||
<string name="follow_list_kind3follows">Všechna sledování</string>
|
||||
<string name="follow_list_global">Globální</string>
|
||||
<string name="connect_through_your_orbot_setup_markdown">
|
||||
## Připojte se přes Tor s Orbotem
|
||||
\n\n1. Nainstalujte [Orbot](https://play.google.com/store/apps/details?id=org.torproject.android)
|
||||
\n2. Spusťte Orbot
|
||||
\n3. V Orbotu zkontrolujte port pro Socks. Výchozí hodnota je 9050
|
||||
\n4. Pokud je to nutné, změňte port v Orbotu
|
||||
\n5. Nastavte port Socks v této obrazovce
|
||||
\n6. Stiskněte tlačítko Aktivovat Tor
|
||||
</string>
|
||||
<string name="orbot_socks_port">Orbot Socks Port</string>
|
||||
<string name="invalid_port_number">Neplatné číslo portu</string>
|
||||
<string name="use_orbot">Použít Orbot</string>
|
||||
<string name="disconnect_from_your_orbot_setup">Odpojit Tor/Orbot</string>
|
||||
|
||||
<string name="app_notification_dms_channel_name">Soukromé zprávy</string>
|
||||
<string name="app_notification_dms_channel_description">Upozorní vás, když dorazí soukromá zpráva</string>
|
||||
|
||||
<string name="app_notification_zaps_channel_name">Přijaté zapsy</string>
|
||||
<string name="app_notification_zaps_channel_description">Upozorní vás, když vás někdo zapsne</string>
|
||||
<string name="app_notification_zaps_channel_message">%1$s sats</string>
|
||||
<string name="app_notification_zaps_channel_message_from">Od %1$s</string>
|
||||
<string name="app_notification_zaps_channel_message_for">pro %1$s</string>
|
||||
|
||||
<string name="reply_notify">Upozornit: </string>
|
||||
|
||||
<string name="channel_list_join_conversation">Připojit se ke konverzaci</string>
|
||||
<string name="channel_list_user_or_group_id">ID uživatele nebo skupiny</string>
|
||||
<string name="channel_list_user_or_group_id_demo">npub, nevent nebo hex</string>
|
||||
<string name="channel_list_create_channel">Vytvořit</string>
|
||||
<string name="channel_list_join_channel">Připojit se</string>
|
||||
|
||||
<string name="today">Dnes</string>
|
||||
|
||||
<string name="content_warning">Upozornění na obsah</string>
|
||||
<string name="content_warning_explanation">Tento příspěvek obsahuje citlivý obsah, který může být pro některé lidi urážlivý nebo rušivý</string>
|
||||
<string name="content_warning_hide_all_sensitive_content">Vždy skrýt citlivý obsah</string>
|
||||
<string name="content_warning_show_all_sensitive_content">Vždy zobrazit citlivý obsah</string>
|
||||
<string name="content_warning_see_warnings">Vždy zobrazovat upozornění na obsah</string>
|
||||
</resources>
|
||||
@@ -3,11 +3,11 @@
|
||||
<string name="app_name_debug" translatable="false">Amethyst Debug</string>
|
||||
<string name="point_to_the_qr_code"> به QRکد اشاره کنید</string>
|
||||
<string name="show_qr">QR کد را نشان بده </string>
|
||||
<string name="profile_image">تصویر پروفایل</string>
|
||||
<string name="profile_image">تصویر نمایه</string>
|
||||
<string name="scan_qr">اسکن QRکد </string>
|
||||
<string name="show_anyway">بهر حال نشان بده</string>
|
||||
<string name="post_was_flagged_as_inappropriate_by">این نوت توسط این افراد نامناسب اعلام شده</string>
|
||||
<string name="post_not_found">این نوت یافت نشد</string>
|
||||
<string name="post_was_flagged_as_inappropriate_by">این یادداشت توسط این افراد نامناسب اعلام شده</string>
|
||||
<string name="post_not_found">این یادداشت یافت نشد</string>
|
||||
<string name="channel_image">تصویر کانال</string>
|
||||
<string name="referenced_event_not_found">رویداد مربوطه یافت نشد</string>
|
||||
<string name="could_not_decrypt_the_message">قادر به رمزگشایی پیام نیست</string>
|
||||
@@ -21,7 +21,7 @@
|
||||
<string name="unknown_author">نویسنده ناشناس</string>
|
||||
<string name="copy_text">کپی متن</string>
|
||||
<string name="copy_user_pubkey">کپی شناسه نویسنده</string>
|
||||
<string name="copy_note_id">کپی شناسه نوت</string>
|
||||
<string name="copy_note_id">کپی شناسه یادداشت</string>
|
||||
<string name="broadcast">انتشار</string>
|
||||
<string name="request_deletion">درخواست حذف/string></string>
|
||||
<string name="block_hide_user">بلاک/پنهان کردن کاربر</string>
|
||||
@@ -30,14 +30,14 @@
|
||||
<string name="report_explicit_content">گزارش محتوای نامناسب</string>
|
||||
<string name="report_illegal_behaviour">گزارش رفتار ناهنجار</string>
|
||||
<string name="login_with_a_private_key_to_be_able_to_reply">با کلید خصوصی وارد شوید تا بتوانید پاسخ دهید</string>
|
||||
<string name="login_with_a_private_key_to_be_able_to_boost_posts">با کلید خصوصی وارد شوید تا بتوانید نوت را بازارسال کنید</string>
|
||||
<string name="login_with_a_private_key_to_like_posts">با کلید خصوصی وارد شوید تا بتوانید نوت را لایک کنید</string>
|
||||
<string name="login_with_a_private_key_to_be_able_to_boost_posts">با کلید خصوصی وارد شوید تا بتوانید یادداشت را بازنشر کنید</string>
|
||||
<string name="login_with_a_private_key_to_like_posts">با کلید خصوصی وارد شوید تا بتوانید یادداشت را لایک کنید</string>
|
||||
<string name="no_zap_amount_setup_long_press_to_change">مقداری برای زپ تعیین نشده. نگه دارید تا تغییر دهید</string>
|
||||
<string name="login_with_a_private_key_to_be_able_to_send_zaps">با کلید خصوصی وارد شوید تا بتوانید زپ کنید</string>
|
||||
<string name="zaps">زپ</string>
|
||||
<string name="view_count">دیدن تعداد</string>
|
||||
<string name="boost">بازارسال</string>
|
||||
<string name="boosted">بازارسال شده</string>
|
||||
<string name="boost">بازنشر</string>
|
||||
<string name="boosted">بازنشر شده</string>
|
||||
<string name="quote">نقل قول</string>
|
||||
<string name="new_amount_in_sats">مبلغ جدید به ساتوشی</string>
|
||||
<string name="add">افزودن</string>
|
||||
@@ -51,7 +51,7 @@
|
||||
<string name="security_filters">فیلترهای ایمنی</string>
|
||||
<string name="log_out">خروج</string>
|
||||
<string name="show_more">ادامه</string>
|
||||
<string name="lightning_invoice">اینویس لایتنینگ</string>
|
||||
<string name="lightning_invoice">صورتحساب لایتنینگ</string>
|
||||
<string name="pay">پرداخت</string>
|
||||
<string name="lightning_tips">انعام لایتنینگ</string>
|
||||
<string name="note_to_receiver">یادداشت برای گیرنده</string>
|
||||
@@ -73,7 +73,7 @@
|
||||
<string name="cancel">لغو</string>
|
||||
<string name="failed_to_upload_the_image">تصویر بارگذاری نشد</string>
|
||||
<string name="relay_address">آدرس رله</string>
|
||||
<string name="posts">نوت ها</string>
|
||||
<string name="posts">یادداشت ها</string>
|
||||
<string name="bytes">بایت</string>
|
||||
<string name="errors">خطاها</string>
|
||||
<string name="home_feed">خبرنامه اصلی</string>
|
||||
@@ -98,7 +98,7 @@
|
||||
<string name="uploading">...در حال بارگذاری</string>
|
||||
<string name="user_does_not_have_a_lightning_address_setup_to_receive_sats">این کاربر آدرس لایتنینگ برای دریافت ساتوشی تنظیم نکرده است</string>
|
||||
<string name="reply_here">"...اینجا پاسخ دهید "</string>
|
||||
<string name="copies_the_note_id_to_the_clipboard_for_sharing">شناسه نوت را برای اشتراک گذاری در کلیپبورد ذخیره می کند </string>
|
||||
<string name="copies_the_note_id_to_the_clipboard_for_sharing">شناسه یادداشت را برای اشتراک گذاری در کلیپبورد ذخیره می کند </string>
|
||||
<string name="copy_channel_id_note_to_the_clipboard">ذخیره شناسه کانال در کلیپبورد</string>
|
||||
<string name="edits_the_channel_metadata">متادیتای کانال را ذخیره می کند</string>
|
||||
<string name="join">پیوستن</string>
|
||||
@@ -107,7 +107,7 @@
|
||||
<string name="blocked_users">کاربران بلاک شده</string>
|
||||
<string name="new_threads">رشته جدید</string>
|
||||
<string name="conversations">گفتگوها</string>
|
||||
<string name="notes">نوت ها</string>
|
||||
<string name="notes">یادداشت ها</string>
|
||||
<string name="replies">پاسخ ها</string>
|
||||
<string name="follows">"دنبال شوندگان"</string>
|
||||
<string name="reports">"گزارش ها"</string>
|
||||
@@ -130,8 +130,8 @@
|
||||
<string name="clear">پاک کردن</string>
|
||||
<string name="app_logo">لوگوی اپ</string>
|
||||
<string name="nsec_npub_hex_private_key">nsec / npub / hex کلید خصوصی</string>
|
||||
<string name="show_password">نشان دادن پسورد</string>
|
||||
<string name="hide_password">پنهان کردن پسورد</string>
|
||||
<string name="show_password">نشان دادن گذرواژه</string>
|
||||
<string name="hide_password">پنهان کردن گذرواژه</string>
|
||||
<string name="invalid_key">کلید نامعتبر</string>
|
||||
<string name="i_accept_the">"می پذیرم "</string>
|
||||
<string name="terms_of_use">شرایط استفاده</string>
|
||||
@@ -139,8 +139,8 @@
|
||||
<string name="key_is_required">کلید الزامیست</string>
|
||||
<string name="login">ورود</string>
|
||||
<string name="generate_a_new_key">ساختن جفت کلید جدید</string>
|
||||
<string name="loading_feed">در حال بارگذاری خبرنامه</string>
|
||||
<string name="error_loading_replies">" :خطا در بارگذاری پاسخ ها"</string>
|
||||
<string name="loading_feed">در حال بارگیری خبرنامه</string>
|
||||
<string name="error_loading_replies">" :خطا در بارگیری پاسخ ها"</string>
|
||||
<string name="try_again">دوباره تلاش کنید</string>
|
||||
<string name="feed_is_empty">خبرنامه خالیست</string>
|
||||
<string name="refresh">تازه سازی</string>
|
||||
@@ -155,7 +155,7 @@
|
||||
<string name="channel_created">کانال ساخته شد</string>
|
||||
<string name="channel_information_changed_to">"اطلاعات کانال تغییر کرد به"</string>
|
||||
<string name="public_chat">گفتگوی عمومی</string>
|
||||
<string name="posts_received">نوت های دریافت شده</string>
|
||||
<string name="posts_received">یادداشت های دریافت شده</string>
|
||||
<string name="remove">بیرون کردن</string>
|
||||
<string name="sats" translatable="false">ساتوشی</string>
|
||||
<string name="translations_auto">خودکار</string>
|
||||
@@ -189,15 +189,15 @@
|
||||
</string>
|
||||
<string name="secret_key_copied_to_clipboard">کلید خصوصی در کلیپبورد کپی شد</string>
|
||||
<string name="copy_my_secret_key">کلید خصوصیم را کپی کن</string>
|
||||
<string name="biometric_authentication_failed">صحت سنجی انجام نشد</string>
|
||||
<string name="biometric_authentication_failed">احراز هویت انجام نشد</string>
|
||||
<string name="biometric_error">خطا</string>
|
||||
<string name="badge_created_by">"ساخته شده توسط %1$s"</string>
|
||||
<string name="badge_award_image_for">"تصویر مدال پاداش برای %1$s"</string>
|
||||
<string name="new_badge_award_notif">یک مدال پاداش دریافت کردید</string>
|
||||
<string name="award_granted_to">مدال پاداش داده شد به</string>
|
||||
<string name="copied_note_text_to_clipboard">نوت در کلیپبورد کپی شد</string>
|
||||
<string name="copied_note_text_to_clipboard">یادداشت در کلیپبورد کپی شد</string>
|
||||
<string name="copied_user_id_to_clipboard" tools:ignore="Typos">@npubنویسنده در کلیپبورد کپی شد </string>
|
||||
<string name="copied_note_id_to_clipboard" tools:ignore="Typos">(@note1)شناسه نوت در کلیپبورد کپی شد</string>
|
||||
<string name="copied_note_id_to_clipboard" tools:ignore="Typos">(@note1)شناسه یادداشت در کلیپبورد کپی شد</string>
|
||||
<string name="select_text_dialog_top">انتخاب متن</string>
|
||||
<string name="github" translatable="false">Github Gist w/ Proof</string>
|
||||
<string name="telegram" translatable="false">Telegram</string>
|
||||
@@ -217,16 +217,16 @@
|
||||
<string name="account_switch_pubkey_only">فقط خواندن، کلید خصوصی ندارد</string>
|
||||
<string name="back">بازگشت</string>
|
||||
<string name="quick_action_select">انتخاب</string>
|
||||
<string name="quick_action_share_browser_link">اشتراک گذاری لینک پیماینده</string>
|
||||
<string name="quick_action_share_browser_link">اشتراک گذاری لینک مرورگر</string>
|
||||
<string name="quick_action_share">اشتراک گذاری</string>
|
||||
<string name="quick_action_copy_user_id">شناسه نویسنده</string>
|
||||
<string name="quick_action_copy_note_id">شناسه نوت</string>
|
||||
<string name="quick_action_copy_note_id">شناسه یادداشت</string>
|
||||
<string name="quick_action_copy_text">کپی متن</string>
|
||||
<string name="quick_action_delete">حذف</string>
|
||||
<string name="quick_action_unfollow">دنبال نکردن</string>
|
||||
<string name="quick_action_follow">دنبال کردن</string>
|
||||
<string name="quick_action_request_deletion_alert_title">Request Deletion</string>
|
||||
<string name="quick_action_request_deletion_alert_body">آماتیست درخواست می کند که نوت شما از رله هایی که درحال حاضر به آن متصل هستید حذف شود. هیچ تضمینی نیست که نوت شما برای همیشه از آن رله ها یا از رله های دیگری که ممکن است در آنها ذخیره شده باشد حذف خواهد شد.. </string>
|
||||
<string name="quick_action_request_deletion_alert_body">آماتیست درخواست می کند که یادداشت شما از رله هایی که درحال حاضر به آن متصل هستید حذف شود. هیچ تضمینی نیست که یادداشت شما برای همیشه از آن رله ها یا از رله های دیگری که ممکن است در آنها ذخیره شده باشد حذف خواهد شد.. </string>
|
||||
<string name="quick_action_block_dialog_btn">بلاک</string>
|
||||
<string name="quick_action_delete_dialog_btn">حذف</string>
|
||||
<string name="quick_action_block">بلاک</string>
|
||||
@@ -238,7 +238,7 @@
|
||||
<string name="report_dialog_impersonation">جعل هویت بدخواهانه</string>
|
||||
<string name="report_dialog_nudity">محتوای برهنگی یا نمایان</string>
|
||||
<string name="report_dialog_illegal">رفتار ناهنجار</string>
|
||||
<string name="report_dialog_blocking_a_user">بلاک کردن کاربر محتوای ایشان را در اپ شما پنهان می کند. نوت های شما همچنان بطور عمومی دیده می شوند که شامل کاربران بلاک شده توسط شما نیز هست. کاربران بلاک شده در صفحه فیلترهای ایمنی فهرست می شوند.</string>
|
||||
<string name="report_dialog_blocking_a_user">بلاک کردن کاربر محتوای ایشان را در اپ شما پنهان می کند. یادداشت های شما همچنان بطور عمومی دیده می شوند که شامل کاربران بلاک شده توسط شما نیز هست. کاربران بلاک شده در صفحه فیلترهای ایمنی فهرست می شوند.</string>
|
||||
<string name="report_dialog_block_hide_user_btn"><![CDATA[Block & Hide User]]></string>
|
||||
<string name="report_dialog_report_btn">گزارش سواستفاده</string>
|
||||
<string name="report_dialog_reminder_public">تمام گزارش ها بطور عمومی دیده می شوند</string>
|
||||
@@ -246,7 +246,7 @@
|
||||
<string name="report_dialog_additional_reason_label">توضیحات اضافه</string>
|
||||
<string name="report_dialog_select_reason_label">دلایل</string>
|
||||
<string name="report_dialog_select_reason_placeholder">دلیلی انتخاب کنید...</string>
|
||||
<string name="report_dialog_post_report_btn">گزارش نوت</string>
|
||||
<string name="report_dialog_post_report_btn">گزارش یادداشت</string>
|
||||
<string name="report_dialog_title">بلاک و گزارش</string>
|
||||
<string name="block_only">بلاک</string>
|
||||
|
||||
@@ -288,7 +288,7 @@
|
||||
|
||||
<string name="custom_zaps_add_a_message">افزودن پیام عمومی</string>
|
||||
<string name="custom_zaps_add_a_message_private">افزودن پیام خصوصی</string>
|
||||
<string name="custom_zaps_add_a_message_nonzap">افزودن پیام اینویس</string>
|
||||
<string name="custom_zaps_add_a_message_nonzap">افزودن پیام صورتحساب</string>
|
||||
|
||||
<string name="custom_zaps_add_a_message_example">از زحمات شما متشکریم!</string>
|
||||
|
||||
@@ -296,7 +296,6 @@
|
||||
<string name="poll_author_no_vote">سازندگان نظرسنجی نمی توانند در نظر سنجی خود رای دهند.</string>
|
||||
<string name="poll_hashtag" translatable="false">#زپ_نظرسنجی</string>
|
||||
|
||||
|
||||
<string name="hash_verification_passed">تصویر از زمان ارسال یکسان است</string>
|
||||
<string name="hash_verification_failed">تصویر تغییر کرده است. ممکن است نویسنده تغییر را ندیده باشد.</string>
|
||||
|
||||
@@ -328,14 +327,18 @@
|
||||
<string name="zap_forward_lnAddress">آدرس لایتنینگ یا @User</string>
|
||||
|
||||
<string name="upload_server_imgur">imgur.com - معتمد</string>
|
||||
<string name="upload_server_imgur_explainer">Imgur میتواند فایل را بهینه کند</string>
|
||||
<string name="upload_server_imgur_explainer">Imgur می تواند فایل را بهینه کند</string>
|
||||
|
||||
<string name="upload_server_nostrimg">nostrimg.com - معتمد</string>
|
||||
<string name="upload_server_nostrimg_explainer">NostrImg میتواند فایل را بهینه کند</string>
|
||||
<string name="upload_server_nostrimg_explainer">NostrImg می تواند فایل را بهینه کند</string>
|
||||
|
||||
<string name="upload_server_nostrbuild">nostr.build - معتمد</string>
|
||||
<string name="upload_server_nostrbuild_explainer">Nostr.build can modify the file</string>
|
||||
<string name="upload_server_nostrbuild_explainer">Nostr.build می تواند فایل را بهینه کند</string>
|
||||
|
||||
<string name="upload_server_nostrfilesdev">nostrfiles.dev - معتمد</string>
|
||||
<string name="upload_server_nostrfilesdev_explainer">Nostrfiles.dev می تواند فایل را بهینه کند</string>
|
||||
|
||||
|
||||
<string name="upload_server_imgur_nip94">قابل بررسی Imgur (NIP-94)</string>
|
||||
<string name="upload_server_imgur_nip94_explainer">بررسی کنید که Imgur فایل را بهینه کرده یا خیر . NIPجدید : سایر کلاینت ها ممکن است آن را نبینند. </string>
|
||||
|
||||
@@ -345,13 +348,66 @@
|
||||
<string name="upload_server_nostrbuild_nip94">Verifiable Nostr.build (NIP-94)</string>
|
||||
<string name="upload_server_nostrbuild_nip94_explainer">بررسی کنید که Nostr.build فایل را بهینه کرده یا خیر . NIPجدید : سایر کلاینت ها ممکن است آن را نبینند.</string>
|
||||
|
||||
<string name="upload_server_nostrfilesdev_nip94">قابل بررسی Nostrfiles.dev (NIP-94)</string>
|
||||
<string name="upload_server_nostrfilesdev_nip94_explainer">بررسی کنید که Nostrfiles.dev فایل را بهینه کرده یا خیر . NIPجدید : سایر کلاینت ها ممکن است آن را نبینند.</string>
|
||||
|
||||
|
||||
<string name="upload_server_relays_nip95">رله های شما (NIP-95)</string>
|
||||
<string name="upload_server_relays_nip95_explainer">فایل ها در رله های شما میزبانی می شوند. NIPجدید: بررسی کنید آیا پشتیبانی می کنند یا خیر </string>
|
||||
|
||||
<string name="connect_via_tor_short">Tor/Orbot تنظیمات</string>
|
||||
<string name="connect_via_tor"> از طریق تنظیم Orbot متصل شوید</string>
|
||||
|
||||
<string name="do_you_really_want_to_disable_tor_title">قطع اتصال از Orbot/Tor?</string>
|
||||
<string name="do_you_really_want_to_disable_tor_text">داده های شما بلافاصله در شبکه معمولی منتقل می شوند</string>
|
||||
<string name="yes">بله</string>
|
||||
<string name="no">خیر</string>
|
||||
|
||||
|
||||
<string name="follow_list_selection">لیست دنبال ها </string>
|
||||
<string name="follow_list_kind3follows">همه دنبال ها</string>
|
||||
<string name="follow_list_global">همگانی</string>
|
||||
<string name="connect_through_your_orbot_setup_markdown">
|
||||
## از طریق Tor با Orbot متصل شوید
|
||||
\n\n1. نصب کنید [Orbot](https://play.google.com/store/apps/details?id=org.torproject.android)
|
||||
\n2. شروع Orbot
|
||||
\n3. در Orbot, پورت Socksرا علامت بزنید.مقدار پیش فرض 9050 است.
|
||||
\n4. اگر لازم است پورت Orbot را تغییر دهید
|
||||
\n5. پورت Socks را در این صفحه پیکربندی کنید
|
||||
\n6. دکمه Activate را بفشارید تا از Orbot به عنوان پراکسی استفده نمایید
|
||||
</string>
|
||||
<string name="orbot_socks_port">Orbot Socks Port</string>
|
||||
<string name="invalid_port_number">شماره پورت نامعتبر</string>
|
||||
<string name="use_orbot">استفاده از Orbot</string>
|
||||
<string name="disconnect_from_your_orbot_setup">قطع اتصال Tor/Orbot</string>
|
||||
|
||||
<string name="app_notification_channel_id" translatable="false">DefaultChannelID</string>
|
||||
<string name="app_notification_private_message" translatable="false">اعلان جدید</string>
|
||||
|
||||
<string name="app_notification_dms_channel_id" translatable="false">PrivateMessagesID</string>
|
||||
<string name="app_notification_dms_channel_name">پیام خصوصی</string>
|
||||
<string name="app_notification_dms_channel_description">وقتی پیام خصوصی دریافت شود اعلان می کند</string>
|
||||
|
||||
<string name="app_notification_zaps_channel_id" translatable="false">ZapsID</string>
|
||||
<string name="app_notification_zaps_channel_name">زپ دریافت شد</string>
|
||||
<string name="app_notification_zaps_channel_description">وقتی کسی زپ کند اعلام می کند</string>
|
||||
<string name="app_notification_zaps_channel_message">%1$s ساتوشی</string>
|
||||
<string name="app_notification_zaps_channel_message_from">از %1$s</string>
|
||||
<string name="app_notification_zaps_channel_message_for">برای %1$s</string>
|
||||
|
||||
<string name="reply_notify">اعلان: </string>
|
||||
|
||||
<string name="channel_list_join_conversation">پیوستن به گفتگو</string>
|
||||
<string name="channel_list_user_or_group_id">شناسه گروه یا کاربر</string>
|
||||
<string name="channel_list_user_or_group_id_demo">npub, nevent or hex</string>
|
||||
<string name="channel_list_create_channel">ساختن</string>
|
||||
<string name="channel_list_join_channel">پیوستن</string>
|
||||
|
||||
<string name="today">امروز</string>
|
||||
|
||||
<string name="content_warning">هشدار محتوا</string>
|
||||
<string name="content_warning_explanation">این یادداشت حاوی محتوای حساس است که ممکن است از نظر برخی توهین آمیز یا مشوش کننده باشد</string>
|
||||
<string name="content_warning_hide_all_sensitive_content">همیشه محتوای حساس را پنهان کن</string>
|
||||
<string name="content_warning_show_all_sensitive_content">همیشه محتوای حساس را نشان بده</string>
|
||||
<string name="content_warning_see_warnings">همیشه هشدار محتوا را نشان بده</string>
|
||||
</resources>
|
||||
|
||||
@@ -0,0 +1,121 @@
|
||||
package com.vitorpamplona.amethyst.service
|
||||
|
||||
import io.mockk.MockKAnnotations
|
||||
import io.mockk.every
|
||||
import io.mockk.impl.annotations.SpyK
|
||||
import io.mockk.unmockkAll
|
||||
import org.junit.After
|
||||
import org.junit.Assert.assertEquals
|
||||
import org.junit.Assert.assertNull
|
||||
import org.junit.Assert.fail
|
||||
import org.junit.Before
|
||||
import org.junit.Test
|
||||
|
||||
class Nip05VerifierTest {
|
||||
private val ALL_UPPER_CASE_USER_NAME = "ONETWO"
|
||||
private val ALL_LOWER_CASE_USER_NAME = "onetwo"
|
||||
|
||||
@SpyK
|
||||
var nip05Verifier = Nip05Verifier()
|
||||
|
||||
@Before
|
||||
fun setUp() = MockKAnnotations.init(this)
|
||||
|
||||
@Test
|
||||
fun `test with matching case on user name`() {
|
||||
// Set-up
|
||||
val userNameToTest = ALL_UPPER_CASE_USER_NAME
|
||||
val expectedPubKey = "ca29c211f1c72d5b6622268ff43d2288ea2b2cb5b9aa196ff9f1704fc914b71b"
|
||||
|
||||
val nostrJson = "{\n" +
|
||||
" \"names\": {\n" +
|
||||
" \"$userNameToTest\": \"$expectedPubKey\" \n" +
|
||||
" }\n" +
|
||||
"}"
|
||||
|
||||
every { nip05Verifier.fetchNip05Json(any(), any(), any()) } answers {
|
||||
secondArg<(String) -> Unit>().invoke(nostrJson)
|
||||
}
|
||||
|
||||
val nip05 = "$userNameToTest@domain.com"
|
||||
var actualPubkeyHex = ""
|
||||
|
||||
// Execution
|
||||
nip05Verifier.verifyNip05(
|
||||
nip05,
|
||||
onSuccess = {
|
||||
actualPubkeyHex = it
|
||||
},
|
||||
onError = {
|
||||
fail("Test failure")
|
||||
}
|
||||
)
|
||||
|
||||
// Verification
|
||||
assertEquals(expectedPubKey, actualPubkeyHex)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `test with NOT matching case on user name`() {
|
||||
// Set-up
|
||||
val expectedPubKey = "ca29c211f1c72d5b6622268ff43d2288ea2b2cb5b9aa196ff9f1704fc914b71b"
|
||||
|
||||
val nostrJson = "{\n" +
|
||||
" \"names\": {\n" +
|
||||
" \"$ALL_UPPER_CASE_USER_NAME\": \"$expectedPubKey\" \n" +
|
||||
" }\n" +
|
||||
"}"
|
||||
every { nip05Verifier.fetchNip05Json(any(), any(), any()) } answers {
|
||||
secondArg<(String) -> Unit>().invoke(nostrJson)
|
||||
}
|
||||
|
||||
val nip05 = "$ALL_LOWER_CASE_USER_NAME@domain.com"
|
||||
var actualPubkeyHex = ""
|
||||
|
||||
// Execution
|
||||
nip05Verifier.verifyNip05(
|
||||
nip05,
|
||||
onSuccess = {
|
||||
actualPubkeyHex = it
|
||||
},
|
||||
onError = {
|
||||
fail("Test failure")
|
||||
}
|
||||
)
|
||||
|
||||
// Verification
|
||||
assertEquals(expectedPubKey, actualPubkeyHex)
|
||||
}
|
||||
|
||||
@After
|
||||
fun tearDown() {
|
||||
unmockkAll()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `execute assemble url with invalid value returns null`() {
|
||||
// given
|
||||
val nip05address = "this@that@that.com"
|
||||
|
||||
// when
|
||||
val actualValue = nip05Verifier.assembleUrl(nip05address)
|
||||
|
||||
// then
|
||||
assertNull(actualValue)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `execute assemble url with valid value returns nip05 url`() {
|
||||
// given
|
||||
val userName = "TheUser"
|
||||
val domain = "domain.com"
|
||||
val nip05address = "$userName@$domain"
|
||||
val expectedValue = "https://$domain/.well-known/nostr.json?name=$userName"
|
||||
|
||||
// when
|
||||
val actualValue = nip05Verifier.assembleUrl(nip05address)
|
||||
|
||||
// then
|
||||
assertEquals(expectedValue, actualValue)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user