Compare commits

...

25 Commits

Author SHA1 Message Date
Vitor Pamplona
4e4e798133 v0.68.4 2023-07-14 18:58:45 -04:00
Vitor Pamplona
e8bd4a12ac Reduces profile feed size to 200 and zaps, reports and followers to 400 2023-07-14 18:55:57 -04:00
Vitor Pamplona
dffed6e78f Moves synchronized Zap allocation to after checking if the zap already exists for this user. 2023-07-14 18:52:11 -04:00
Vitor Pamplona
46a4f360f7 Moves UserProfile NewThreads, Conversations, Reports and App Recommendations to be additive filters 2023-07-14 18:22:53 -04:00
Vitor Pamplona
64a5071345 Moves NIP-05 captions from NIP-05 to Nostr Address 2023-07-14 17:41:16 -04:00
Vitor Pamplona
1fba93b4f0 Moves badges out of the User class 2023-07-14 17:19:40 -04:00
Vitor Pamplona
caa67b03a8 Keeping media feed active from the start of the app. 2023-07-14 16:54:57 -04:00
Vitor Pamplona
5f1b2f9eb8 Only logging pruning and printing stacktraces when it matters 2023-07-14 16:54:43 -04:00
Vitor Pamplona
2968cb12aa Adds EOSE limits to the Video tab 2023-07-14 16:31:57 -04:00
Vitor Pamplona
8261b38de9 Adds EOSE limits to the Discovery tab. 2023-07-14 16:31:48 -04:00
Vitor Pamplona
2c4489a0d0 Only triggers mutable state if the state actually changes. 2023-07-14 16:31:31 -04:00
Vitor Pamplona
52dcb73fd7 Puts the Wifi Processing in an IO Thread 2023-07-14 16:30:48 -04:00
Vitor Pamplona
91a2f8ecfa v0.68.3 2023-07-14 09:33:08 -04:00
Vitor Pamplona
6905e1b47f Clicks on Community and Channel headers now take you to the community instead of expanding 2023-07-14 09:22:56 -04:00
Vitor Pamplona
640b0d3174 Adds the post highlighted to the highlight display 2023-07-14 09:04:34 -04:00
Vitor Pamplona
a86cd8772f Merge remote-tracking branch 'origin/HEAD' 2023-07-14 08:38:49 -04:00
Vitor Pamplona
83be195a43 Migrates DropDownMenu to MutableState 2023-07-14 08:38:34 -04:00
Vitor Pamplona
14bf73e2f1 Merge pull request #504 from akiomik/update-i18n-ja
Update translations for ja
2023-07-14 07:43:18 -04:00
Vitor Pamplona
efb7b06e4e Merge pull request #505 from greenart7c3/main
fix crash when changing language in android < 13
2023-07-14 07:42:58 -04:00
Vitor Pamplona
8b9f37d678 Merge pull request #506 from greenart7c3/string_resources
update pt-br translations
2023-07-14 07:42:29 -04:00
greenart7c3
e83f16f5c5 update pt-br translations 2023-07-14 06:46:30 -03:00
greenart7c3
e648970b17 fix crash when changing setting in android api < 13 2023-07-14 05:51:09 -03:00
Akiomi Kamakura
44badf8c86 Update translations for ja 2023-07-14 15:21:13 +09:00
Vitor Pamplona
516ff17eed Activates NostrCheck on the Media Feed 2023-07-13 21:32:48 -04:00
Vitor Pamplona
3b1533eb82 Fixes size of custom reactions on the Stories feed. 2023-07-13 21:19:08 -04:00
43 changed files with 794 additions and 374 deletions

View File

@@ -41,7 +41,7 @@ height="80">](https://github.com/vitorpamplona/amethyst/releases)
- [x] URI Support (NIP-21)
- [x] Created_at Limits (NIP-22)
- [x] Event Deletion (NIP-09)
- [x] Identity Verification (NIP-05)
- [x] Nostr Address (NIP-05)
- [x] Long-form Content (NIP-23)
- [x] Parameterized Replaceable Events (NIP-33)
- [x] Online Relay Search (NIP-50)

View File

@@ -13,8 +13,8 @@ android {
applicationId "com.vitorpamplona.amethyst"
minSdk 26
targetSdk 33
versionCode 250
versionName "0.68.2"
versionCode 252
versionName "0.68.4"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
vectorDrawables {

View File

@@ -79,6 +79,7 @@ object ServiceManager {
NostrAccountDataSource.start()
NostrChatroomListDataSource.start()
NostrDiscoveryDataSource.start()
NostrVideoDataSource.start()
// More Info Data Sources
NostrSingleEventDataSource.start()

View File

@@ -238,9 +238,6 @@ object LocalCache {
// Log.d("TN", "New Note (${notes.size},${users.size}) ${note.author?.toBestDisplayName()} ${note.event?.content()?.split("\n")?.take(100)} ${formattedDateTime(event.createdAt)}")
// Prepares user's profile view.
author.addNote(note)
// Counts the replies
replyTo.forEach {
it.addReply(note)
@@ -279,8 +276,6 @@ object LocalCache {
if (event.createdAt > (note.createdAt() ?: 0)) {
note.loadEvent(event, author, replyTo)
author.addNote(note)
refreshObservers(note)
}
}
@@ -310,9 +305,6 @@ object LocalCache {
// Log.d("TN", "New Note (${notes.size},${users.size}) ${note.author?.toBestDisplayName()} ${note.event?.content()?.split("\n")?.take(100)} ${formattedDateTime(event.createdAt)}")
// Prepares user's profile view.
author.addNote(note)
// Counts the replies
replyTo.forEach {
it.addReply(note)
@@ -518,7 +510,7 @@ object LocalCache {
if (event.createdAt > (note.createdAt() ?: 0)) {
note.loadEvent(event, author, replyTo)
author.updateAcceptedBadges(note)
refreshObservers(note)
}
}
@@ -634,8 +626,6 @@ object LocalCache {
event.deleteEvents().mapNotNull { notes[it] }.forEach { deleteNote ->
// must be the same author
if (deleteNote.author?.pubkeyHex == event.pubKey) {
deleteNote.author?.removeNote(deleteNote)
// reverts the add
val mentions = deleteNote.event?.tags()?.filter { it.firstOrNull() == "p" }
?.mapNotNull { it.getOrNull(1) }?.mapNotNull { checkGetOrCreateUser(it) }
@@ -697,9 +687,6 @@ object LocalCache {
note.loadEvent(event, author, repliesTo)
// Prepares user's profile view.
author.addNote(note)
// Counts the replies
repliesTo.forEach {
it.addBoost(note)
@@ -722,9 +709,6 @@ object LocalCache {
note.loadEvent(event, author, repliesTo)
// Prepares user's profile view.
author.addNote(note)
// Counts the replies
repliesTo.forEach {
it.addBoost(note)
@@ -750,9 +734,6 @@ object LocalCache {
note.loadEvent(event, author, eventsApproved)
// Prepares user's profile view.
author.addNote(note)
// Counts the replies
repliesTo.forEach {
it.addBoost(note)
@@ -1072,9 +1053,6 @@ object LocalCache {
note.loadEvent(event, author, emptyList())
// Adds to user profile
author.addNote(note)
refreshObservers(note)
}
@@ -1245,24 +1223,26 @@ object LocalCache {
}
}
println("PRUNE: ${toBeRemoved.size} messages removed from ${it.value.toBestDisplayName()}. ${it.value.notes.size} kept")
if (toBeRemoved.size > 100 || it.value.notes.size > 100) {
println("PRUNE: ${toBeRemoved.size} messages removed from ${it.value.toBestDisplayName()}. ${it.value.notes.size} kept")
}
}
}
fun pruneHiddenMessages(account: Account) {
checkNotInMainThread()
val toBeRemoved = account.hiddenUsers.map {
(users[it]?.notes ?: emptySet())
val toBeRemoved = account.hiddenUsers.map { userHex ->
(
notes.values.filter {
it.event?.pubKey() == userHex
} + addressables.values.filter {
it.event?.pubKey() == userHex
}
).toSet()
}.flatten()
account.hiddenUsers.forEach {
users[it]?.clearNotes()
}
toBeRemoved.forEach {
it.author?.removeNote(it)
// Counts the replies
it.replyTo?.forEach { masterNote ->
masterNote.removeReply(it)

View File

@@ -212,26 +212,58 @@ open class Note(val idHex: String) {
}
@Synchronized
private fun innerAddZap(zapRequest: Note, zap: Note?): Boolean {
if (zapRequest !in zaps.keys) {
zaps = zaps + Pair(zapRequest, zap)
return true
} else if (zaps[zapRequest] == null) {
zaps = zaps + Pair(zapRequest, zap)
return true
}
return false
}
fun addZap(zapRequest: Note, zap: Note?) {
checkNotInMainThread()
if (zapRequest !in zaps.keys) {
zaps = zaps + Pair(zapRequest, zap)
liveSet?.zaps?.invalidateData()
} else if (zapRequest in zaps.keys && zaps[zapRequest] == null) {
zaps = zaps + Pair(zapRequest, zap)
liveSet?.zaps?.invalidateData()
val inserted = innerAddZap(zapRequest, zap)
if (inserted) {
liveSet?.zaps?.invalidateData()
}
} else if (zaps[zapRequest] == null) {
val inserted = innerAddZap(zapRequest, zap)
if (inserted) {
liveSet?.zaps?.invalidateData()
}
}
}
@Synchronized
private fun innerAddZapPayment(zapPaymentRequest: Note, zapPayment: Note?): Boolean {
if (zapPaymentRequest !in zapPayments.keys) {
zapPayments = zapPayments + Pair(zapPaymentRequest, zapPayment)
return true
} else if (zapPayments[zapPaymentRequest] == null) {
zapPayments = zapPayments + Pair(zapPaymentRequest, zapPayment)
return true
}
return false
}
fun addZapPayment(zapPaymentRequest: Note, zapPayment: Note?) {
checkNotInMainThread()
if (zapPaymentRequest !in zapPayments.keys) {
zapPayments = zapPayments + Pair(zapPaymentRequest, zapPayment)
liveSet?.zaps?.invalidateData()
} else if (zapPaymentRequest in zapPayments.keys && zapPayments[zapPaymentRequest] == null) {
zapPayments = zapPayments + Pair(zapPaymentRequest, zapPayment)
liveSet?.zaps?.invalidateData()
val inserted = innerAddZapPayment(zapPaymentRequest, zapPayment)
if (inserted) {
liveSet?.zaps?.invalidateData()
}
} else if (zapPayments[zapPaymentRequest] == null) {
val inserted = innerAddZapPayment(zapPaymentRequest, zapPayment)
if (inserted) {
liveSet?.zaps?.invalidateData()
}
}
}

View File

@@ -31,10 +31,6 @@ class User(val pubkeyHex: String) {
var latestContactList: ContactListEvent? = null
var latestBookmarkList: BookmarkListEvent? = null
var latestAcceptedBadges: AddressableNote? = null
var notes = setOf<Note>()
private set
var reports = mapOf<User, Set<Note>>()
private set
@@ -106,21 +102,6 @@ class User(val pubkeyHex: String) {
liveSet?.relays?.invalidateData()
}
fun addNote(note: Note) {
if (note !in notes) {
notes = notes + note
// No need for Listener yet
}
}
fun removeNote(note: Note) {
notes = notes - note
}
fun clearNotes() {
notes = setOf<Note>()
}
fun addReport(note: Note) {
val author = note.author ?: return
@@ -144,13 +125,6 @@ class User(val pubkeyHex: String) {
}
}
fun updateAcceptedBadges(note: AddressableNote) {
if (latestAcceptedBadges?.idHex != note.idHex) {
latestAcceptedBadges = note
liveSet?.badges?.invalidateData()
}
}
fun addZap(zapRequest: Note, zap: Note?) {
if (zapRequest !in zaps.keys) {
zaps = zaps + Pair(zapRequest, zap)
@@ -351,7 +325,6 @@ class UserLiveSet(u: User) {
val relayInfo: UserLiveData = UserLiveData(u)
val metadata: UserLiveData = UserLiveData(u)
val zaps: UserLiveData = UserLiveData(u)
val badges: UserLiveData = UserLiveData(u)
val bookmarks: UserLiveData = UserLiveData(u)
fun isInUse(): Boolean {
@@ -363,7 +336,6 @@ class UserLiveSet(u: User) {
relayInfo.hasObservers() ||
metadata.hasObservers() ||
zaps.hasObservers() ||
badges.hasObservers() ||
bookmarks.hasObservers()
}
}

View File

@@ -17,7 +17,6 @@ object NostrDiscoveryDataSource : NostrDataSource("DiscoveryFeed") {
lateinit var account: Account
val latestEOSEs = EOSEAccount()
val dataSource = "Discovery"
fun createLiveStreamFilter(): TypedFilter {
val follows = account.selectedUsersFollowList(account.defaultDiscoveryFollowList)
@@ -32,7 +31,7 @@ object NostrDiscoveryDataSource : NostrDataSource("DiscoveryFeed") {
authors = followKeys,
kinds = listOf(LiveActivitiesChatMessageEvent.kind, LiveActivitiesEvent.kind),
limit = 300,
since = latestEOSEs.users[account.userProfile()]?.followList?.get(dataSource)?.relayList
since = latestEOSEs.users[account.userProfile()]?.followList?.get(account.defaultDiscoveryFollowList)?.relayList
)
)
}
@@ -50,7 +49,7 @@ object NostrDiscoveryDataSource : NostrDataSource("DiscoveryFeed") {
authors = followKeys,
kinds = listOf(ChannelCreateEvent.kind, ChannelMetadataEvent.kind, ChannelMessageEvent.kind),
limit = 300,
since = latestEOSEs.users[account.userProfile()]?.followList?.get(dataSource)?.relayList
since = latestEOSEs.users[account.userProfile()]?.followList?.get(account.defaultDiscoveryFollowList)?.relayList
)
)
}
@@ -68,7 +67,7 @@ object NostrDiscoveryDataSource : NostrDataSource("DiscoveryFeed") {
authors = followKeys,
kinds = listOf(CommunityDefinitionEvent.kind, CommunityPostApprovalEvent.kind),
limit = 300,
since = latestEOSEs.users[account.userProfile()]?.followList?.get(dataSource)?.relayList
since = latestEOSEs.users[account.userProfile()]?.followList?.get(account.defaultDiscoveryFollowList)?.relayList
)
)
}
@@ -88,7 +87,7 @@ object NostrDiscoveryDataSource : NostrDataSource("DiscoveryFeed") {
}.flatten()
),
limit = 300,
since = latestEOSEs.users[account.userProfile()]?.followList?.get(dataSource)?.relayList
since = latestEOSEs.users[account.userProfile()]?.followList?.get(account.defaultDiscoveryFollowList)?.relayList
)
)
}
@@ -108,7 +107,7 @@ object NostrDiscoveryDataSource : NostrDataSource("DiscoveryFeed") {
}.flatten()
),
limit = 300,
since = latestEOSEs.users[account.userProfile()]?.followList?.get(dataSource)?.relayList
since = latestEOSEs.users[account.userProfile()]?.followList?.get(account.defaultDiscoveryFollowList)?.relayList
)
)
}
@@ -128,12 +127,14 @@ object NostrDiscoveryDataSource : NostrDataSource("DiscoveryFeed") {
}.flatten()
),
limit = 300,
since = latestEOSEs.users[account.userProfile()]?.followList?.get(dataSource)?.relayList
since = latestEOSEs.users[account.userProfile()]?.followList?.get(account.defaultDiscoveryFollowList)?.relayList
)
)
}
val discoveryFeedChannel = requestNewChannel()
val discoveryFeedChannel = requestNewChannel() { time, relayUrl ->
latestEOSEs.addOrUpdate(account.userProfile(), account.defaultDiscoveryFollowList, relayUrl, time)
}
override fun updateChannelFilters() {
discoveryFeedChannel.typedFilters = listOfNotNull(

View File

@@ -3,6 +3,7 @@ package com.vitorpamplona.amethyst.service
import com.vitorpamplona.amethyst.model.Account
import com.vitorpamplona.amethyst.service.model.FileHeaderEvent
import com.vitorpamplona.amethyst.service.model.FileStorageHeaderEvent
import com.vitorpamplona.amethyst.service.relays.EOSEAccount
import com.vitorpamplona.amethyst.service.relays.FeedType
import com.vitorpamplona.amethyst.service.relays.JsonFilter
import com.vitorpamplona.amethyst.service.relays.TypedFilter
@@ -10,6 +11,8 @@ import com.vitorpamplona.amethyst.service.relays.TypedFilter
object NostrVideoDataSource : NostrDataSource("VideoFeed") {
lateinit var account: Account
val latestEOSEs = EOSEAccount()
fun createContextualFilter(): TypedFilter? {
val follows = account.selectedUsersFollowList(account.defaultStoriesFollowList)
@@ -22,7 +25,8 @@ object NostrVideoDataSource : NostrDataSource("VideoFeed") {
filter = JsonFilter(
authors = followKeys,
kinds = listOf(FileHeaderEvent.kind, FileStorageHeaderEvent.kind),
limit = 200
limit = 200,
since = latestEOSEs.users[account.userProfile()]?.followList?.get(account.defaultStoriesFollowList)?.relayList
)
)
}
@@ -41,12 +45,15 @@ object NostrVideoDataSource : NostrDataSource("VideoFeed") {
listOf(it, it.lowercase(), it.uppercase(), it.capitalize())
}.flatten()
),
limit = 100
limit = 100,
since = latestEOSEs.users[account.userProfile()]?.followList?.get(account.defaultStoriesFollowList)?.relayList
)
)
}
val videoFeedChannel = requestNewChannel()
val videoFeedChannel = requestNewChannel() { time, relayUrl ->
latestEOSEs.addOrUpdate(account.userProfile(), account.defaultStoriesFollowList, relayUrl, time)
}
override fun updateChannelFilters() {
videoFeedChannel.typedFilters = listOfNotNull(createContextualFilter(), createFollowTagsFilter()).ifEmpty { null }

View File

@@ -11,7 +11,12 @@ object ConnectivityStatus {
val isOnWifi: MutableState<Boolean> = onWifi
fun updateConnectivityStatus(isOnMobileData: Boolean, isOnWifi: Boolean) {
onMobileData.value = isOnMobileData
onWifi.value = isOnWifi
if (onMobileData.value != isOnMobileData) {
onMobileData.value = isOnMobileData
}
if (onWifi.value != isOnWifi) {
onWifi.value = isOnWifi
}
}
}

View File

@@ -25,5 +25,6 @@ class BadgeProfilesEvent(
companion object {
const val kind = 30008
const val standardDTAg = "profile_badges"
}
}

View File

@@ -20,6 +20,8 @@ class HighlightEvent(
fun author() = taggedUsers().firstOrNull()
fun quote() = content
fun inPost() = taggedAddresses().firstOrNull()
companion object {
const val kind = 9802

View File

@@ -147,8 +147,8 @@ class MetadataEvent(
UserMetadata::class.java
)
} catch (e: Exception) {
e.printStackTrace()
Log.w("MT", "Content Parse Error ${e.localizedMessage} $content")
// e.printStackTrace()
Log.w("MT", "Content Parse Error: ${e.localizedMessage} $content")
null
}

View File

@@ -139,14 +139,18 @@ class MainActivity : AppCompatActivity() {
networkCapabilities: NetworkCapabilities
) {
super.onCapabilitiesChanged(network, networkCapabilities)
val hasMobileData = networkCapabilities.hasTransport(NetworkCapabilities.TRANSPORT_CELLULAR)
val hasWifi = networkCapabilities.hasTransport(NetworkCapabilities.TRANSPORT_WIFI)
Log.d("NETWORKCALLBACK", "onCapabilitiesChanged: hasMobileData $hasMobileData")
Log.d("NETWORKCALLBACK", "onCapabilitiesChanged: hasWifi $hasWifi")
ConnectivityStatus.updateConnectivityStatus(
networkCapabilities.hasTransport(NetworkCapabilities.TRANSPORT_CELLULAR),
networkCapabilities.hasTransport(NetworkCapabilities.TRANSPORT_WIFI)
)
GlobalScope.launch(Dispatchers.IO) {
val hasMobileData =
networkCapabilities.hasTransport(NetworkCapabilities.TRANSPORT_CELLULAR)
val hasWifi = networkCapabilities.hasTransport(NetworkCapabilities.TRANSPORT_WIFI)
Log.d("NETWORKCALLBACK", "onCapabilitiesChanged: hasMobileData $hasMobileData")
Log.d("NETWORKCALLBACK", "onCapabilitiesChanged: hasWifi $hasWifi")
ConnectivityStatus.updateConnectivityStatus(
networkCapabilities.hasTransport(NetworkCapabilities.TRANSPORT_CELLULAR),
networkCapabilities.hasTransport(NetworkCapabilities.TRANSPORT_WIFI)
)
}
}
// lost network connection

View File

@@ -129,7 +129,8 @@ fun isNIP94Server(selectedServer: ServersAvailable?): Boolean {
return selectedServer == ServersAvailable.NOSTRIMG_NIP_94 ||
// selectedServer == ServersAvailable.IMGUR_NIP_94 ||
selectedServer == ServersAvailable.NOSTR_BUILD_NIP_94 ||
selectedServer == ServersAvailable.NOSTRFILES_DEV_NIP_94
selectedServer == ServersAvailable.NOSTRFILES_DEV_NIP_94 ||
selectedServer == ServersAvailable.NOSTRCHECK_ME_NIP_94
}
@Composable
@@ -139,6 +140,7 @@ fun ImageVideoPost(postViewModel: NewMediaModel, accountViewModel: AccountViewMo
Triple(ServersAvailable.NOSTRIMG_NIP_94, stringResource(id = R.string.upload_server_nostrimg_nip94), stringResource(id = R.string.upload_server_nostrimg_nip94_explainer)),
Triple(ServersAvailable.NOSTR_BUILD_NIP_94, stringResource(id = R.string.upload_server_nostrbuild_nip94), stringResource(id = R.string.upload_server_nostrbuild_nip94_explainer)),
Triple(ServersAvailable.NOSTRFILES_DEV_NIP_94, stringResource(id = R.string.upload_server_nostrfilesdev_nip94), stringResource(id = R.string.upload_server_nostrfilesdev_nip94_explainer)),
Triple(ServersAvailable.NOSTRCHECK_ME_NIP_94, stringResource(id = R.string.upload_server_nostrcheckme_nip94), stringResource(id = R.string.upload_server_nostrcheckme_nip94_explainer)),
Triple(ServersAvailable.NIP95, stringResource(id = R.string.upload_server_relays_nip95), stringResource(id = R.string.upload_server_relays_nip95_explainer))
)

View File

@@ -0,0 +1,157 @@
package com.vitorpamplona.amethyst.ui.buttons
import android.Manifest
import android.net.Uri
import android.os.Build
import androidx.compose.animation.core.animateFloatAsState
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.material.ButtonDefaults
import androidx.compose.material.CircularProgressIndicator
import androidx.compose.material.Icon
import androidx.compose.material.MaterialTheme
import androidx.compose.material.OutlinedButton
import androidx.compose.material.ProgressIndicatorDefaults
import androidx.compose.material.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.lifecycle.viewmodel.compose.viewModel
import com.google.accompanist.permissions.ExperimentalPermissionsApi
import com.google.accompanist.permissions.isGranted
import com.google.accompanist.permissions.rememberPermissionState
import com.vitorpamplona.amethyst.R
import com.vitorpamplona.amethyst.ui.actions.GallerySelect
import com.vitorpamplona.amethyst.ui.actions.NewMediaModel
import com.vitorpamplona.amethyst.ui.actions.NewMediaView
import com.vitorpamplona.amethyst.ui.navigation.Route
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
@OptIn(ExperimentalPermissionsApi::class)
@Composable
fun NewImageButton(accountViewModel: AccountViewModel, nav: (String) -> Unit) {
var wantsToPost by remember {
mutableStateOf(false)
}
var pickedURI by remember {
mutableStateOf<Uri?>(null)
}
val scope = rememberCoroutineScope()
val postViewModel: NewMediaModel = viewModel()
postViewModel.onceUploaded {
scope.launch(Dispatchers.Default) {
// awaits an refresh on the list
delay(250)
withContext(Dispatchers.Main) {
val route = Route.Video.route.replace("{scrollToTop}", "true")
nav(route)
}
}
}
if (wantsToPost) {
val cameraPermissionState =
rememberPermissionState(
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
Manifest.permission.READ_MEDIA_IMAGES
} else {
Manifest.permission.READ_EXTERNAL_STORAGE
}
)
if (cameraPermissionState.status.isGranted) {
var showGallerySelect by remember { mutableStateOf(false) }
if (showGallerySelect) {
GallerySelect(
onImageUri = { uri ->
wantsToPost = false
showGallerySelect = false
pickedURI = uri
}
)
}
showGallerySelect = true
} else {
LaunchedEffect(key1 = accountViewModel) {
cameraPermissionState.launchPermissionRequest()
}
}
}
pickedURI?.let {
NewMediaView(
uri = it,
onClose = { pickedURI = null },
postViewModel = postViewModel,
accountViewModel = accountViewModel,
nav = nav
)
}
if (postViewModel.isUploadingImage) {
ShowProgress(postViewModel)
} else {
OutlinedButton(
onClick = { wantsToPost = true },
modifier = Modifier.size(55.dp),
shape = CircleShape,
colors = ButtonDefaults.outlinedButtonColors(backgroundColor = MaterialTheme.colors.primary),
contentPadding = PaddingValues(0.dp)
) {
Icon(
painter = painterResource(R.drawable.ic_compose),
null,
modifier = Modifier.size(26.dp),
tint = Color.White
)
}
}
}
@Composable
private fun ShowProgress(postViewModel: NewMediaModel) {
Box(Modifier.size(55.dp), contentAlignment = Alignment.Center) {
CircularProgressIndicator(
progress = animateFloatAsState(
targetValue = postViewModel.uploadingPercentage.value,
animationSpec = ProgressIndicatorDefaults.ProgressAnimationSpec
).value,
modifier = Modifier
.size(55.dp)
.clip(CircleShape)
.background(MaterialTheme.colors.background),
strokeWidth = 5.dp
)
postViewModel.uploadingDescription.value?.let {
Text(
it,
color = MaterialTheme.colors.onSurface,
fontSize = 10.sp,
textAlign = TextAlign.Center
)
}
}
}

View File

@@ -583,14 +583,22 @@ fun ClickableInLineIconRenderer(
style: SpanStyle,
onClick: (Int) -> Unit
) {
val placeholderSize = remember(style) {
if (style.fontSize == TextUnit.Unspecified) {
22.sp
} else {
style.fontSize.times(1.1f)
}
}
val inlineContent = wordsInOrder.mapIndexedNotNull { idx, value ->
if (value is ImageUrlType) {
Pair(
"inlineContent$idx",
InlineTextContent(
Placeholder(
width = 17.sp,
height = 17.sp,
width = placeholderSize,
height = placeholderSize,
placeholderVerticalAlign = PlaceholderVerticalAlign.Center
)
) {
@@ -651,14 +659,22 @@ fun InLineIconRenderer(
overflow: TextOverflow = TextOverflow.Clip,
modifier: Modifier = Modifier
) {
val placeholderSize = remember(fontSize) {
if (fontSize == TextUnit.Unspecified) {
22.sp
} else {
fontSize.times(1.1f)
}
}
val inlineContent = wordsInOrder.mapIndexedNotNull { idx, value ->
if (value is ImageUrlType) {
Pair(
"inlineContent$idx",
InlineTextContent(
Placeholder(
width = 20.sp,
height = 20.sp,
width = placeholderSize,
height = placeholderSize,
placeholderVerticalAlign = PlaceholderVerticalAlign.Center
)
) {

View File

@@ -5,21 +5,37 @@ import com.vitorpamplona.amethyst.model.Note
import com.vitorpamplona.amethyst.model.User
import com.vitorpamplona.amethyst.service.model.AppRecommendationEvent
class UserProfileAppRecommendationsFeedFilter(val user: User) : FeedFilter<Note>() {
class UserProfileAppRecommendationsFeedFilter(val user: User) : AdditiveFeedFilter<Note>() {
override fun feedKey(): String {
return user.pubkeyHex
}
override fun feed(): List<Note> {
val recommendations = LocalCache.addressables.values.filter {
(it.event as? AppRecommendationEvent)?.pubKey == user.pubkeyHex
}.mapNotNull {
(it.event as? AppRecommendationEvent)?.recommendations()
}.flatten()
.map {
return sort(innerApplyFilter(LocalCache.addressables.values))
}
override fun applyFilter(collection: Set<Note>): Set<Note> {
return innerApplyFilter(collection)
}
private fun innerApplyFilter(collection: Collection<Note>): Set<Note> {
val recommendations = collection.asSequence()
.filter { it.event is AppRecommendationEvent }
.mapNotNull {
val noteEvent = it.event as? AppRecommendationEvent
if (noteEvent != null && noteEvent.pubKey == user.pubkeyHex) {
noteEvent.recommendations()
} else {
null
}
}.flatten().map {
LocalCache.getOrCreateAddressableNote(it)
}.toSet().toList()
}.toSet()
return recommendations
}
override fun sort(collection: Set<Note>): List<Note> {
return collection.sortedWith(compareBy({ it.createdAt() }, { it.idHex }))
}
}

View File

@@ -1,18 +1,45 @@
package com.vitorpamplona.amethyst.ui.dal
import com.vitorpamplona.amethyst.model.Account
import com.vitorpamplona.amethyst.model.LocalCache
import com.vitorpamplona.amethyst.model.Note
import com.vitorpamplona.amethyst.model.User
import com.vitorpamplona.amethyst.service.model.ChannelMessageEvent
import com.vitorpamplona.amethyst.service.model.LiveActivitiesChatMessageEvent
import com.vitorpamplona.amethyst.service.model.PollNoteEvent
import com.vitorpamplona.amethyst.service.model.TextNoteEvent
class UserProfileConversationsFeedFilter(val user: User, val account: Account) : FeedFilter<Note>() {
class UserProfileConversationsFeedFilter(val user: User, val account: Account) : AdditiveFeedFilter<Note>() {
override fun feedKey(): String {
return account.userProfile().pubkeyHex + "-" + user.pubkeyHex
}
override fun feed(): List<Note> {
return user.notes
.filter { account.isAcceptable(it) == true && !it.isNewThread() }
.sortedWith(compareBy({ it.createdAt() }, { it.idHex }))
.reversed() ?: emptyList()
return sort(innerApplyFilter(LocalCache.notes.values))
}
override fun applyFilter(collection: Set<Note>): Set<Note> {
return innerApplyFilter(collection)
}
private fun innerApplyFilter(collection: Collection<Note>): Set<Note> {
return collection
.filter {
it.author == user &&
(
it.event is TextNoteEvent ||
it.event is PollNoteEvent ||
it.event is ChannelMessageEvent ||
it.event is LiveActivitiesChatMessageEvent
) &&
!it.isNewThread() &&
account.isAcceptable(it) == true
}.toSet()
}
override fun sort(collection: Set<Note>): List<Note> {
return collection.sortedWith(compareBy({ it.createdAt() }, { it.idHex })).reversed()
}
override fun limit() = 200
}

View File

@@ -12,4 +12,6 @@ class UserProfileFollowersFeedFilter(val user: User, val account: Account) : Fee
override fun feed(): List<User> {
return LocalCache.users.values.filter { it.isFollowing(user) && !account.isHidden(it) }
}
override fun limit() = 400
}

View File

@@ -8,7 +8,7 @@ import com.vitorpamplona.amethyst.service.model.ContactListEvent
class UserProfileFollowsFeedFilter(val user: User, val account: Account) : FeedFilter<User>() {
override fun feedKey(): String {
return account.userProfile()?.pubkeyHex + "-" + user.pubkeyHex
return account.userProfile().pubkeyHex + "-" + user.pubkeyHex
}
val cache: MutableMap<ContactListEvent, List<User>> = mutableMapOf()

View File

@@ -4,32 +4,53 @@ import com.vitorpamplona.amethyst.model.Account
import com.vitorpamplona.amethyst.model.LocalCache
import com.vitorpamplona.amethyst.model.Note
import com.vitorpamplona.amethyst.model.User
import com.vitorpamplona.amethyst.service.model.AppRecommendationEvent
import com.vitorpamplona.amethyst.service.model.BookmarkListEvent
import com.vitorpamplona.amethyst.service.model.EmojiPackSelectionEvent
import com.vitorpamplona.amethyst.service.model.PeopleListEvent
import com.vitorpamplona.amethyst.service.model.AudioTrackEvent
import com.vitorpamplona.amethyst.service.model.ClassifiedsEvent
import com.vitorpamplona.amethyst.service.model.GenericRepostEvent
import com.vitorpamplona.amethyst.service.model.HighlightEvent
import com.vitorpamplona.amethyst.service.model.LongTextNoteEvent
import com.vitorpamplona.amethyst.service.model.PollNoteEvent
import com.vitorpamplona.amethyst.service.model.RepostEvent
import com.vitorpamplona.amethyst.service.model.TextNoteEvent
class UserProfileNewThreadFeedFilter(val user: User, val account: Account) : FeedFilter<Note>() {
class UserProfileNewThreadFeedFilter(val user: User, val account: Account) : AdditiveFeedFilter<Note>() {
override fun feedKey(): String {
return account.userProfile().pubkeyHex + "-" + user.pubkeyHex
}
override fun feed(): List<Note> {
val longFormNotes = LocalCache.addressables.values
val notes = innerApplyFilter(LocalCache.notes.values)
val longFormNotes = innerApplyFilter(LocalCache.addressables.values)
return sort(notes + longFormNotes)
}
override fun applyFilter(collection: Set<Note>): Set<Note> {
return innerApplyFilter(collection)
}
private fun innerApplyFilter(collection: Collection<Note>): Set<Note> {
return collection
.filter {
it.author == user &&
(
it.event !is PeopleListEvent &&
it.event !is BookmarkListEvent &&
it.event !is AppRecommendationEvent &&
it.event !is EmojiPackSelectionEvent
)
}
return user.notes
.plus(longFormNotes)
.filter { account.isAcceptable(it) == true && it.isNewThread() }
.sortedWith(compareBy({ it.createdAt() }, { it.idHex }))
.reversed() ?: emptyList()
it.event is TextNoteEvent ||
it.event is ClassifiedsEvent ||
it.event is RepostEvent ||
it.event is GenericRepostEvent ||
it.event is LongTextNoteEvent ||
it.event is PollNoteEvent ||
it.event is HighlightEvent ||
it.event is AudioTrackEvent
) &&
it.isNewThread() &&
account.isAcceptable(it) == true
}.toSet()
}
override fun sort(collection: Set<Note>): List<Note> {
return collection.sortedWith(compareBy({ it.createdAt() }, { it.idHex })).reversed()
}
override fun limit() = 200
}

View File

@@ -1,20 +1,29 @@
package com.vitorpamplona.amethyst.ui.dal
import com.vitorpamplona.amethyst.model.LocalCache
import com.vitorpamplona.amethyst.model.Note
import com.vitorpamplona.amethyst.model.User
import com.vitorpamplona.amethyst.service.model.ReportEvent
class UserProfileReportsFeedFilter(val user: User) : FeedFilter<Note>() {
class UserProfileReportsFeedFilter(val user: User) : AdditiveFeedFilter<Note>() {
override fun feedKey(): String {
return user.pubkeyHex ?: ""
return user.pubkeyHex
}
override fun feed(): List<Note> {
val reportNotes = LocalCache.notes.values.filter { (it.event as? ReportEvent)?.isTaggedUser(user.pubkeyHex) == true }
return reportNotes
.sortedWith(compareBy({ it.createdAt() }, { it.idHex }))
.reversed()
return sort(innerApplyFilter(user.reports.values.flatten()))
}
override fun applyFilter(collection: Set<Note>): Set<Note> {
return innerApplyFilter(collection)
}
private fun innerApplyFilter(collection: Collection<Note>): Set<Note> {
return collection.filter { it.event is ReportEvent && it.event?.isTaggedUser(user.pubkeyHex) == true }.toSet()
}
override fun sort(collection: Set<Note>): List<Note> {
return collection.sortedWith(compareBy({ it.createdAt() }, { it.idHex })).reversed()
}
override fun limit() = 400
}

View File

@@ -13,4 +13,6 @@ class UserProfileZapsFeedFilter(val user: User) : FeedFilter<ZapReqResponse>() {
override fun feed(): List<ZapReqResponse> {
return UserZaps.forProfileFeed(user.zaps)
}
override fun limit() = 400
}

View File

@@ -49,7 +49,11 @@ fun BadgeCompose(likeSetCard: BadgeCard, isInnerNote: Boolean = false, routeForL
val context = LocalContext.current.applicationContext
var popupExpanded by remember { mutableStateOf(false) }
val popupExpanded = remember { mutableStateOf(false) }
val enablePopup = remember {
{ popupExpanded.value = true }
}
val scope = rememberCoroutineScope()
if (note == null) {
@@ -89,7 +93,7 @@ fun BadgeCompose(likeSetCard: BadgeCard, isInnerNote: Boolean = false, routeForL
)?.let { nav(it) }
}
},
onLongClick = { popupExpanded = true }
onLongClick = enablePopup
)
) {
Row(
@@ -136,7 +140,7 @@ fun BadgeCompose(likeSetCard: BadgeCard, isInnerNote: Boolean = false, routeForL
IconButton(
modifier = Modifier.then(Modifier.size(24.dp)),
onClick = { popupExpanded = true }
onClick = enablePopup
) {
Icon(
imageVector = Icons.Default.MoreVert,
@@ -145,7 +149,7 @@ fun BadgeCompose(likeSetCard: BadgeCard, isInnerNote: Boolean = false, routeForL
tint = MaterialTheme.colors.placeholderText
)
NoteDropDownMenu(note, popupExpanded, { popupExpanded = false }, accountViewModel)
NoteDropDownMenu(note, popupExpanded, accountViewModel)
}
}

View File

@@ -492,6 +492,7 @@ fun RenderLiveActivityThumb(
showVideo = false,
showBottomDiviser = false,
showFlag = false,
sendToChannel = true,
modifier = remember {
Modifier.padding(start = 0.dp, end = 0.dp, top = 5.dp, bottom = 5.dp)
},

View File

@@ -41,7 +41,10 @@ fun MessageSetCompose(messageSetCard: MessageSetCard, routeForLastRead: String,
val accountState by accountViewModel.accountLiveData.observeAsState()
val loggedIn = remember(accountState) { accountState?.account?.userProfile() } ?: return
var popupExpanded by remember { mutableStateOf(false) }
val popupExpanded = remember { mutableStateOf(false) }
val enablePopup = remember {
{ popupExpanded.value = true }
}
val scope = rememberCoroutineScope()
@@ -84,7 +87,7 @@ fun MessageSetCompose(messageSetCard: MessageSetCard, routeForLastRead: String,
)?.let { nav(it) }
}
},
onLongClick = { popupExpanded = true }
onLongClick = enablePopup
)
.fillMaxWidth()
}
@@ -104,7 +107,7 @@ fun MessageSetCompose(messageSetCard: MessageSetCard, routeForLastRead: String,
nav = nav
)
NoteDropDownMenu(baseNote, popupExpanded, { popupExpanded = false }, accountViewModel)
NoteDropDownMenu(baseNote, popupExpanded, accountViewModel)
}
}
}

View File

@@ -89,7 +89,11 @@ import kotlin.time.measureTimedValue
fun MultiSetCompose(multiSetCard: MultiSetCard, routeForLastRead: String, accountViewModel: AccountViewModel, nav: (String) -> Unit) {
val baseNote = remember { multiSetCard.note }
var popupExpanded by remember { mutableStateOf(false) }
val popupExpanded = remember { mutableStateOf(false) }
val enablePopup = remember {
{ popupExpanded.value = true }
}
val scope = rememberCoroutineScope()
val defaultBackgroundColor = MaterialTheme.colors.background
@@ -130,7 +134,7 @@ fun MultiSetCompose(multiSetCard: MultiSetCard, routeForLastRead: String, accoun
routeFor(baseNote, accountViewModel.userProfile())?.let { nav(it) }
}
},
onLongClick = { popupExpanded = true }
onLongClick = enablePopup
)
.fillMaxWidth()
}
@@ -157,7 +161,7 @@ fun MultiSetCompose(multiSetCard: MultiSetCard, routeForLastRead: String, accoun
}
Log.d("Rendering Metrics", "Complete: ${baseNote.event?.content()?.split("\n")?.getOrNull(0)?.take(15)}.. $elapsed")
NoteDropDownMenu(baseNote, popupExpanded, { popupExpanded = false }, accountViewModel)
NoteDropDownMenu(baseNote, popupExpanded, accountViewModel)
}
}
}

View File

@@ -183,7 +183,7 @@ private fun NIP05VerifiedSymbol(nip05Verified: MutableState<Boolean?>) {
} else if (nip05Verified.value == true) {
Icon(
painter = painterResource(R.drawable.ic_verified_transparent),
"NIP-05 Verified",
"Nostr Address Verified",
tint = Nip05.copy(0.52f),
modifier = Modifier
.size(14.dp)
@@ -193,7 +193,7 @@ private fun NIP05VerifiedSymbol(nip05Verified: MutableState<Boolean?>) {
Icon(
tint = Color.Red,
imageVector = Icons.Default.Report,
contentDescription = "Invalid Nip05",
contentDescription = "Invalid Nostr Address",
modifier = Modifier
.size(14.dp)
.padding(top = 1.dp)

View File

@@ -130,6 +130,7 @@ import com.vitorpamplona.amethyst.ui.components.ClickableUrl
import com.vitorpamplona.amethyst.ui.components.CreateClickableTextWithEmoji
import com.vitorpamplona.amethyst.ui.components.CreateTextWithEmoji
import com.vitorpamplona.amethyst.ui.components.LoadThumbAndThenVideoView
import com.vitorpamplona.amethyst.ui.components.MeasureSpaceWidth
import com.vitorpamplona.amethyst.ui.components.ObserveDisplayNip05Status
import com.vitorpamplona.amethyst.ui.components.RobohashAsyncImageProxy
import com.vitorpamplona.amethyst.ui.components.SensitivityWarning
@@ -433,12 +434,14 @@ fun NormalNote(
channelNote = baseNote,
showVideo = !makeItShort,
showBottomDiviser = true,
sendToChannel = true,
accountViewModel = accountViewModel,
nav = nav
)
is CommunityDefinitionEvent -> CommunityHeader(
baseNote = baseNote,
showBottomDiviser = true,
sendToCommunity = true,
accountViewModel = accountViewModel,
nav = nav
)
@@ -470,6 +473,7 @@ fun NormalNote(
fun CommunityHeader(
baseNote: Note,
showBottomDiviser: Boolean,
sendToCommunity: Boolean,
modifier: Modifier = StdPadding,
accountViewModel: AccountViewModel,
nav: (String) -> Unit
@@ -478,7 +482,13 @@ fun CommunityHeader(
Column(
modifier = modifier.clickable {
expanded.value = !expanded.value
if (sendToCommunity) {
routeFor(baseNote, accountViewModel.userProfile())?.let {
nav(it)
}
} else {
expanded.value = !expanded.value
}
}
) {
ShortCommunityHeader(baseNote, expanded, accountViewModel, nav)
@@ -1148,6 +1158,10 @@ fun routeFor(note: Note, loggedIn: User): String? {
return null
}
fun routeFor(note: Channel): String {
return "Channel/${note.idHex}"
}
fun routeFor(user: User): String {
return "User/${user.pubkeyHex}"
}
@@ -1441,11 +1455,15 @@ private fun RenderHighlight(
val url = remember() {
(note.event as? HighlightEvent)?.inUrl()
}
val postHex = remember() {
(note.event as? HighlightEvent)?.taggedAddresses()?.firstOrNull()
}
DisplayHighlight(
quote,
author,
url,
postHex,
makeItShort,
canPreview,
backgroundColor,
@@ -2298,6 +2316,7 @@ private fun ReplyRow(
channelHex = channelHex,
showVideo = false,
showBottomDiviser = false,
sendToChannel = true,
modifier = remember { Modifier.padding(vertical = 5.dp) },
accountViewModel = accountViewModel,
nav = nav
@@ -2428,28 +2447,35 @@ fun MoreOptionsButton(
baseNote: Note,
accountViewModel: AccountViewModel
) {
var moreActionsExpanded by remember { mutableStateOf(false) }
val popupExpanded = remember { mutableStateOf(false) }
val enablePopup = remember {
{ popupExpanded.value = true }
}
IconButton(
modifier = Size24Modifier,
onClick = { moreActionsExpanded = true }
onClick = enablePopup
) {
Icon(
imageVector = Icons.Default.MoreVert,
null,
modifier = Size15Modifier,
tint = MaterialTheme.colors.placeholderText
)
VerticalDotsIcon()
NoteDropDownMenu(
baseNote,
moreActionsExpanded,
{ moreActionsExpanded = false },
popupExpanded,
accountViewModel
)
}
}
@Composable
private fun VerticalDotsIcon() {
Icon(
imageVector = Icons.Default.MoreVert,
null,
modifier = Size15Modifier,
tint = MaterialTheme.colors.placeholderText
)
}
@Composable
fun TimeAgo(note: Note) {
val time = remember(note) { note.createdAt() } ?: return
@@ -2613,6 +2639,7 @@ fun DisplayHighlight(
highlight: String,
authorHex: String?,
url: String?,
postAddress: ATag?,
makeItShort: Boolean,
canPreview: Boolean,
backgroundColor: MutableState<Color>,
@@ -2637,13 +2664,15 @@ fun DisplayHighlight(
nav
)
DisplayQuoteAuthor(authorHex ?: "", url, nav)
DisplayQuoteAuthor(authorHex ?: "", url, postAddress, accountViewModel, nav)
}
@Composable
private fun DisplayQuoteAuthor(
authorHex: String,
url: String?,
postAddress: ATag?,
accountViewModel: AccountViewModel,
nav: (String) -> Unit
) {
var userBase by remember { mutableStateOf<User?>(LocalCache.getUserIfExists(authorHex)) }
@@ -2659,13 +2688,47 @@ private fun DisplayQuoteAuthor(
}
}
Row {
userBase?.let { userBase ->
LoadAndDisplayUser(userBase, nav)
}
MeasureSpaceWidth {
Row(horizontalArrangement = Arrangement.spacedBy(it), verticalAlignment = Alignment.CenterVertically) {
userBase?.let { userBase ->
LoadAndDisplayUser(userBase, nav)
}
url?.let { url ->
LoadAndDisplayUrl(url)
url?.let { url ->
LoadAndDisplayUrl(url)
}
postAddress?.let { address ->
LoadAndDisplayPost(address, accountViewModel, nav)
}
}
}
}
@Composable
private fun LoadAndDisplayPost(postAddress: ATag, accountViewModel: AccountViewModel, nav: (String) -> Unit) {
LoadAddressableNote(aTag = postAddress) {
it?.let { note ->
val noteEvent by note.live().metadata.map {
it.note.event
}.distinctUntilChanged().observeAsState(note.event)
val title = remember(noteEvent) {
(noteEvent as? LongTextNoteEvent)?.title()
}
title?.let {
Text(remember { "-" }, maxLines = 1)
ClickableText(
text = AnnotatedString(title),
onClick = {
routeFor(note, accountViewModel.userProfile())?.let {
nav(it)
}
},
style = LocalTextStyle.current.copy(color = MaterialTheme.colors.primary)
)
}
}
}
}
@@ -2682,7 +2745,7 @@ private fun LoadAndDisplayUrl(url: String) {
}
validatedUrl?.host?.let { host ->
Text(remember { "on " }, maxLines = 1)
Text(remember { "-" }, maxLines = 1)
ClickableUrl(urlText = host, url = url)
}
}

View File

@@ -20,6 +20,7 @@ import androidx.compose.material.ripple.rememberRipple
import androidx.compose.runtime.Composable
import androidx.compose.runtime.Immutable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.MutableState
import androidx.compose.runtime.derivedStateOf
import androidx.compose.runtime.getValue
import androidx.compose.runtime.livedata.observeAsState
@@ -310,7 +311,7 @@ data class DropDownParams(
)
@Composable
fun NoteDropDownMenu(note: Note, popupExpanded: Boolean, onDismiss: () -> Unit, accountViewModel: AccountViewModel) {
fun NoteDropDownMenu(note: Note, popupExpanded: MutableState<Boolean>, accountViewModel: AccountViewModel) {
var reportDialogShowing by remember { mutableStateOf(false) }
var state by remember {
@@ -326,8 +327,12 @@ fun NoteDropDownMenu(note: Note, popupExpanded: Boolean, onDismiss: () -> Unit,
)
}
val onDismiss = remember(popupExpanded) {
{ popupExpanded.value = false }
}
DropdownMenu(
expanded = popupExpanded,
expanded = popupExpanded.value,
onDismissRequest = onDismiss
) {
val clipboardManager = LocalClipboardManager.current

View File

@@ -240,7 +240,10 @@ fun NoteMaster(
val context = LocalContext.current
var moreActionsExpanded by remember { mutableStateOf(false) }
val moreActionsExpanded = remember { mutableStateOf(false) }
val enablePopup = remember {
{ moreActionsExpanded.value = true }
}
val noteEvent = note?.event
@@ -310,7 +313,7 @@ fun NoteMaster(
IconButton(
modifier = Modifier.then(Modifier.size(24.dp)),
onClick = { moreActionsExpanded = true }
onClick = enablePopup
) {
Icon(
imageVector = Icons.Default.MoreVert,
@@ -319,7 +322,7 @@ fun NoteMaster(
tint = MaterialTheme.colors.placeholderText
)
NoteDropDownMenu(baseNote, moreActionsExpanded, { moreActionsExpanded = false }, accountViewModel)
NoteDropDownMenu(baseNote, moreActionsExpanded, accountViewModel)
}
}
@@ -363,6 +366,7 @@ fun NoteMaster(
channelHex = note.channelHex()!!,
showVideo = true,
showBottomDiviser = false,
sendToChannel = true,
accountViewModel = accountViewModel,
nav = nav
)
@@ -411,6 +415,7 @@ fun NoteMaster(
noteEvent.quote(),
noteEvent.author(),
noteEvent.inUrl(),
noteEvent.inPost(),
false,
true,
backgroundColor,

View File

@@ -107,6 +107,7 @@ import com.vitorpamplona.amethyst.ui.note.ChatroomMessageCompose
import com.vitorpamplona.amethyst.ui.note.ClickableUserPicture
import com.vitorpamplona.amethyst.ui.note.DisplayUncitedHashtags
import com.vitorpamplona.amethyst.ui.note.LikeReaction
import com.vitorpamplona.amethyst.ui.note.LoadChannel
import com.vitorpamplona.amethyst.ui.note.MoreOptionsButton
import com.vitorpamplona.amethyst.ui.note.NoteAuthorPicture
import com.vitorpamplona.amethyst.ui.note.NoteUsernameDisplay
@@ -114,6 +115,7 @@ import com.vitorpamplona.amethyst.ui.note.TimeAgo
import com.vitorpamplona.amethyst.ui.note.UserPicture
import com.vitorpamplona.amethyst.ui.note.UsernameDisplay
import com.vitorpamplona.amethyst.ui.note.ZapReaction
import com.vitorpamplona.amethyst.ui.note.routeFor
import com.vitorpamplona.amethyst.ui.note.timeAgo
import com.vitorpamplona.amethyst.ui.screen.NostrChannelFeedViewModel
import com.vitorpamplona.amethyst.ui.screen.RefreshingChatroomFeedView
@@ -143,18 +145,7 @@ fun ChannelScreen(
) {
if (channelId == null) return
var channelBase by remember { mutableStateOf<Channel?>(LocalCache.getChannelIfExists(channelId)) }
LaunchedEffect(channelId) {
withContext(Dispatchers.IO) {
val newChannelBase = LocalCache.checkGetOrCreateChannel(channelId)
if (newChannelBase != channelBase) {
channelBase = newChannelBase
}
}
}
channelBase?.let {
LoadChannel(channelId) {
PrepareChannelViewModels(
baseChannel = it,
accountViewModel = accountViewModel,
@@ -516,6 +507,7 @@ fun ChannelHeader(
channelNote: Note,
showVideo: Boolean,
showBottomDiviser: Boolean,
sendToChannel: Boolean,
modifier: Modifier = StdPadding,
accountViewModel: AccountViewModel,
nav: (String) -> Unit
@@ -530,6 +522,7 @@ fun ChannelHeader(
channelHex = it,
showVideo = showVideo,
showBottomDiviser = showBottomDiviser,
sendToChannel = sendToChannel,
accountViewModel = accountViewModel,
nav = nav
)
@@ -542,26 +535,18 @@ fun ChannelHeader(
showVideo: Boolean,
showBottomDiviser: Boolean,
showFlag: Boolean = true,
sendToChannel: Boolean = false,
modifier: Modifier = StdPadding,
accountViewModel: AccountViewModel,
nav: (String) -> Unit
) {
var baseChannel by remember { mutableStateOf(LocalCache.channels[channelHex]) }
if (baseChannel == null) {
LaunchedEffect(key1 = channelHex) {
launch(Dispatchers.IO) {
baseChannel = LocalCache.checkGetOrCreateChannel(channelHex)
}
}
}
baseChannel?.let {
LoadChannel(channelHex) {
ChannelHeader(
it,
showVideo,
showBottomDiviser,
showFlag,
sendToChannel,
modifier,
accountViewModel,
nav
@@ -575,6 +560,7 @@ fun ChannelHeader(
showVideo: Boolean,
showBottomDiviser: Boolean,
showFlag: Boolean = true,
sendToChannel: Boolean = false,
modifier: Modifier = StdPadding,
accountViewModel: AccountViewModel,
nav: (String) -> Unit
@@ -588,7 +574,11 @@ fun ChannelHeader(
Column(
modifier = modifier.clickable {
expanded.value = !expanded.value
if (sendToChannel) {
nav(routeFor(baseChannel))
} else {
expanded.value = !expanded.value
}
}
) {
ShortChannelHeader(baseChannel, expanded, accountViewModel, nav, showFlag)

View File

@@ -94,7 +94,13 @@ fun CommunityScreen(note: AddressableNote, feedViewModel: NostrCommunityFeedView
}
Column(Modifier.fillMaxSize()) {
CommunityHeader(baseNote = note, showBottomDiviser = true, accountViewModel = accountViewModel, nav = nav)
CommunityHeader(
baseNote = note,
showBottomDiviser = true,
sendToCommunity = false,
accountViewModel = accountViewModel,
nav = nav
)
RefresheableFeedView(
feedViewModel,
null,

View File

@@ -31,6 +31,7 @@ import androidx.navigation.compose.currentBackStackEntryAsState
import androidx.navigation.compose.rememberNavController
import com.vitorpamplona.amethyst.ui.buttons.ChannelFabColumn
import com.vitorpamplona.amethyst.ui.buttons.NewCommunityNoteButton
import com.vitorpamplona.amethyst.ui.buttons.NewImageButton
import com.vitorpamplona.amethyst.ui.buttons.NewNoteButton
import com.vitorpamplona.amethyst.ui.navigation.*
import com.vitorpamplona.amethyst.ui.navigation.AccountSwitchBottomSheet

View File

@@ -49,6 +49,8 @@ import androidx.compose.ui.unit.sp
import androidx.core.content.ContextCompat
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.LifecycleEventObserver
import androidx.lifecycle.distinctUntilChanged
import androidx.lifecycle.map
import androidx.lifecycle.viewmodel.compose.viewModel
import coil.compose.AsyncImage
import com.vitorpamplona.amethyst.R
@@ -57,6 +59,7 @@ import com.vitorpamplona.amethyst.model.LocalCache
import com.vitorpamplona.amethyst.model.Note
import com.vitorpamplona.amethyst.model.User
import com.vitorpamplona.amethyst.service.NostrUserProfileDataSource
import com.vitorpamplona.amethyst.service.model.ATag
import com.vitorpamplona.amethyst.service.model.AppDefinitionEvent
import com.vitorpamplona.amethyst.service.model.BadgeDefinitionEvent
import com.vitorpamplona.amethyst.service.model.BadgeProfilesEvent
@@ -78,6 +81,7 @@ import com.vitorpamplona.amethyst.ui.components.figureOutMimeType
import com.vitorpamplona.amethyst.ui.dal.UserProfileReportsFeedFilter
import com.vitorpamplona.amethyst.ui.navigation.ShowQRDialog
import com.vitorpamplona.amethyst.ui.note.ClickableUserPicture
import com.vitorpamplona.amethyst.ui.note.LoadAddressableNote
import com.vitorpamplona.amethyst.ui.screen.FeedState
import com.vitorpamplona.amethyst.ui.screen.LnZapFeedView
import com.vitorpamplona.amethyst.ui.screen.NostrUserAppRecommendationsFeedViewModel
@@ -97,6 +101,7 @@ import com.vitorpamplona.amethyst.ui.theme.BitcoinOrange
import com.vitorpamplona.amethyst.ui.theme.ButtonBorder
import com.vitorpamplona.amethyst.ui.theme.Size35dp
import com.vitorpamplona.amethyst.ui.theme.placeholderText
import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.toImmutableList
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
@@ -1110,31 +1115,42 @@ private fun WatchApp(baseApp: Note, nav: (String) -> Unit) {
}
@Composable
@OptIn(ExperimentalLayoutApi::class)
private fun DisplayBadges(
baseUser: User,
nav: (String) -> Unit
) {
val userBadgeState by baseUser.live().badges.observeAsState()
val badgeList by remember(userBadgeState) {
derivedStateOf {
val list = (userBadgeState?.user?.latestAcceptedBadges?.event as? BadgeProfilesEvent)?.badgeAwardEvents()
if (list.isNullOrEmpty()) {
null
} else {
list.toImmutableList()
LoadAddressableNote(
aTag = ATag(
BadgeProfilesEvent.kind,
baseUser.pubkeyHex,
BadgeProfilesEvent.standardDTAg,
null
)
) {
if (it != null) {
val badgeList by it.live().metadata.map {
(it.note.event as? BadgeProfilesEvent)?.badgeAwardEvents()?.toImmutableList()
}.distinctUntilChanged().observeAsState()
badgeList?.let { list ->
RenderBadgeList(list, nav)
}
}
}
}
badgeList?.let { list ->
FlowRow(
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier.padding(vertical = 5.dp)
) {
list.forEach { badgeAwardEvent ->
LoadAndRenderBadge(badgeAwardEvent, nav)
}
@Composable
@OptIn(ExperimentalLayoutApi::class)
private fun RenderBadgeList(
list: ImmutableList<String>,
nav: (String) -> Unit
) {
FlowRow(
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier.padding(vertical = 5.dp)
) {
list.forEach { badgeAwardEvent ->
LoadAndRenderBadge(badgeAwardEvent, nav)
}
}
}

View File

@@ -40,7 +40,9 @@ import com.vitorpamplona.amethyst.ui.theme.DoubleVertSpacer
import com.vitorpamplona.amethyst.ui.theme.StdPadding
import kotlinx.collections.immutable.persistentListOf
import kotlinx.collections.immutable.toImmutableList
import kotlinx.coroutines.DelicateCoroutinesApi
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.launch
import org.xmlpull.v1.XmlPullParser
import org.xmlpull.v1.XmlPullParserException
@@ -80,12 +82,19 @@ fun Context.getLangPreferenceDropdownEntries(): Map<String, String> {
}
fun getLanguageIndex(languageEntries: Map<String, String>): Int {
var languageIndex = languageEntries.values.toTypedArray().indexOf(Locale.current.toLanguageTag())
val language = LocalPreferences.getPreferredLanguage()
var languageIndex = -1
if (language.isNotBlank()) {
languageIndex = languageEntries.values.toTypedArray().indexOf(language)
} else {
languageIndex = languageEntries.values.toTypedArray().indexOf(Locale.current.toLanguageTag())
}
if (languageIndex == -1) languageIndex = languageEntries.values.toTypedArray().indexOf(Locale.current.language)
if (languageIndex == -1) languageIndex = languageEntries.values.toTypedArray().indexOf("en")
return languageIndex
}
@OptIn(DelicateCoroutinesApi::class)
@Composable
fun SettingsScreen(
accountViewModel: AccountViewModel,
@@ -139,10 +148,13 @@ fun SettingsScreen(
placeholder = languageList[languageIndex],
options = languageList,
onSelect = {
scope.launch(Dispatchers.IO) {
val locale = languageEntries[languageList[it]]
accountViewModel.account.settings.preferredLanguage = locale
LocalPreferences.saveToEncryptedStorage(accountViewModel.account)
GlobalScope.launch(Dispatchers.Main) {
val job = scope.launch(Dispatchers.IO) {
val locale = languageEntries[languageList[it]]
accountViewModel.account.settings.preferredLanguage = locale
LocalPreferences.saveToEncryptedStorage(accountViewModel.account)
}
job.join()
val appLocale: LocaleListCompat = LocaleListCompat.forLanguageTags(languageEntries[languageList[it]])
AppCompatDelegate.setApplicationLocales(appLocale)
}

View File

@@ -1,18 +1,13 @@
package com.vitorpamplona.amethyst.ui.screen.loggedIn
import android.Manifest
import android.net.Uri
import android.os.Build
import androidx.compose.animation.Crossfade
import androidx.compose.animation.core.tween
import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.ExperimentalLayoutApi
import androidx.compose.foundation.layout.FlowRow
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxHeight
@@ -23,14 +18,9 @@ import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.pager.PagerState
import androidx.compose.foundation.pager.VerticalPager
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.material.ButtonDefaults
import androidx.compose.material.CircularProgressIndicator
import androidx.compose.material.Icon
import androidx.compose.material.IconButton
import androidx.compose.material.MaterialTheme
import androidx.compose.material.OutlinedButton
import androidx.compose.material.Text
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.MoreVert
import androidx.compose.runtime.Composable
@@ -42,34 +32,21 @@ import androidx.compose.runtime.getValue
import androidx.compose.runtime.livedata.observeAsState
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalLifecycleOwner
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.LifecycleEventObserver
import androidx.lifecycle.viewmodel.compose.viewModel
import com.google.accompanist.permissions.ExperimentalPermissionsApi
import com.google.accompanist.permissions.isGranted
import com.google.accompanist.permissions.rememberPermissionState
import com.vitorpamplona.amethyst.R
import com.vitorpamplona.amethyst.model.Note
import com.vitorpamplona.amethyst.service.NostrVideoDataSource
import com.vitorpamplona.amethyst.service.model.FileHeaderEvent
import com.vitorpamplona.amethyst.service.model.FileStorageHeaderEvent
import com.vitorpamplona.amethyst.ui.actions.GallerySelect
import com.vitorpamplona.amethyst.ui.actions.NewMediaModel
import com.vitorpamplona.amethyst.ui.actions.NewMediaView
import com.vitorpamplona.amethyst.ui.actions.NewPostView
import com.vitorpamplona.amethyst.ui.components.ObserveDisplayNip05Status
import com.vitorpamplona.amethyst.ui.navigation.Route
import com.vitorpamplona.amethyst.ui.note.FileHeaderDisplay
import com.vitorpamplona.amethyst.ui.note.FileStorageHeaderDisplay
import com.vitorpamplona.amethyst.ui.note.LikeReaction
@@ -91,10 +68,6 @@ import com.vitorpamplona.amethyst.ui.theme.Size35dp
import com.vitorpamplona.amethyst.ui.theme.onBackgroundColorFilter
import com.vitorpamplona.amethyst.ui.theme.placeholderText
import kotlinx.collections.immutable.ImmutableList
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
@Composable
fun VideoScreen(
@@ -112,10 +85,6 @@ fun VideoScreen(
println("Video Start")
NostrVideoDataSource.start()
}
if (event == Lifecycle.Event.ON_PAUSE) {
println("Video Stop")
NostrVideoDataSource.stop()
}
}
lifeCycleOwner.lifecycle.addObserver(observer)
@@ -331,11 +300,14 @@ private fun VideoUserOptionAction(
note: Note,
accountViewModel: AccountViewModel
) {
var moreActionsExpanded by remember { mutableStateOf(false) }
val popupExpanded = remember { mutableStateOf(false) }
val enablePopup = remember {
{ popupExpanded.value = true }
}
IconButton(
modifier = remember { Modifier.size(22.dp) },
onClick = { moreActionsExpanded = true }
onClick = enablePopup
) {
Icon(
imageVector = Icons.Default.MoreVert,
@@ -346,8 +318,7 @@ private fun VideoUserOptionAction(
NoteDropDownMenu(
note,
moreActionsExpanded,
{ moreActionsExpanded = false },
popupExpanded,
accountViewModel
)
}
@@ -401,110 +372,3 @@ fun ReactionsColumn(baseNote: Note, accountViewModel: AccountViewModel, nav: (St
ViewCountReaction(baseNote, grayTint = MaterialTheme.colors.onBackground, barChartSize = 39.dp, viewCountColorFilter = MaterialTheme.colors.onBackgroundColorFilter)
}
}
@OptIn(ExperimentalPermissionsApi::class)
@Composable
fun NewImageButton(accountViewModel: AccountViewModel, nav: (String) -> Unit) {
var wantsToPost by remember {
mutableStateOf(false)
}
var pickedURI by remember {
mutableStateOf<Uri?>(null)
}
val scope = rememberCoroutineScope()
val postViewModel: NewMediaModel = viewModel()
postViewModel.onceUploaded {
scope.launch(Dispatchers.Default) {
// awaits an refresh on the list
delay(250)
withContext(Dispatchers.Main) {
val route = Route.Video.route.replace("{scrollToTop}", "true")
nav(route)
}
}
}
if (wantsToPost) {
val cameraPermissionState =
rememberPermissionState(
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
Manifest.permission.READ_MEDIA_IMAGES
} else {
Manifest.permission.READ_EXTERNAL_STORAGE
}
)
if (cameraPermissionState.status.isGranted) {
var showGallerySelect by remember { mutableStateOf(false) }
if (showGallerySelect) {
GallerySelect(
onImageUri = { uri ->
wantsToPost = false
showGallerySelect = false
pickedURI = uri
}
)
}
showGallerySelect = true
} else {
LaunchedEffect(key1 = accountViewModel) {
cameraPermissionState.launchPermissionRequest()
}
}
}
pickedURI?.let {
NewMediaView(
uri = it,
onClose = { pickedURI = null },
postViewModel = postViewModel,
accountViewModel = accountViewModel,
nav = nav
)
}
if (postViewModel.isUploadingImage) {
ShowProgress(postViewModel)
} else {
OutlinedButton(
onClick = { wantsToPost = true },
modifier = Modifier.size(55.dp),
shape = CircleShape,
colors = ButtonDefaults.outlinedButtonColors(backgroundColor = MaterialTheme.colors.primary),
contentPadding = PaddingValues(0.dp)
) {
Icon(
painter = painterResource(R.drawable.ic_compose),
null,
modifier = Modifier.size(26.dp),
tint = Color.White
)
}
}
}
@Composable
private fun ShowProgress(postViewModel: NewMediaModel) {
Box(Modifier.size(55.dp), contentAlignment = Alignment.Center) {
CircularProgressIndicator(
progress = postViewModel.uploadingPercentage.value,
modifier = Modifier
.size(55.dp)
.clip(CircleShape)
.background(MaterialTheme.colors.background),
strokeWidth = 5.dp
)
postViewModel.uploadingDescription.value?.let {
Text(
it,
color = MaterialTheme.colors.onSurface,
fontSize = 10.sp,
textAlign = TextAlign.Center
)
}
}
}

View File

@@ -158,7 +158,7 @@
<string name="translations_to">al</string>
<string name="translations_show_in_lang_first">Montri en %1$s unua</string>
<string name="translations_always_translate_to_lang">Ĉiam traduki al</string>
<string name="nip_05" translatable="false">NIP-05</string>
<string name="nip_05">Nostr Address</string>
<string name="lnurl" translatable="false">LNURL...</string>
<string name="never">neniam</string>
<string name="now">nun</string>

View File

@@ -164,7 +164,7 @@
<string name="translations_show_in_lang_first">ابتدا به نشان بده %1$s</string>
<string name="translations_always_translate_to_lang">همیشه به ترجمه کن %1$s</string>
<string name="translations_never_translate_from_lang">هرگز به ترجمه نکن %1$s</string>
<string name="nip_05" translatable="false">NIP-05</string>
<string name="nip_05">Nostr Address</string>
<string name="lnurl" translatable="false">LNURL...</string>
<string name="never">هرگز</string>
<string name="now">اکنون</string>

View File

@@ -164,7 +164,7 @@
<string name="translations_show_in_lang_first">最初に%1$sで表示</string>
<string name="translations_always_translate_to_lang">常に%1$sに翻訳</string>
<string name="translations_never_translate_from_lang">%1$sを翻訳しない</string>
<string name="never">never</string>
<string name="never">なし</string>
<string name="now"></string>
<string name="h">時間</string>
<string name="m"></string>
@@ -459,4 +459,17 @@
<string name="add_sensitive_content_label">センシティブなコンテンツ</string>
<string name="add_sensitive_content_description">コンテンツを表示する前に、センシティブなコンテンツであることを示す警告を表示します</string>
<string name="settings">設定</string>
<string name="always">常に</string>
<string name="wifi_only">Wifiのみ</string>
<string name="system">システム</string>
<string name="light">ライト</string>
<string name="dark">ダーク</string>
<string name="application_preferences">アプリ設定</string>
<string name="language">言語</string>
<string name="theme">テーマ</string>
<string name="automatically_load_images_gifs">自動的に画像/GIFを読み込み</string>
<string name="automatically_play_videos">自動的に動画を再生</string>
<string name="automatically_show_url_preview">自動的にURLプレビューを表示</string>
<string name="load_image">画像読み込み</string>
</resources>

View File

@@ -161,7 +161,7 @@
<string name="translations_show_in_lang_first">Eerst laten zien in %1$s</string>
<string name="translations_always_translate_to_lang">Altijd vertalen naar %1$s</string>
<string name="translations_never_translate_from_lang">Nooit vertalen vanuit %1$s</string>
<string name="nip_05" translatable="false">NIP-05</string>
<string name="nip_05">Nostr Address</string>
<string name="lnurl" translatable="false">LNURL...</string>
<string name="never">nooit</string>
<string name="now">nu</string>

View File

@@ -23,6 +23,7 @@
<string name="copy_note_id">Copiar ID da Nota</string>
<string name="broadcast">Transmitir</string>
<string name="request_deletion">Pedir para excluir</string>
<string name="block_report">Bloquear / Denunciar</string>
<string name="block_hide_user">Bloquear e ocultar usuário</string>
<string name="report_spam_scam">Denunciar spam/fraude</string>
<string name="report_impersonation">Denunciar representação</string>
@@ -33,6 +34,8 @@
<string name="login_with_a_private_key_to_like_posts">Faça login com uma chave privada para curtir postagens</string>
<string name="no_zap_amount_setup_long_press_to_change">Nenhuma configuração de quantidade de Zap. Pressione e segure para alterar</string>
<string name="login_with_a_private_key_to_be_able_to_send_zaps">Faça login com uma chave privada para poder enviar Zaps</string>
<string name="login_with_a_private_key_to_be_able_to_follow">Faça login com uma chave privada para poder seguir</string>
<string name="login_with_a_private_key_to_be_able_to_unfollow">Faça login com uma chave privada para poder remover seguidores</string>
<string name="zaps">Zaps</string>
<string name="view_count">Contagem de visualizações</string>
<string name="boost">Impulsionar</string>
@@ -121,6 +124,7 @@
<string name="send_a_direct_message">Enviar uma mensagem direta</string>
<string name="edits_the_user_s_metadata">Muda os dados do usuário</string>
<string name="follow">Seguir</string>
<string name="follow_back">Seguir de volta</string>
<string name="unblock">Desbloquear</string>
<string name="copy_user_id">Copiar ID do usuário</string>
<string name="unblock_user">Desbloquear Usuário</string>
@@ -247,5 +251,180 @@
<string name="wallet_connect_service_explainer">Use sua chave privada para pagar zaps sem sair do app. Qualquer pessoa com acesso à sua chave privada Nostr poderá gastar o saldo da sua carteira. Guarde apenas os fundos que você pode perder e use um retransmissor privado, se possível. O operador de Relay pode ver seus metadados de pagamentos.</string>
<string name="wallet_connect_service_pubkey">Wallet Connect Pubkey</string>
<string name="wallet_connect_service_relay">Wallet Connect Relay</string>
<string name="wallet_connect_service_secret">Wallet Connect Secret</string>
<string name="wallet_connect_service_show_secret">Mostrar chave secreta</string>
<string name="wallet_connect_service_secret_placeholder">chave privada nsec / hex</string>
<string name="pledge_amount_in_sats">Valor em Sats</string>
<string name="post_poll">Postar enquete</string>
<string name="poll_heading_required">Campos obrigatórios:</string>
<string name="poll_zap_recipients">Destinatários Zap</string>
<string name="poll_primary_description">Descrição da enquete principal...</string>
<string name="poll_option_index">Opção %s</string>
<string name="poll_option_description">Descrição da opção de enquete</string>
<string name="poll_heading_optional">Campos opcionais:</string>
<string name="poll_zap_value_min">Zap mínimo</string>
<string name="poll_zap_value_max">Zap máximo</string>
<string name="poll_consensus_threshold">Consenso</string>
<string name="poll_consensus_threshold_percent">(0100)%</string>
<string name="poll_closing_time">Fechar depois</string>
<string name="poll_closing_time_days">dias</string>
<string name="poll_is_closed">Enquete está fechada para novos votos</string>
<string name="poll_zap_amount">Valor do Zap</string>
<string name="one_vote_per_user_on_atomic_votes">Apenas um voto por usuário é permitido neste tipo de enquete</string>
<string name="looking_for_event">Procurando o evento %1$s</string>
<string name="custom_zaps_add_a_message">Adicionar uma mensagem pública</string>
<string name="custom_zaps_add_a_message_private">Adicionar uma mensagem privada</string>
<string name="custom_zaps_add_a_message_nonzap">Adicionar uma mensagem</string>
<string name="custom_zaps_add_a_message_example">Obrigado por todo o seu trabalho!</string>
<string name="lightning_create_and_add_invoice">Criar e adicionar</string>
<string name="poll_author_no_vote">Os autores de enquetes não podem votar em suas próprias enquetes.</string>
<string name="hash_verification_passed">A imagem é a mesma desde o post</string>
<string name="hash_verification_failed">A imagem mudou. O autor pode não ter visto a mudança</string>
<string name="content_description_add_image">Adicionar Imagem</string>
<string name="content_description_add_video">Adicionar Video</string>
<string name="content_description_add_document">Adicionar Documento</string>
<string name="add_content">Adicionar à mensagem</string>
<string name="content_description">Descrição do conteúdo</string>
<string name="content_description_example">Um barco azul em uma praia de areia branca ao pôr do sol</string>
<string name="zap_type">Tipo de Zap</string>
<string name="zap_type_explainer">Tipo de Zap para todas as opções</string>
<string name="zap_type_public">Público</string>
<string name="zap_type_public_explainer">Todos podem ver a transação e a mensagem</string>
<string name="zap_type_private">Privado</string>
<string name="zap_type_private_explainer">O remetente e o destinatário podem se ver e ler a mensagem</string>
<string name="zap_type_anonymous">Anônimo</string>
<string name="zap_type_anonymous_explainer">Destinatário e o público não sabem quem enviou o pagamento</string>
<string name="zap_type_nonzap">Sem Zap</string>
<string name="zap_type_nonzap_explainer">Nenhum traço no Nostr, apenas na Lightning</string>
<string name="file_server">Servidor de arquivos</string>
<string name="zap_forward_lnAddress">LnAddress ou @Usuario</string>
<string name="upload_server_imgur">imgur.com - confiável</string>
<string name="upload_server_imgur_explainer">Imgur pode modificar o arquivo</string>
<string name="upload_server_nostrimg">nostrimg.com - confiável</string>
<string name="upload_server_nostrimg_explainer">NostrImg pode modificar o arquivo</string>
<string name="upload_server_nostrbuild">nostr.build - confiável</string>
<string name="upload_server_nostrbuild_explainer">Nostr.build pode modificar o arquivo</string>
<string name="upload_server_nostrfilesdev">nostrfiles.dev - confiável</string>
<string name="upload_server_nostrfilesdev_explainer">Nostrfiles.dev pode modificar o arquivo</string>
<string name="upload_server_nostrcheckme">nostrcheck.me - confiável</string>
<string name="upload_server_nostrcheckme_explainer">nostrcheck.me pode modificar o arquivo</string>
<string name="upload_server_imgur_nip94">Imgur verificável (NIP-94)</string>
<string name="upload_server_imgur_nip94_explainer">Verifica se o Imgur modificou o arquivo. Novo NIP: outros clientes podem não ver</string>
<string name="upload_server_nostrimg_nip94">NostrImg verificável (NIP-94)</string>
<string name="upload_server_nostrimg_nip94_explainer">Verifica se NostrImg modificou o arquivo. Novo NIP: outros clientes podem não ver</string>
<string name="upload_server_nostrbuild_nip94">Nostr.build verificável (NIP-94)</string>
<string name="upload_server_nostrbuild_nip94_explainer">Verifica se Nostr.build modificou o arquivo. Novo NIP: outros clientes podem não ver</string>
<string name="upload_server_nostrfilesdev_nip94">Nostrfiles.dev verificável (NIP-94)</string>
<string name="upload_server_nostrfilesdev_nip94_explainer">Verifica se Nostrfiles.dev modificou o arquivo. Novo NIP: outros clientes podem não ver</string>
<string name="upload_server_nostrcheckme_nip94">Verificável Nostrcheck.me (NIP-94)</string>
<string name="upload_server_nostrcheckme_nip94_explainer">Verifica se Nostrcheck.me modificou o arquivo. Novo NIP: outros clientes podem não ver</string>
<string name="upload_server_relays_nip95">Seus relays (NIP-95)</string>
<string name="upload_server_relays_nip95_explainer">Os arquivos são hospedados por seus relays. Novo NIP: verifique se eles suportam</string>
<string name="connect_via_tor_short">Configuração do Tor/Orbot</string>
<string name="connect_via_tor">Conecte-se através da configuração do Orbot</string>
<string name="do_you_really_want_to_disable_tor_title">Desconectar do Orbot/Tor?</string>
<string name="do_you_really_want_to_disable_tor_text">Seus dados serão transferidos imediatamente na rede regular</string>
<string name="yes">Sim</string>
<string name="no">Não</string>
<string name="follow_list_selection">Lista de seguidores</string>
<string name="follow_list_kind3follows">Seguindo</string>
<string name="follow_list_global">Global</string>
<string name="connect_through_your_orbot_setup_markdown">
## Conecte-se através do Tor com o Orbot
\n\n1. Instale o [Orbot](https://play.google.com/store/apps/details?id=org.torproject.android)
\n2. Iniciar Orbot
\n3. No Orbot, verifique a porta Socks. O padrão usa 9050
\n4. Se necessário altere a porta no Orbot
\n5. Configure a porta Socks nesta tela
\n6. Pressione o botão Ativar para usar o Orbot como proxy
</string>
<string name="orbot_socks_port">Porta Socks Orbot</string>
<string name="invalid_port_number">Número de porta inválido</string>
<string name="use_orbot">Usar Orbot</string>
<string name="disconnect_from_your_orbot_setup">Desconectar Tor/Orbot</string>
<string name="app_notification_dms_channel_name">Mensagens privadas</string>
<string name="app_notification_dms_channel_description">Notifica você quando chega uma mensagem privada</string>
<string name="app_notification_zaps_channel_name">Zaps Recebidos</string>
<string name="app_notification_zaps_channel_description">Notifica quando alguém te manda um zap</string>
<string name="app_notification_zaps_channel_message">%1$s sats</string>
<string name="app_notification_zaps_channel_message_from">De %1$s</string>
<string name="app_notification_zaps_channel_message_for">por %1$s</string>
<string name="reply_notify">"Notificar: "</string>
<string name="channel_list_join_conversation">Entrar na conversa</string>
<string name="channel_list_user_or_group_id">ID do usuário ou grupo</string>
<string name="channel_list_user_or_group_id_demo">npub, nevent ou hex</string>
<string name="channel_list_create_channel">Criar</string>
<string name="channel_list_join_channel">Entrar</string>
<string name="today">Hoje</string>
<string name="content_warning">Aviso de conteúdo</string>
<string name="content_warning_explanation">Esta nota contém conteúdo sensível que algumas pessoas podem achar ofensivo ou perturbador</string>
<string name="content_warning_hide_all_sensitive_content">Sempre ocultar conteúdo sensível</string>
<string name="content_warning_show_all_sensitive_content">Sempre mostrar conteúdo sensível</string>
<string name="content_warning_see_warnings">Sempre mostrar avisos de conteúdo</string>
<string name="recommended_apps">"Recomenda: "</string>
<string name="filter_spam_from_strangers">Filtrar spam de estranhos</string>
<string name="warn_when_posts_have_reports_from_your_follows">Avise quando as postagens tiverem denuncias de seus seguidores</string>
<string name="new_reaction_symbol">Novo símbolo de reação</string>
<string name="no_reaction_type_setup_long_press_to_change">Nenhum tipo de reação selecionado. Pressione e segure para alterar</string>
<string name="zapraiser">Arrecadação de Zaps</string>
<string name="zapraiser_explainer">Adiciona uma quantidade alvo de sats para financiar esta postagem. Os clientes podem mostrar isso como uma barra de progresso para incentivar doações</string>
<string name="zapraiser_target_amount_in_sats">Valor alvo em Sats</string>
<string name="sats_to_complete">Arrecadação de Zaps em %1$s. %2$s sats para meta</string>
<string name="read_from_relay">Ler do Relay</string>
<string name="write_to_relay">Enviar para o Relay</string>
<string name="an_error_ocurred_trying_to_get_relay_information">Ocorreu um erro ao tentar obter informações do relay de %1$s</string>
<string name="owner">Proprietário</string>
<string name="version">Versão</string>
<string name="software">Programa</string>
<string name="contact">Contato</string>
<string name="supports">NIPs Suportados</string>
<string name="admission_fees">Taxas de admissão</string>
<string name="payments_url">URL de pagamentos</string>
<string name="limitations">Limitações</string>
<string name="countries">Países</string>
<string name="languages">Línguas</string>
<string name="tags">Tags</string>
<string name="posting_policy">Política de postagem</string>
<string name="message_length">Tamanho da mensagem</string>
<string name="subscriptions">Assinaturas</string>
<string name="filters">Filtros</string>
<string name="subscription_id_length">Comprimento do ID da assinatura</string>
<string name="minimum_prefix">Prefixo mínimo</string>
<string name="maximum_event_tags">Máximo de tags de evento</string>
<string name="content_length">Tamanho do conteúdo</string>
<string name="minimum_pow">PoW mínimo</string>
<string name="auth">Autenticação</string>
<string name="payment">Pagamento</string>
<string name="cashu">Token Cashu</string>
<string name="cashu_redeem">Resgatar</string>
<string name="live_stream_live_tag">AO VIVO</string>
<string name="live_stream_offline_tag">DESLIGADA</string>
<string name="live_stream_ended_tag">FINALIZADA</string>
<string name="live_stream_planned_tag">AGENDADA</string>
<string name="live_stream_is_offline">A transmissão ao vivo está offline</string>
<string name="live_stream_has_ended">Transmissão ao vivo encerrada</string>
<string name="are_you_sure_you_want_to_log_out">Sair exclui todas as suas informações locais. Certifique-se de fazer backup de suas chaves privadas para evitar a perda de sua conta. Você quer continuar?</string>
<string name="followed_tags">Tags Seguidas</string>
<string name="relay_setup">Relays</string>
<string name="discover_live">Ao vivo</string>
<string name="discover_community">Comunidade</string>
<string name="discover_chat">Chats</string>
<string name="community_approved_posts">Postagens Aprovadas</string>
<string name="groups_no_descriptor">Este grupo não tem uma descrição ou regras. Fale com o proprietário para adicionar</string>
<string name="community_no_descriptor">Esta comunidade não tem uma descrição. Fale com o proprietário para adicionar</string>
<string name="add_sensitive_content_label">Conteúdo sensível</string>
<string name="add_sensitive_content_description">Adiciona aviso de conteúdo sensível antes de mostrar este conteúdo</string>
<string name="settings">Configurações</string>
<string name="always">Sempre</string>
<string name="wifi_only">Somente wifi</string>
<string name="system">Sistema</string>
<string name="light">Claro</string>
<string name="dark">Escuro</string>
<string name="application_preferences">Preferências do aplicativo</string>
<string name="language">Linguagem</string>
<string name="theme">Tema</string>
<string name="automatically_load_images_gifs">Carregar imagens/gifs automaticamente</string>
<string name="automatically_play_videos">Reproduzir vídeos automaticamente</string>
<string name="automatically_show_url_preview">Mostrar automaticamente a visualização da URL</string>
<string name="load_image">Carregar imagem</string>
</resources>

View File

@@ -167,7 +167,7 @@
<string name="translations_show_in_lang_first">Show in %1$s first</string>
<string name="translations_always_translate_to_lang">Always translate to %1$s</string>
<string name="translations_never_translate_from_lang">Never translate from %1$s</string>
<string name="nip_05" translatable="false">NIP-05</string>
<string name="nip_05">Nostr Address</string>
<string name="lnurl" translatable="false">LNURL...</string>
<string name="never">never</string>
<string name="now">now</string>
@@ -360,9 +360,6 @@
<string name="upload_server_nostrcheckme_nip94">Verifiable Nostrcheck.me (NIP-94)</string>
<string name="upload_server_nostrcheckme_nip94_explainer">Checks if Nostrcheck.me modified the file. New NIP: other clients might not see it</string>
<string name="upload_server_relays_nip95">Your relays (NIP-95)</string>
<string name="upload_server_relays_nip95_explainer">Files are hosted by your relays. New NIP: check if they support</string>