mirror of
https://github.com/vitorpamplona/amethyst.git
synced 2025-04-08 11:58:03 +02:00
Merge branch 'main' into amber
This commit is contained in:
commit
a0308938de
@ -82,13 +82,14 @@ height="80">](https://github.com/vitorpamplona/amethyst/releases)
|
||||
- [x] Private Messages and Small Groups (NIP-24)
|
||||
- [x] Gift Wraps & Seals (NIP-59)
|
||||
- [x] Versioned Encrypted Payloads (NIP-44)
|
||||
- [x] Expiration Support (NIP-40)
|
||||
- [x] Status Event (NIP-315)
|
||||
- [ ] Marketplace (NIP-15)
|
||||
- [ ] Image/Video Capture in the app
|
||||
- [ ] Local Database
|
||||
- [ ] Bookmarks, Pinned Posts, Muted Events (NIP-51)
|
||||
- [ ] Proof of Work in the Phone (NIP-13, NIP-20)
|
||||
- [ ] Workspaces
|
||||
- [ ] Expiration Support (NIP-40)
|
||||
- [ ] Infinity Scroll
|
||||
- [ ] Relay List Metadata (NIP-65)
|
||||
- [ ] Signing Requests (NIP-46)
|
||||
|
@ -13,14 +13,14 @@ android {
|
||||
applicationId "com.vitorpamplona.amethyst"
|
||||
minSdk 26
|
||||
targetSdk 34
|
||||
versionCode 278
|
||||
versionName "0.74.5"
|
||||
versionCode 281
|
||||
versionName "0.75.2"
|
||||
|
||||
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
||||
vectorDrawables {
|
||||
useSupportLibrary true
|
||||
}
|
||||
resourceConfigurations += ['ar', 'cs', 'de', 'eo', 'es', 'fa', 'fr', 'hu', 'ja', 'night', 'nl', 'pt-rBR', 'ru', 'sv-rSE', 'ta', 'tr', 'uk', 'zh', 'sh-rHK', 'zh-rTW']
|
||||
resourceConfigurations += ['ar', 'cs', 'de', 'eo', 'es', 'fa', 'fr', 'hu', 'ja', 'night', 'nl', 'pt-rBR', 'ru', 'sv-rSE', 'ta', 'th', 'tr', 'uk', 'zh', 'sh-rHK', 'zh-rTW']
|
||||
}
|
||||
|
||||
buildTypes {
|
||||
|
@ -62,7 +62,7 @@
|
||||
android:usesCleartextTraffic="true"
|
||||
android:hardwareAccelerated="true"
|
||||
android:localeConfig="@xml/locales_config"
|
||||
tools:targetApi="33">
|
||||
tools:targetApi="34">
|
||||
<activity
|
||||
android:name=".ui.MainActivity"
|
||||
android:exported="true"
|
||||
|
@ -133,6 +133,7 @@ object ServiceManager {
|
||||
LocalCache.pruneContactLists(accounts)
|
||||
LocalCache.pruneRepliesAndReactions(accounts)
|
||||
LocalCache.prunePastVersionsOfReplaceables()
|
||||
LocalCache.pruneExpiredEvents()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1174,6 +1174,25 @@ class Account(
|
||||
}
|
||||
}
|
||||
|
||||
fun updateStatus(oldStatus: AddressableNote, newStatus: String) {
|
||||
if (!isWriteable()) return
|
||||
val oldEvent = oldStatus.event as? StatusEvent ?: return
|
||||
|
||||
val event = StatusEvent.update(oldEvent, newStatus, keyPair.privKey!!)
|
||||
|
||||
Client.send(event)
|
||||
LocalCache.consume(event, null)
|
||||
}
|
||||
|
||||
fun createStatus(newStatus: String) {
|
||||
if (!isWriteable()) return
|
||||
|
||||
val event = StatusEvent.create(newStatus, "general", expiration = null, keyPair.privKey!!)
|
||||
|
||||
Client.send(event)
|
||||
LocalCache.consume(event, null)
|
||||
}
|
||||
|
||||
fun removeEmojiPack(usersEmojiList: Note, emojiList: Note) {
|
||||
if (!isWriteable()) return
|
||||
|
||||
|
@ -14,7 +14,10 @@ import com.vitorpamplona.quartz.encoders.Nip19
|
||||
import com.vitorpamplona.quartz.encoders.decodePublicKeyAsHexOrNull
|
||||
import com.vitorpamplona.quartz.encoders.toHexKey
|
||||
import com.vitorpamplona.quartz.events.*
|
||||
import com.vitorpamplona.quartz.utils.TimeUtils
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
import kotlinx.collections.immutable.persistentSetOf
|
||||
import kotlinx.collections.immutable.toImmutableList
|
||||
import kotlinx.collections.immutable.toImmutableSet
|
||||
import kotlinx.coroutines.*
|
||||
import kotlinx.coroutines.flow.MutableSharedFlow
|
||||
@ -342,6 +345,27 @@ object LocalCache {
|
||||
private fun consume(event: PinListEvent) { consumeBaseReplaceable(event) }
|
||||
private fun consume(event: RelaySetEvent) { consumeBaseReplaceable(event) }
|
||||
private fun consume(event: AudioTrackEvent) { consumeBaseReplaceable(event) }
|
||||
fun consume(event: StatusEvent, relay: Relay?) {
|
||||
val version = getOrCreateNote(event.id)
|
||||
val note = getOrCreateAddressableNote(event.address())
|
||||
val author = getOrCreateUser(event.pubKey)
|
||||
|
||||
if (version.event == null) {
|
||||
version.loadEvent(event, author, emptyList())
|
||||
version.moveAllReferencesTo(note)
|
||||
}
|
||||
|
||||
// Already processed this event.
|
||||
if (note.event?.id() == event.id()) return
|
||||
|
||||
if (event.createdAt > (note.createdAt() ?: 0)) {
|
||||
note.loadEvent(event, author, emptyList())
|
||||
|
||||
author.liveSet?.statuses?.invalidateData()
|
||||
|
||||
refreshObservers(note)
|
||||
}
|
||||
}
|
||||
|
||||
fun consume(event: BadgeDefinitionEvent) { consumeBaseReplaceable(event) }
|
||||
|
||||
@ -1088,7 +1112,7 @@ object LocalCache {
|
||||
}
|
||||
|
||||
return notes.values.filter {
|
||||
(it.event !is GenericRepostEvent && it.event !is RepostEvent && it.event !is CommunityPostApprovalEvent && it.event !is ReactionEvent && it.event !is GiftWrapEvent) &&
|
||||
(it.event !is GenericRepostEvent && it.event !is RepostEvent && it.event !is CommunityPostApprovalEvent && it.event !is ReactionEvent && it.event !is GiftWrapEvent && it.event !is LnZapEvent && it.event !is LnZapRequestEvent) &&
|
||||
(
|
||||
it.event?.content()?.contains(text, true) ?: false ||
|
||||
it.event?.matchTag1With(text) ?: false ||
|
||||
@ -1096,7 +1120,7 @@ object LocalCache {
|
||||
it.idNote().startsWith(text, true)
|
||||
)
|
||||
} + addressables.values.filter {
|
||||
(it.event !is GenericRepostEvent && it.event !is RepostEvent && it.event !is CommunityPostApprovalEvent && it.event !is ReactionEvent && it.event !is GiftWrapEvent) &&
|
||||
(it.event !is GenericRepostEvent && it.event !is RepostEvent && it.event !is CommunityPostApprovalEvent && it.event !is ReactionEvent && it.event !is GiftWrapEvent && it.event !is LnZapEvent && it.event !is LnZapRequestEvent) &&
|
||||
(
|
||||
it.event?.content()?.contains(text, true) ?: false ||
|
||||
it.event?.matchTag1With(text) ?: false ||
|
||||
@ -1125,6 +1149,18 @@ object LocalCache {
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun findStatusesForUser(user: User): ImmutableList<AddressableNote> {
|
||||
checkNotInMainThread()
|
||||
|
||||
return addressables.filter {
|
||||
val noteEvent = it.value.event
|
||||
(noteEvent is StatusEvent && noteEvent.pubKey == user.pubkeyHex && !noteEvent.isExpired() && noteEvent.content.isNotBlank())
|
||||
}.values
|
||||
.sortedWith(compareBy({ it.event?.expiration() ?: it.event?.createdAt() }, { it.idHex }))
|
||||
.reversed()
|
||||
.toImmutableList()
|
||||
}
|
||||
|
||||
fun cleanObservers() {
|
||||
notes.forEach {
|
||||
it.value.clearLive()
|
||||
@ -1268,6 +1304,39 @@ object LocalCache {
|
||||
}
|
||||
}
|
||||
|
||||
fun pruneExpiredEvents() {
|
||||
checkNotInMainThread()
|
||||
|
||||
val now = TimeUtils.now()
|
||||
|
||||
val toBeRemoved = notes.filter {
|
||||
it.value.event?.isExpired() == true
|
||||
}.values
|
||||
|
||||
val childrenToBeRemoved = mutableListOf<Note>()
|
||||
|
||||
toBeRemoved.forEach {
|
||||
notes.remove(it.idHex)
|
||||
|
||||
it.replyTo?.forEach { masterNote ->
|
||||
masterNote.removeReply(it)
|
||||
masterNote.removeBoost(it)
|
||||
masterNote.removeReaction(it)
|
||||
masterNote.removeZap(it)
|
||||
masterNote.removeReport(it)
|
||||
masterNote.clearEOSE() // allows reloading of these events
|
||||
}
|
||||
|
||||
childrenToBeRemoved.addAll(it.removeAllChildNotes())
|
||||
}
|
||||
|
||||
removeChildrenOf(childrenToBeRemoved)
|
||||
|
||||
if (toBeRemoved.size > 1) {
|
||||
println("PRUNE: ${toBeRemoved.size} thread replies removed.")
|
||||
}
|
||||
}
|
||||
|
||||
fun pruneHiddenMessages(account: Account) {
|
||||
checkNotInMainThread()
|
||||
|
||||
@ -1423,6 +1492,7 @@ object LocalCache {
|
||||
is PrivateDmEvent -> consume(event, relay)
|
||||
is PinListEvent -> consume(event)
|
||||
is PeopleListEvent -> consume(event)
|
||||
is PollNoteEvent -> consume(event, relay)
|
||||
is ReactionEvent -> consume(event)
|
||||
is RecommendRelayEvent -> consume(event)
|
||||
is RelaySetEvent -> consume(event)
|
||||
@ -1439,8 +1509,9 @@ object LocalCache {
|
||||
}
|
||||
consume(event)
|
||||
}
|
||||
is StatusEvent -> consume(event, relay)
|
||||
is TextNoteEvent -> consume(event, relay)
|
||||
is PollNoteEvent -> consume(event, relay)
|
||||
|
||||
else -> {
|
||||
Log.w("Event Not Supported", event.toJson())
|
||||
}
|
||||
|
@ -3,6 +3,8 @@ package com.vitorpamplona.amethyst.model
|
||||
import androidx.compose.runtime.Immutable
|
||||
import androidx.compose.runtime.Stable
|
||||
import androidx.lifecycle.LiveData
|
||||
import androidx.lifecycle.distinctUntilChanged
|
||||
import androidx.lifecycle.map
|
||||
import com.vitorpamplona.amethyst.service.NostrSingleEventDataSource
|
||||
import com.vitorpamplona.amethyst.service.checkNotInMainThread
|
||||
import com.vitorpamplona.amethyst.service.firstFullCharOrEmoji
|
||||
@ -10,6 +12,7 @@ import com.vitorpamplona.amethyst.service.relays.EOSETime
|
||||
import com.vitorpamplona.amethyst.service.relays.Relay
|
||||
import com.vitorpamplona.amethyst.ui.actions.updated
|
||||
import com.vitorpamplona.amethyst.ui.components.BundledUpdate
|
||||
import com.vitorpamplona.amethyst.ui.note.combineWith
|
||||
import com.vitorpamplona.amethyst.ui.note.toShortenHex
|
||||
import com.vitorpamplona.quartz.encoders.ATag
|
||||
import com.vitorpamplona.quartz.encoders.Hex
|
||||
@ -651,10 +654,18 @@ open class Note(val idHex: String) {
|
||||
}
|
||||
}
|
||||
|
||||
@Stable
|
||||
class NoteLiveSet(u: Note) {
|
||||
// Observers line up here.
|
||||
val metadata: NoteLiveData = NoteLiveData(u)
|
||||
|
||||
val authorChanges = metadata.map {
|
||||
it.note.author
|
||||
}
|
||||
val hasEvent = metadata.map {
|
||||
it.note.event != null
|
||||
}.distinctUntilChanged()
|
||||
|
||||
val reactions: NoteLiveData = NoteLiveData(u)
|
||||
val boosts: NoteLiveData = NoteLiveData(u)
|
||||
val replies: NoteLiveData = NoteLiveData(u)
|
||||
@ -662,6 +673,20 @@ class NoteLiveSet(u: Note) {
|
||||
val relays: NoteLiveData = NoteLiveData(u)
|
||||
val zaps: NoteLiveData = NoteLiveData(u)
|
||||
|
||||
val hasReactions = zaps.combineWith(boosts, reactions) { zapState, boostState, reactionState ->
|
||||
zapState?.note?.zaps?.isNotEmpty() ?: false ||
|
||||
boostState?.note?.boosts?.isNotEmpty() ?: false ||
|
||||
reactionState?.note?.reactions?.isNotEmpty() ?: false
|
||||
}.distinctUntilChanged()
|
||||
|
||||
val replyCount = replies.map {
|
||||
it.note.replies.size
|
||||
}.distinctUntilChanged()
|
||||
|
||||
val boostCount = boosts.map {
|
||||
it.note.boosts.size
|
||||
}.distinctUntilChanged()
|
||||
|
||||
fun isInUse(): Boolean {
|
||||
return metadata.hasObservers() ||
|
||||
reactions.hasObservers() ||
|
||||
|
@ -3,6 +3,8 @@ package com.vitorpamplona.amethyst.model
|
||||
import androidx.compose.runtime.Immutable
|
||||
import androidx.compose.runtime.Stable
|
||||
import androidx.lifecycle.LiveData
|
||||
import androidx.lifecycle.distinctUntilChanged
|
||||
import androidx.lifecycle.map
|
||||
import com.vitorpamplona.amethyst.service.NostrSingleUserDataSource
|
||||
import com.vitorpamplona.amethyst.service.checkNotInMainThread
|
||||
import com.vitorpamplona.amethyst.service.relays.EOSETime
|
||||
@ -378,6 +380,7 @@ class User(val pubkeyHex: String) {
|
||||
}
|
||||
}
|
||||
|
||||
@Stable
|
||||
class UserLiveSet(u: User) {
|
||||
// UI Observers line up here.
|
||||
val follows: UserLiveData = UserLiveData(u)
|
||||
@ -389,6 +392,19 @@ class UserLiveSet(u: User) {
|
||||
val metadata: UserLiveData = UserLiveData(u)
|
||||
val zaps: UserLiveData = UserLiveData(u)
|
||||
val bookmarks: UserLiveData = UserLiveData(u)
|
||||
val statuses: UserLiveData = UserLiveData(u)
|
||||
|
||||
val profilePictureChanges = metadata.map {
|
||||
it.user.profilePicture()
|
||||
}.distinctUntilChanged()
|
||||
|
||||
val nip05Changes = metadata.map {
|
||||
it.user.nip05()
|
||||
}.distinctUntilChanged()
|
||||
|
||||
val userMetadataInfo = metadata.map {
|
||||
it.user.info
|
||||
}.distinctUntilChanged()
|
||||
|
||||
fun isInUse(): Boolean {
|
||||
return follows.hasObservers() ||
|
||||
@ -399,6 +415,7 @@ class UserLiveSet(u: User) {
|
||||
relayInfo.hasObservers() ||
|
||||
metadata.hasObservers() ||
|
||||
zaps.hasObservers() ||
|
||||
statuses.hasObservers() ||
|
||||
bookmarks.hasObservers()
|
||||
}
|
||||
|
||||
@ -412,6 +429,7 @@ class UserLiveSet(u: User) {
|
||||
metadata.destroy()
|
||||
zaps.destroy()
|
||||
bookmarks.destroy()
|
||||
statuses.destroy()
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -63,9 +63,9 @@ object NostrAccountDataSource : NostrDataSource("AccountData") {
|
||||
return TypedFilter(
|
||||
types = COMMON_FEED_TYPES,
|
||||
filter = JsonFilter(
|
||||
kinds = listOf(AdvertisedRelayListEvent.kind),
|
||||
kinds = listOf(AdvertisedRelayListEvent.kind, StatusEvent.kind),
|
||||
authors = listOf(account.userProfile().pubkeyHex),
|
||||
limit = 1
|
||||
limit = 5
|
||||
)
|
||||
)
|
||||
}
|
||||
|
@ -7,6 +7,7 @@ import com.vitorpamplona.amethyst.service.relays.JsonFilter
|
||||
import com.vitorpamplona.amethyst.service.relays.TypedFilter
|
||||
import com.vitorpamplona.quartz.events.MetadataEvent
|
||||
import com.vitorpamplona.quartz.events.ReportEvent
|
||||
import com.vitorpamplona.quartz.events.StatusEvent
|
||||
|
||||
object NostrSingleUserDataSource : NostrDataSource("SingleUserFeed") {
|
||||
var usersToWatch = setOf<User>()
|
||||
@ -26,6 +27,21 @@ object NostrSingleUserDataSource : NostrDataSource("SingleUserFeed") {
|
||||
}
|
||||
}
|
||||
|
||||
fun createUserStatusFilter(): List<TypedFilter>? {
|
||||
if (usersToWatch.isEmpty()) return null
|
||||
|
||||
return usersToWatch.map {
|
||||
TypedFilter(
|
||||
types = COMMON_FEED_TYPES,
|
||||
filter = JsonFilter(
|
||||
kinds = listOf(StatusEvent.kind),
|
||||
authors = listOf(it.pubkeyHex),
|
||||
since = it.latestEOSEs
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fun createUserReportFilter(): List<TypedFilter>? {
|
||||
if (usersToWatch.isEmpty()) return null
|
||||
|
||||
@ -59,8 +75,8 @@ object NostrSingleUserDataSource : NostrDataSource("SingleUserFeed") {
|
||||
val userChannelOnce = requestNewChannel()
|
||||
|
||||
override fun updateChannelFilters() {
|
||||
userChannel.typedFilters = listOfNotNull(createUserFilter()).flatten().ifEmpty { null }
|
||||
userChannelOnce.typedFilters = listOfNotNull(createUserReportFilter()).flatten().ifEmpty { null }
|
||||
userChannel.typedFilters = listOfNotNull(createUserReportFilter(), createUserStatusFilter()).flatten().ifEmpty { null }
|
||||
userChannelOnce.typedFilters = listOfNotNull(createUserFilter()).flatten().ifEmpty { null }
|
||||
}
|
||||
|
||||
fun add(user: User) {
|
||||
|
@ -120,7 +120,7 @@ fun JoinUserOrChannelView(searchBarViewModel: SearchBarViewModel, onClose: () ->
|
||||
horizontalArrangement = Arrangement.SpaceBetween,
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
CloseButton(onCancel = {
|
||||
CloseButton(onPress = {
|
||||
searchBarViewModel.clear()
|
||||
NostrSearchEventOrUserDataSource.clear()
|
||||
onClose()
|
||||
|
@ -51,7 +51,7 @@ fun NewChannelView(onClose: () -> Unit, accountViewModel: AccountViewModel, chan
|
||||
horizontalArrangement = Arrangement.SpaceBetween,
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
CloseButton(onCancel = {
|
||||
CloseButton(onPress = {
|
||||
postViewModel.clear()
|
||||
onClose()
|
||||
})
|
||||
|
@ -123,7 +123,7 @@ fun NewMediaView(uri: Uri, onClose: () -> Unit, postViewModel: NewMediaModel, ac
|
||||
horizontalArrangement = Arrangement.SpaceBetween,
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
CloseButton(onCancel = {
|
||||
CloseButton(onPress = {
|
||||
postViewModel.cancel()
|
||||
onClose()
|
||||
})
|
||||
|
@ -100,6 +100,7 @@ import com.vitorpamplona.amethyst.ui.theme.QuoteBorder
|
||||
import com.vitorpamplona.amethyst.ui.theme.Size10dp
|
||||
import com.vitorpamplona.amethyst.ui.theme.Size5dp
|
||||
import com.vitorpamplona.amethyst.ui.theme.StdHorzSpacer
|
||||
import com.vitorpamplona.amethyst.ui.theme.StdVertSpacer
|
||||
import com.vitorpamplona.amethyst.ui.theme.mediumImportanceLink
|
||||
import com.vitorpamplona.amethyst.ui.theme.placeholderText
|
||||
import com.vitorpamplona.amethyst.ui.theme.replyModifier
|
||||
@ -188,316 +189,340 @@ fun NewPostView(
|
||||
decorFitsSystemWindows = false
|
||||
)
|
||||
) {
|
||||
Surface(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.fillMaxHeight()
|
||||
) {
|
||||
if (showRelaysDialog) {
|
||||
RelaySelectionDialog(
|
||||
list = relayList,
|
||||
onClose = {
|
||||
showRelaysDialog = false
|
||||
},
|
||||
onPost = {
|
||||
relayList = it
|
||||
},
|
||||
accountViewModel = accountViewModel,
|
||||
nav = nav
|
||||
)
|
||||
}
|
||||
if (showRelaysDialog) {
|
||||
RelaySelectionDialog(
|
||||
list = relayList,
|
||||
onClose = {
|
||||
showRelaysDialog = false
|
||||
},
|
||||
onPost = {
|
||||
relayList = it
|
||||
},
|
||||
accountViewModel = accountViewModel,
|
||||
nav = nav
|
||||
)
|
||||
}
|
||||
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.fillMaxHeight()
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.padding(start = 10.dp, end = 10.dp, top = 10.dp)
|
||||
.imePadding()
|
||||
.weight(1f)
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.SpaceBetween,
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
CloseButton(onCancel = {
|
||||
Scaffold(
|
||||
topBar = {
|
||||
TopAppBar(
|
||||
title = {
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(end = 10.dp),
|
||||
horizontalArrangement = Arrangement.SpaceBetween,
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Spacer(modifier = StdHorzSpacer)
|
||||
|
||||
Box {
|
||||
IconButton(
|
||||
modifier = Modifier.align(Alignment.Center),
|
||||
onClick = {
|
||||
showRelaysDialog = true
|
||||
}
|
||||
) {
|
||||
Icon(
|
||||
painter = painterResource(R.drawable.relays),
|
||||
contentDescription = null,
|
||||
modifier = Modifier.height(25.dp),
|
||||
tint = MaterialTheme.colors.onBackground
|
||||
)
|
||||
}
|
||||
}
|
||||
PostButton(
|
||||
onPost = {
|
||||
scope.launch(Dispatchers.IO) {
|
||||
postViewModel.sendPost(relayList = relayList)
|
||||
onClose()
|
||||
}
|
||||
},
|
||||
isActive = postViewModel.canPost()
|
||||
)
|
||||
}
|
||||
},
|
||||
navigationIcon = {
|
||||
Spacer(modifier = StdHorzSpacer)
|
||||
CloseButton(onPress = {
|
||||
postViewModel.cancel()
|
||||
onClose()
|
||||
})
|
||||
|
||||
Box {
|
||||
IconButton(
|
||||
modifier = Modifier.align(Alignment.Center),
|
||||
onClick = {
|
||||
showRelaysDialog = true
|
||||
}
|
||||
) {
|
||||
Icon(
|
||||
painter = painterResource(R.drawable.relays),
|
||||
contentDescription = null,
|
||||
modifier = Modifier.height(25.dp),
|
||||
tint = MaterialTheme.colors.onBackground
|
||||
)
|
||||
}
|
||||
}
|
||||
PostButton(
|
||||
onPost = {
|
||||
scope.launch(Dispatchers.IO) {
|
||||
event = postViewModel.sendPost(relayList = relayList, signEvent = account.keyPair.privKey != null)
|
||||
if (event == null) {
|
||||
onClose()
|
||||
}
|
||||
}
|
||||
},
|
||||
isActive = postViewModel.canPost()
|
||||
)
|
||||
}
|
||||
|
||||
Row(
|
||||
},
|
||||
backgroundColor = MaterialTheme.colors.surface,
|
||||
elevation = 0.dp
|
||||
)
|
||||
}
|
||||
) { pad ->
|
||||
Surface(
|
||||
modifier = Modifier
|
||||
.padding(
|
||||
start = Size10dp,
|
||||
top = pad.calculateTopPadding(),
|
||||
end = Size10dp,
|
||||
bottom = pad.calculateBottomPadding()
|
||||
)
|
||||
.fillMaxSize()
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.fillMaxHeight()
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(top = 10.dp)
|
||||
.imePadding()
|
||||
.weight(1f)
|
||||
) {
|
||||
Column(
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.verticalScroll(scrollState)
|
||||
.weight(1f)
|
||||
) {
|
||||
postViewModel.originalNote?.let {
|
||||
NoteCompose(
|
||||
baseNote = it,
|
||||
makeItShort = true,
|
||||
unPackReply = false,
|
||||
isQuotedNote = true,
|
||||
modifier = MaterialTheme.colors.replyModifier,
|
||||
accountViewModel = accountViewModel,
|
||||
nav = nav
|
||||
)
|
||||
}
|
||||
|
||||
Notifying(postViewModel.mentions?.toImmutableList()) {
|
||||
postViewModel.removeFromReplyList(it)
|
||||
}
|
||||
|
||||
if (enableMessageInterface) {
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
modifier = Modifier.padding(vertical = Size5dp, horizontal = Size10dp)
|
||||
) {
|
||||
SendDirectMessageTo(postViewModel = postViewModel)
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.verticalScroll(scrollState)
|
||||
) {
|
||||
postViewModel.originalNote?.let {
|
||||
Row(Modifier.heightIn(max = 200.dp)) {
|
||||
NoteCompose(
|
||||
baseNote = it,
|
||||
makeItShort = true,
|
||||
unPackReply = false,
|
||||
isQuotedNote = true,
|
||||
modifier = MaterialTheme.colors.replyModifier,
|
||||
accountViewModel = accountViewModel,
|
||||
nav = nav
|
||||
)
|
||||
Spacer(modifier = StdVertSpacer)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
MessageField(postViewModel)
|
||||
|
||||
if (postViewModel.wantsPoll) {
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
modifier = Modifier.padding(vertical = Size5dp, horizontal = Size10dp)
|
||||
) {
|
||||
PollField(postViewModel)
|
||||
Row() {
|
||||
Notifying(postViewModel.mentions?.toImmutableList()) {
|
||||
postViewModel.removeFromReplyList(it)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (postViewModel.wantsToMarkAsSensitive) {
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
modifier = Modifier
|
||||
.padding(vertical = Size5dp, horizontal = Size10dp)
|
||||
) {
|
||||
ContentSensitivityExplainer(postViewModel)
|
||||
if (enableMessageInterface) {
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
modifier = Modifier.padding(vertical = Size5dp, horizontal = Size10dp)
|
||||
) {
|
||||
SendDirectMessageTo(postViewModel = postViewModel)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (postViewModel.wantsToAddGeoHash) {
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
modifier = Modifier
|
||||
.padding(vertical = Size5dp, horizontal = Size10dp)
|
||||
) {
|
||||
LocationAsHash(postViewModel)
|
||||
MessageField(postViewModel)
|
||||
|
||||
if (postViewModel.wantsPoll) {
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
modifier = Modifier.padding(vertical = Size5dp, horizontal = Size10dp)
|
||||
) {
|
||||
PollField(postViewModel)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (postViewModel.wantsForwardZapTo) {
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
modifier = Modifier.padding(vertical = Size5dp, horizontal = Size10dp)
|
||||
) {
|
||||
FowardZapTo(postViewModel)
|
||||
if (postViewModel.wantsToMarkAsSensitive) {
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
modifier = Modifier
|
||||
.padding(vertical = Size5dp, horizontal = Size10dp)
|
||||
) {
|
||||
ContentSensitivityExplainer(postViewModel)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
val url = postViewModel.contentToAddUrl
|
||||
if (url != null) {
|
||||
Row(verticalAlignment = Alignment.CenterVertically, modifier = Modifier.padding(vertical = Size5dp, horizontal = Size10dp)) {
|
||||
ImageVideoDescription(
|
||||
url,
|
||||
account.defaultFileServer,
|
||||
onAdd = { description, server, sensitiveContent ->
|
||||
postViewModel.upload(url, description, sensitiveContent, server, context, relayList)
|
||||
account.changeDefaultFileServer(server)
|
||||
},
|
||||
onCancel = {
|
||||
postViewModel.contentToAddUrl = null
|
||||
},
|
||||
onError = {
|
||||
scope.launch {
|
||||
postViewModel.imageUploadingError.emit(it)
|
||||
}
|
||||
},
|
||||
accountViewModel = accountViewModel
|
||||
)
|
||||
if (postViewModel.wantsToAddGeoHash) {
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
modifier = Modifier
|
||||
.padding(vertical = Size5dp, horizontal = Size10dp)
|
||||
) {
|
||||
LocationAsHash(postViewModel)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
val user = postViewModel.account?.userProfile()
|
||||
val lud16 = user?.info?.lnAddress()
|
||||
if (postViewModel.wantsForwardZapTo) {
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
modifier = Modifier.padding(vertical = Size5dp, horizontal = Size10dp)
|
||||
) {
|
||||
FowardZapTo(postViewModel)
|
||||
}
|
||||
}
|
||||
|
||||
if (lud16 != null && postViewModel.wantsInvoice) {
|
||||
Row(verticalAlignment = Alignment.CenterVertically, modifier = Modifier.padding(vertical = Size5dp, horizontal = Size10dp)) {
|
||||
Column(Modifier.fillMaxWidth()) {
|
||||
InvoiceRequest(
|
||||
lud16,
|
||||
user.pubkeyHex,
|
||||
account,
|
||||
stringResource(id = R.string.lightning_invoice),
|
||||
stringResource(id = R.string.lightning_create_and_add_invoice),
|
||||
onSuccess = {
|
||||
postViewModel.message = TextFieldValue(postViewModel.message.text + "\n\n" + it)
|
||||
postViewModel.wantsInvoice = false
|
||||
val url = postViewModel.contentToAddUrl
|
||||
if (url != null) {
|
||||
Row(verticalAlignment = Alignment.CenterVertically, modifier = Modifier.padding(vertical = Size5dp, horizontal = Size10dp)) {
|
||||
ImageVideoDescription(
|
||||
url,
|
||||
account.defaultFileServer,
|
||||
onAdd = { description, server, sensitiveContent ->
|
||||
postViewModel.upload(url, description, sensitiveContent, server, context, relayList)
|
||||
account.changeDefaultFileServer(server)
|
||||
},
|
||||
onClose = {
|
||||
postViewModel.wantsInvoice = false
|
||||
}
|
||||
onCancel = {
|
||||
postViewModel.contentToAddUrl = null
|
||||
},
|
||||
onError = {
|
||||
scope.launch {
|
||||
postViewModel.imageUploadingError.emit(it)
|
||||
}
|
||||
},
|
||||
accountViewModel = accountViewModel
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (lud16 != null && postViewModel.wantsZapraiser) {
|
||||
Row(verticalAlignment = Alignment.CenterVertically, modifier = Modifier.padding(vertical = Size5dp, horizontal = Size10dp)) {
|
||||
ZapRaiserRequest(
|
||||
stringResource(id = R.string.zapraiser),
|
||||
postViewModel
|
||||
)
|
||||
}
|
||||
}
|
||||
val user = postViewModel.account?.userProfile()
|
||||
val lud16 = user?.info?.lnAddress()
|
||||
|
||||
val myUrlPreview = postViewModel.urlPreview
|
||||
if (myUrlPreview != null) {
|
||||
Row(modifier = Modifier.padding(vertical = Size5dp, horizontal = Size10dp)) {
|
||||
if (isValidURL(myUrlPreview)) {
|
||||
val removedParamsFromUrl =
|
||||
myUrlPreview.split("?")[0].lowercase()
|
||||
if (imageExtensions.any { removedParamsFromUrl.endsWith(it) }) {
|
||||
AsyncImage(
|
||||
model = myUrlPreview,
|
||||
contentDescription = myUrlPreview,
|
||||
contentScale = ContentScale.FillWidth,
|
||||
modifier = Modifier
|
||||
.padding(top = 4.dp)
|
||||
.fillMaxWidth()
|
||||
.clip(shape = QuoteBorder)
|
||||
.border(
|
||||
1.dp,
|
||||
MaterialTheme.colors.subtleBorder,
|
||||
QuoteBorder
|
||||
)
|
||||
if (lud16 != null && postViewModel.wantsInvoice) {
|
||||
Row(verticalAlignment = Alignment.CenterVertically, modifier = Modifier.padding(vertical = Size5dp, horizontal = Size10dp)) {
|
||||
Column(Modifier.fillMaxWidth()) {
|
||||
InvoiceRequest(
|
||||
lud16,
|
||||
user.pubkeyHex,
|
||||
account,
|
||||
stringResource(id = R.string.lightning_invoice),
|
||||
stringResource(id = R.string.lightning_create_and_add_invoice),
|
||||
onSuccess = {
|
||||
postViewModel.message = TextFieldValue(postViewModel.message.text + "\n\n" + it)
|
||||
postViewModel.wantsInvoice = false
|
||||
},
|
||||
onClose = {
|
||||
postViewModel.wantsInvoice = false
|
||||
}
|
||||
)
|
||||
} else if (videoExtensions.any { removedParamsFromUrl.endsWith(it) }) {
|
||||
VideoView(myUrlPreview, roundedCorner = true, accountViewModel = accountViewModel)
|
||||
} else {
|
||||
UrlPreview(myUrlPreview, myUrlPreview, accountViewModel)
|
||||
}
|
||||
} else if (startsWithNIP19Scheme(myUrlPreview)) {
|
||||
val bgColor = MaterialTheme.colors.background
|
||||
val backgroundColor = remember {
|
||||
mutableStateOf(bgColor)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
BechLink(
|
||||
myUrlPreview,
|
||||
true,
|
||||
backgroundColor,
|
||||
accountViewModel,
|
||||
nav
|
||||
if (lud16 != null && postViewModel.wantsZapraiser) {
|
||||
Row(verticalAlignment = Alignment.CenterVertically, modifier = Modifier.padding(vertical = Size5dp, horizontal = Size10dp)) {
|
||||
ZapRaiserRequest(
|
||||
stringResource(id = R.string.zapraiser),
|
||||
postViewModel
|
||||
)
|
||||
} else if (noProtocolUrlValidator.matcher(myUrlPreview).matches()) {
|
||||
UrlPreview("https://$myUrlPreview", myUrlPreview, accountViewModel)
|
||||
}
|
||||
}
|
||||
|
||||
val myUrlPreview = postViewModel.urlPreview
|
||||
if (myUrlPreview != null) {
|
||||
Row(modifier = Modifier.padding(vertical = Size5dp, horizontal = Size10dp)) {
|
||||
if (isValidURL(myUrlPreview)) {
|
||||
val removedParamsFromUrl =
|
||||
myUrlPreview.split("?")[0].lowercase()
|
||||
if (imageExtensions.any { removedParamsFromUrl.endsWith(it) }) {
|
||||
AsyncImage(
|
||||
model = myUrlPreview,
|
||||
contentDescription = myUrlPreview,
|
||||
contentScale = ContentScale.FillWidth,
|
||||
modifier = Modifier
|
||||
.padding(top = 4.dp)
|
||||
.fillMaxWidth()
|
||||
.clip(shape = QuoteBorder)
|
||||
.border(
|
||||
1.dp,
|
||||
MaterialTheme.colors.subtleBorder,
|
||||
QuoteBorder
|
||||
)
|
||||
)
|
||||
} else if (videoExtensions.any { removedParamsFromUrl.endsWith(it) }) {
|
||||
VideoView(myUrlPreview, roundedCorner = true, accountViewModel = accountViewModel)
|
||||
} else {
|
||||
UrlPreview(myUrlPreview, myUrlPreview, accountViewModel)
|
||||
}
|
||||
} else if (startsWithNIP19Scheme(myUrlPreview)) {
|
||||
val bgColor = MaterialTheme.colors.background
|
||||
val backgroundColor = remember {
|
||||
mutableStateOf(bgColor)
|
||||
}
|
||||
|
||||
BechLink(
|
||||
myUrlPreview,
|
||||
true,
|
||||
backgroundColor,
|
||||
accountViewModel,
|
||||
nav
|
||||
)
|
||||
} else if (noProtocolUrlValidator.matcher(myUrlPreview).matches()) {
|
||||
UrlPreview("https://$myUrlPreview", myUrlPreview, accountViewModel)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
val userSuggestions = postViewModel.userSuggestions
|
||||
if (userSuggestions.isNotEmpty()) {
|
||||
LazyColumn(
|
||||
contentPadding = PaddingValues(
|
||||
top = 10.dp
|
||||
),
|
||||
modifier = Modifier.heightIn(0.dp, 300.dp)
|
||||
) {
|
||||
itemsIndexed(
|
||||
userSuggestions,
|
||||
key = { _, item -> item.pubkeyHex }
|
||||
) { _, item ->
|
||||
UserLine(item, accountViewModel) {
|
||||
postViewModel.autocompleteWithUser(item)
|
||||
val userSuggestions = postViewModel.userSuggestions
|
||||
if (userSuggestions.isNotEmpty()) {
|
||||
LazyColumn(
|
||||
contentPadding = PaddingValues(
|
||||
top = 10.dp
|
||||
),
|
||||
modifier = Modifier.heightIn(0.dp, 300.dp)
|
||||
) {
|
||||
itemsIndexed(
|
||||
userSuggestions,
|
||||
key = { _, item -> item.pubkeyHex }
|
||||
) { _, item ->
|
||||
UserLine(item, accountViewModel) {
|
||||
postViewModel.autocompleteWithUser(item)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.height(50.dp),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
UploadFromGallery(
|
||||
isUploading = postViewModel.isUploadingImage,
|
||||
tint = MaterialTheme.colors.onBackground,
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.height(50.dp),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
postViewModel.selectImage(it)
|
||||
}
|
||||
|
||||
if (postViewModel.canUsePoll) {
|
||||
// These should be hashtag recommendations the user selects in the future.
|
||||
// val hashtag = stringResource(R.string.poll_hashtag)
|
||||
// postViewModel.includePollHashtagInMessage(postViewModel.wantsPoll, hashtag)
|
||||
AddPollButton(postViewModel.wantsPoll) {
|
||||
postViewModel.wantsPoll = !postViewModel.wantsPoll
|
||||
UploadFromGallery(
|
||||
isUploading = postViewModel.isUploadingImage,
|
||||
tint = MaterialTheme.colors.onBackground,
|
||||
modifier = Modifier
|
||||
) {
|
||||
postViewModel.selectImage(it)
|
||||
}
|
||||
}
|
||||
|
||||
if (postViewModel.canAddInvoice) {
|
||||
AddLnInvoiceButton(postViewModel.wantsInvoice) {
|
||||
postViewModel.wantsInvoice = !postViewModel.wantsInvoice
|
||||
if (postViewModel.canUsePoll) {
|
||||
// These should be hashtag recommendations the user selects in the future.
|
||||
// val hashtag = stringResource(R.string.poll_hashtag)
|
||||
// postViewModel.includePollHashtagInMessage(postViewModel.wantsPoll, hashtag)
|
||||
AddPollButton(postViewModel.wantsPoll) {
|
||||
postViewModel.wantsPoll = !postViewModel.wantsPoll
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (postViewModel.canAddZapRaiser) {
|
||||
AddZapraiserButton(postViewModel.wantsZapraiser) {
|
||||
postViewModel.wantsZapraiser = !postViewModel.wantsZapraiser
|
||||
if (postViewModel.canAddInvoice) {
|
||||
AddLnInvoiceButton(postViewModel.wantsInvoice) {
|
||||
postViewModel.wantsInvoice = !postViewModel.wantsInvoice
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
MarkAsSensitive(postViewModel) {
|
||||
postViewModel.wantsToMarkAsSensitive = !postViewModel.wantsToMarkAsSensitive
|
||||
}
|
||||
if (postViewModel.canAddZapRaiser) {
|
||||
AddZapraiserButton(postViewModel.wantsZapraiser) {
|
||||
postViewModel.wantsZapraiser = !postViewModel.wantsZapraiser
|
||||
}
|
||||
}
|
||||
|
||||
AddGeoHash(postViewModel) {
|
||||
postViewModel.wantsToAddGeoHash = !postViewModel.wantsToAddGeoHash
|
||||
}
|
||||
MarkAsSensitive(postViewModel) {
|
||||
postViewModel.wantsToMarkAsSensitive = !postViewModel.wantsToMarkAsSensitive
|
||||
}
|
||||
|
||||
ForwardZapTo(postViewModel) {
|
||||
postViewModel.wantsForwardZapTo = !postViewModel.wantsForwardZapTo
|
||||
AddGeoHash(postViewModel) {
|
||||
postViewModel.wantsToAddGeoHash = !postViewModel.wantsToAddGeoHash
|
||||
}
|
||||
|
||||
ForwardZapTo(postViewModel) {
|
||||
postViewModel.wantsForwardZapTo = !postViewModel.wantsForwardZapTo
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1183,10 +1208,10 @@ private fun MarkAsSensitive(
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun CloseButton(onCancel: () -> Unit) {
|
||||
fun CloseButton(onPress: () -> Unit) {
|
||||
Button(
|
||||
onClick = {
|
||||
onCancel()
|
||||
onPress()
|
||||
},
|
||||
shape = ButtonBorder,
|
||||
colors = ButtonDefaults
|
||||
|
@ -157,7 +157,7 @@ fun NewRelayListView(onClose: () -> Unit, accountViewModel: AccountViewModel, re
|
||||
},
|
||||
navigationIcon = {
|
||||
Spacer(modifier = StdHorzSpacer)
|
||||
CloseButton(onCancel = {
|
||||
CloseButton(onPress = {
|
||||
postViewModel.clear()
|
||||
onClose()
|
||||
})
|
||||
|
@ -97,7 +97,7 @@ fun NewUserMetadataView(onClose: () -> Unit, account: Account) {
|
||||
horizontalArrangement = Arrangement.SpaceBetween,
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
CloseButton(onCancel = {
|
||||
CloseButton(onPress = {
|
||||
postViewModel.clear()
|
||||
onClose()
|
||||
})
|
||||
|
@ -74,7 +74,7 @@ fun RelayInformationDialog(
|
||||
horizontalArrangement = Arrangement.SpaceBetween,
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
CloseButton(onCancel = {
|
||||
CloseButton(onPress = {
|
||||
onClose()
|
||||
})
|
||||
}
|
||||
|
@ -110,7 +110,7 @@ fun RelaySelectionDialog(
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
CloseButton(
|
||||
onCancel = {
|
||||
onPress = {
|
||||
onClose()
|
||||
}
|
||||
)
|
||||
|
@ -30,8 +30,6 @@ import androidx.compose.ui.unit.Constraints
|
||||
import androidx.compose.ui.unit.Dp
|
||||
import androidx.compose.ui.unit.LayoutDirection
|
||||
import androidx.compose.ui.unit.em
|
||||
import androidx.lifecycle.distinctUntilChanged
|
||||
import androidx.lifecycle.map
|
||||
import com.halilibo.richtext.markdown.Markdown
|
||||
import com.halilibo.richtext.markdown.MarkdownParseOptions
|
||||
import com.halilibo.richtext.ui.material.MaterialRichText
|
||||
@ -421,9 +419,7 @@ private fun ObserveNIP19Event(
|
||||
|
||||
@Composable
|
||||
fun ObserveNote(note: Note, onRefresh: () -> Unit) {
|
||||
val loadedNoteId by note.live().metadata.map {
|
||||
it.note.event?.id()
|
||||
}.distinctUntilChanged().observeAsState(note.event?.id())
|
||||
val loadedNoteId by note.live().metadata.observeAsState()
|
||||
|
||||
LaunchedEffect(key1 = loadedNoteId) {
|
||||
if (loadedNoteId != null) {
|
||||
@ -460,9 +456,7 @@ private fun ObserveNIP19User(
|
||||
|
||||
@Composable
|
||||
private fun ObserveUser(user: User, onRefresh: () -> Unit) {
|
||||
val loadedUserMetaId by user.live().metadata.map {
|
||||
it.user.info?.latestMetadata?.id
|
||||
}.distinctUntilChanged().observeAsState(user.info?.latestMetadata?.id)
|
||||
val loadedUserMetaId by user.live().metadata.observeAsState()
|
||||
|
||||
LaunchedEffect(key1 = loadedUserMetaId) {
|
||||
if (loadedUserMetaId != null) {
|
||||
@ -854,9 +848,7 @@ private fun DisplayUserFromTag(
|
||||
val route = remember { "User/${baseUser.pubkeyHex}" }
|
||||
val hex = remember { baseUser.pubkeyDisplayHex() }
|
||||
|
||||
val meta by baseUser.live().metadata.map {
|
||||
it.user.info
|
||||
}.distinctUntilChanged().observeAsState(baseUser.info)
|
||||
val meta by baseUser.live().userMetadataInfo.observeAsState(baseUser.info)
|
||||
|
||||
Crossfade(targetState = meta) {
|
||||
Row() {
|
||||
|
@ -663,7 +663,7 @@ fun ZoomableImageDialog(
|
||||
horizontalArrangement = Arrangement.SpaceBetween,
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
CloseButton(onCancel = onDismiss)
|
||||
CloseButton(onPress = onDismiss)
|
||||
|
||||
val myContent = allImages[pagerState.currentPage]
|
||||
if (myContent is ZoomableUrlContent) {
|
||||
|
@ -19,6 +19,7 @@ import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.layout.width
|
||||
import androidx.compose.foundation.rememberScrollState
|
||||
import androidx.compose.foundation.shape.CircleShape
|
||||
import androidx.compose.foundation.text.KeyboardOptions
|
||||
import androidx.compose.foundation.verticalScroll
|
||||
import androidx.compose.material.AlertDialog
|
||||
import androidx.compose.material.Divider
|
||||
@ -27,13 +28,18 @@ import androidx.compose.material.Icon
|
||||
import androidx.compose.material.IconButton
|
||||
import androidx.compose.material.MaterialTheme
|
||||
import androidx.compose.material.ModalBottomSheetState
|
||||
import androidx.compose.material.OutlinedTextField
|
||||
import androidx.compose.material.ScaffoldState
|
||||
import androidx.compose.material.Surface
|
||||
import androidx.compose.material.Text
|
||||
import androidx.compose.material.TextButton
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.Delete
|
||||
import androidx.compose.material.icons.filled.Send
|
||||
import androidx.compose.runtime.Composable
|
||||
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
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
@ -46,13 +52,15 @@ import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.layout.ContentScale
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.platform.LocalFocusManager
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.text.input.KeyboardCapitalization
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import androidx.lifecycle.distinctUntilChanged
|
||||
import androidx.lifecycle.map
|
||||
import androidx.lifecycle.viewmodel.compose.viewModel
|
||||
import coil.compose.AsyncImage
|
||||
import com.vitorpamplona.amethyst.BuildConfig
|
||||
@ -65,12 +73,16 @@ import com.vitorpamplona.amethyst.service.HttpClient
|
||||
import com.vitorpamplona.amethyst.ui.actions.NewRelayListView
|
||||
import com.vitorpamplona.amethyst.ui.components.CreateTextWithEmoji
|
||||
import com.vitorpamplona.amethyst.ui.components.RobohashAsyncImageProxy
|
||||
import com.vitorpamplona.amethyst.ui.note.LoadStatuses
|
||||
import com.vitorpamplona.amethyst.ui.screen.RelayPoolViewModel
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountBackupDialog
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.ConnectOrbotDialog
|
||||
import com.vitorpamplona.amethyst.ui.theme.DoubleHorzSpacer
|
||||
import com.vitorpamplona.amethyst.ui.theme.Size10dp
|
||||
import com.vitorpamplona.amethyst.ui.theme.Size16dp
|
||||
import com.vitorpamplona.amethyst.ui.theme.Size20Modifier
|
||||
import com.vitorpamplona.amethyst.ui.theme.Size26Modifier
|
||||
import com.vitorpamplona.amethyst.ui.theme.placeholderText
|
||||
import com.vitorpamplona.quartz.events.toImmutableListOfLists
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
@ -97,6 +109,7 @@ fun DrawerContent(
|
||||
.padding(horizontal = 25.dp)
|
||||
.padding(top = 100.dp),
|
||||
scaffoldState,
|
||||
accountViewModel,
|
||||
nav
|
||||
)
|
||||
Divider(
|
||||
@ -123,6 +136,7 @@ fun ProfileContent(
|
||||
baseAccountUser: User,
|
||||
modifier: Modifier = Modifier,
|
||||
scaffoldState: ScaffoldState,
|
||||
accountViewModel: AccountViewModel,
|
||||
nav: (String) -> Unit
|
||||
) {
|
||||
val coroutineScope = rememberCoroutineScope()
|
||||
@ -215,9 +229,14 @@ fun ProfileContent(
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
Row(Modifier.padding(top = Size10dp)) {
|
||||
EditStatusBox(baseAccountUser, accountViewModel)
|
||||
}
|
||||
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.padding(top = 15.dp)
|
||||
.padding(top = Size10dp)
|
||||
.clickable(onClick = {
|
||||
nav(route)
|
||||
coroutineScope.launch {
|
||||
@ -231,6 +250,132 @@ fun ProfileContent(
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun EditStatusBox(baseAccountUser: User, accountViewModel: AccountViewModel) {
|
||||
val scope = rememberCoroutineScope()
|
||||
val focusManager = LocalFocusManager.current
|
||||
|
||||
LoadStatuses(user = baseAccountUser) { statuses ->
|
||||
if (statuses.isEmpty()) {
|
||||
val currentStatus = remember {
|
||||
mutableStateOf("")
|
||||
}
|
||||
val hasChanged by remember {
|
||||
derivedStateOf {
|
||||
currentStatus.value != ""
|
||||
}
|
||||
}
|
||||
|
||||
OutlinedTextField(
|
||||
value = currentStatus.value,
|
||||
onValueChange = { currentStatus.value = it },
|
||||
label = { Text(text = stringResource(R.string.status_update)) },
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
placeholder = {
|
||||
Text(
|
||||
text = stringResource(R.string.status_update),
|
||||
color = MaterialTheme.colors.placeholderText
|
||||
)
|
||||
},
|
||||
keyboardOptions = KeyboardOptions.Default.copy(
|
||||
capitalization = KeyboardCapitalization.Sentences
|
||||
),
|
||||
singleLine = true,
|
||||
trailingIcon = {
|
||||
if (hasChanged) {
|
||||
UserStatusSendButton() {
|
||||
scope.launch(Dispatchers.IO) {
|
||||
accountViewModel.createStatus(currentStatus.value)
|
||||
focusManager.clearFocus(true)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
} else {
|
||||
statuses.forEach {
|
||||
val originalStatus by it.live().metadata.map {
|
||||
it.note.event?.content() ?: ""
|
||||
}.observeAsState(it.event?.content() ?: "")
|
||||
|
||||
val thisStatus = remember {
|
||||
mutableStateOf(originalStatus)
|
||||
}
|
||||
val hasChanged by remember {
|
||||
derivedStateOf {
|
||||
thisStatus.value != originalStatus
|
||||
}
|
||||
}
|
||||
|
||||
OutlinedTextField(
|
||||
value = thisStatus.value,
|
||||
onValueChange = { thisStatus.value = it },
|
||||
label = { Text(text = stringResource(R.string.status_update)) },
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
placeholder = {
|
||||
Text(
|
||||
text = stringResource(R.string.status_update),
|
||||
color = MaterialTheme.colors.placeholderText
|
||||
)
|
||||
},
|
||||
keyboardOptions = KeyboardOptions.Default.copy(
|
||||
capitalization = KeyboardCapitalization.Sentences
|
||||
),
|
||||
singleLine = true,
|
||||
trailingIcon = {
|
||||
if (hasChanged) {
|
||||
UserStatusSendButton() {
|
||||
scope.launch(Dispatchers.IO) {
|
||||
accountViewModel.updateStatus(it, thisStatus.value)
|
||||
focusManager.clearFocus(true)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
UserStatusDeleteButton() {
|
||||
scope.launch(Dispatchers.IO) {
|
||||
accountViewModel.updateStatus(it, "")
|
||||
accountViewModel.delete(it)
|
||||
focusManager.clearFocus(true)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun UserStatusSendButton(onClick: () -> Unit) {
|
||||
IconButton(
|
||||
modifier = Size26Modifier,
|
||||
onClick = onClick
|
||||
) {
|
||||
Icon(
|
||||
imageVector = Icons.Default.Send,
|
||||
null,
|
||||
modifier = Size20Modifier,
|
||||
tint = MaterialTheme.colors.placeholderText
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun UserStatusDeleteButton(onClick: () -> Unit) {
|
||||
IconButton(
|
||||
modifier = Size26Modifier,
|
||||
onClick = onClick
|
||||
) {
|
||||
Icon(
|
||||
imageVector = Icons.Default.Delete,
|
||||
null,
|
||||
modifier = Size20Modifier,
|
||||
tint = MaterialTheme.colors.placeholderText
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun FollowingAndFollowerCounts(baseAccountUser: User) {
|
||||
var followingCount by remember { mutableStateOf("--") }
|
||||
@ -484,7 +629,7 @@ private fun RelayStatus(
|
||||
relayViewModel: RelayPoolViewModel
|
||||
) {
|
||||
val connectedRelaysText by relayViewModel.connectionStatus.observeAsState("--/--")
|
||||
val isConnected by relayViewModel.isConnected.distinctUntilChanged().observeAsState(false)
|
||||
val isConnected by relayViewModel.isConnected.observeAsState(false)
|
||||
|
||||
RenderRelayStatus(connectedRelaysText, isConnected)
|
||||
}
|
||||
|
@ -95,7 +95,7 @@ fun AddBountyAmountDialog(bounty: Note, accountViewModel: AccountViewModel, onCl
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
) {
|
||||
CloseButton(onCancel = {
|
||||
CloseButton(onPress = {
|
||||
postViewModel.cancel()
|
||||
onClose()
|
||||
})
|
||||
|
@ -93,24 +93,10 @@ fun ChannelCardCompose(
|
||||
accountViewModel: AccountViewModel,
|
||||
nav: (String) -> Unit
|
||||
) {
|
||||
val isBlank by baseNote.live().metadata.map {
|
||||
it.note.event == null
|
||||
}.distinctUntilChanged().observeAsState(baseNote.event == null)
|
||||
val hasEvent by baseNote.live().hasEvent.observeAsState(baseNote.event != null)
|
||||
|
||||
Crossfade(targetState = isBlank) {
|
||||
Crossfade(targetState = hasEvent) {
|
||||
if (it) {
|
||||
LongPressToQuickAction(baseNote = baseNote, accountViewModel = accountViewModel) { showPopup ->
|
||||
BlankNote(
|
||||
remember {
|
||||
modifier.combinedClickable(
|
||||
onClick = { },
|
||||
onLongClick = showPopup
|
||||
)
|
||||
},
|
||||
false
|
||||
)
|
||||
}
|
||||
} else {
|
||||
if (forceEventKind == null || baseNote.event?.kind() == forceEventKind) {
|
||||
CheckHiddenChannelCardCompose(
|
||||
baseNote,
|
||||
@ -122,6 +108,18 @@ fun ChannelCardCompose(
|
||||
nav
|
||||
)
|
||||
}
|
||||
} else {
|
||||
LongPressToQuickAction(baseNote = baseNote, accountViewModel = accountViewModel) { showPopup ->
|
||||
BlankNote(
|
||||
remember {
|
||||
modifier.combinedClickable(
|
||||
onClick = { },
|
||||
onLongClick = showPopup
|
||||
)
|
||||
},
|
||||
false
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -77,14 +77,12 @@ fun ChatroomHeaderCompose(
|
||||
accountViewModel: AccountViewModel,
|
||||
nav: (String) -> Unit
|
||||
) {
|
||||
val isBlank by baseNote.live().metadata.map {
|
||||
it.note.event == null
|
||||
}.observeAsState(baseNote.event == null)
|
||||
val hasEvent by baseNote.live().hasEvent.observeAsState(baseNote.event != null)
|
||||
|
||||
if (isBlank) {
|
||||
BlankNote(Modifier)
|
||||
} else {
|
||||
if (hasEvent) {
|
||||
ChatroomComposeChannelOrUser(baseNote, accountViewModel, nav)
|
||||
} else {
|
||||
BlankNote(Modifier)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -90,12 +90,20 @@ fun ChatroomMessageCompose(
|
||||
nav: (String) -> Unit,
|
||||
onWantsToReply: (Note) -> Unit
|
||||
) {
|
||||
val isBlank by baseNote.live().metadata.map {
|
||||
it.note.event == null
|
||||
}.observeAsState(baseNote.event == null)
|
||||
val hasEvent by baseNote.live().hasEvent.observeAsState(baseNote.event != null)
|
||||
|
||||
Crossfade(targetState = isBlank) {
|
||||
Crossfade(targetState = hasEvent) {
|
||||
if (it) {
|
||||
CheckHiddenChatMessage(
|
||||
baseNote,
|
||||
routeForLastRead,
|
||||
innerQuote,
|
||||
parentBackgroundColor,
|
||||
accountViewModel,
|
||||
nav,
|
||||
onWantsToReply
|
||||
)
|
||||
} else {
|
||||
LongPressToQuickAction(baseNote = baseNote, accountViewModel = accountViewModel) { showPopup ->
|
||||
BlankNote(
|
||||
remember {
|
||||
@ -106,16 +114,6 @@ fun ChatroomMessageCompose(
|
||||
}
|
||||
)
|
||||
}
|
||||
} else {
|
||||
CheckHiddenChatMessage(
|
||||
baseNote,
|
||||
routeForLastRead,
|
||||
innerQuote,
|
||||
parentBackgroundColor,
|
||||
accountViewModel,
|
||||
nav,
|
||||
onWantsToReply
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -41,8 +41,6 @@ import androidx.compose.ui.text.SpanStyle
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import androidx.lifecycle.distinctUntilChanged
|
||||
import androidx.lifecycle.map
|
||||
import com.vitorpamplona.amethyst.R
|
||||
import com.vitorpamplona.amethyst.model.LocalCache
|
||||
import com.vitorpamplona.amethyst.model.Note
|
||||
@ -579,9 +577,7 @@ private fun WatchNoteAuthor(
|
||||
baseNote: Note,
|
||||
onContent: @Composable (User?) -> Unit
|
||||
) {
|
||||
val author by baseNote.live().metadata.map {
|
||||
it.note.author
|
||||
}.observeAsState(baseNote.author)
|
||||
val author by baseNote.live().authorChanges.observeAsState(baseNote.author)
|
||||
|
||||
onContent(author)
|
||||
}
|
||||
@ -591,9 +587,7 @@ private fun WatchUserMetadata(
|
||||
author: User,
|
||||
onNewMetadata: @Composable (String?) -> Unit
|
||||
) {
|
||||
val userProfile by author.live().metadata.map {
|
||||
it.user.profilePicture()
|
||||
}.distinctUntilChanged().observeAsState(author.profilePicture())
|
||||
val userProfile by author.live().profilePictureChanges.observeAsState(author.profilePicture())
|
||||
|
||||
onNewMetadata(userProfile)
|
||||
}
|
||||
|
@ -5,38 +5,58 @@ import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.text.ClickableText
|
||||
import androidx.compose.material.Icon
|
||||
import androidx.compose.material.IconButton
|
||||
import androidx.compose.material.LocalTextStyle
|
||||
import androidx.compose.material.MaterialTheme
|
||||
import androidx.compose.material.Text
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.OpenInNew
|
||||
import androidx.compose.runtime.Composable
|
||||
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
|
||||
import androidx.compose.runtime.mutableIntStateOf
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.platform.LocalUriHandler
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.text.AnnotatedString
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.lifecycle.map
|
||||
import androidx.compose.ui.unit.sp
|
||||
import com.vitorpamplona.amethyst.R
|
||||
import com.vitorpamplona.amethyst.model.AddressableNote
|
||||
import com.vitorpamplona.amethyst.model.Note
|
||||
import com.vitorpamplona.amethyst.model.User
|
||||
import com.vitorpamplona.amethyst.service.Nip05Verifier
|
||||
import com.vitorpamplona.amethyst.ui.note.LoadAddressableNote
|
||||
import com.vitorpamplona.amethyst.ui.note.LoadStatuses
|
||||
import com.vitorpamplona.amethyst.ui.note.NIP05CheckingIcon
|
||||
import com.vitorpamplona.amethyst.ui.note.NIP05FailedVerification
|
||||
import com.vitorpamplona.amethyst.ui.note.NIP05VerifiedIcon
|
||||
import com.vitorpamplona.amethyst.ui.note.routeFor
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
||||
import com.vitorpamplona.amethyst.ui.theme.Font14SP
|
||||
import com.vitorpamplona.amethyst.ui.theme.NIP05IconSize
|
||||
import com.vitorpamplona.amethyst.ui.theme.Size15Modifier
|
||||
import com.vitorpamplona.amethyst.ui.theme.Size16Modifier
|
||||
import com.vitorpamplona.amethyst.ui.theme.Size18Modifier
|
||||
import com.vitorpamplona.amethyst.ui.theme.lessImportantLink
|
||||
import com.vitorpamplona.amethyst.ui.theme.nip05
|
||||
import com.vitorpamplona.amethyst.ui.theme.placeholderText
|
||||
import com.vitorpamplona.quartz.events.AddressableEvent
|
||||
import com.vitorpamplona.quartz.events.UserMetadata
|
||||
import com.vitorpamplona.quartz.utils.TimeUtils
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlin.time.Duration.Companion.seconds
|
||||
|
||||
@Composable
|
||||
fun nip05VerificationAsAState(userMetadata: UserMetadata, pubkeyHex: String): MutableState<Boolean?> {
|
||||
@ -93,47 +113,193 @@ fun nip05VerificationAsAState(userMetadata: UserMetadata, pubkeyHex: String): Mu
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun ObserveDisplayNip05Status(baseNote: Note, columnModifier: Modifier = Modifier) {
|
||||
val noteState by baseNote.live().metadata.observeAsState()
|
||||
val author by remember(noteState) {
|
||||
derivedStateOf {
|
||||
noteState?.note?.author
|
||||
}
|
||||
}
|
||||
fun ObserveDisplayNip05Status(
|
||||
baseNote: Note,
|
||||
columnModifier: Modifier = Modifier,
|
||||
accountViewModel: AccountViewModel,
|
||||
nav: (String) -> Unit
|
||||
) {
|
||||
val author by baseNote.live().authorChanges.observeAsState()
|
||||
|
||||
author?.let {
|
||||
ObserveDisplayNip05Status(it, columnModifier)
|
||||
ObserveDisplayNip05Status(it, columnModifier, accountViewModel, nav)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun ObserveDisplayNip05Status(baseUser: User, columnModifier: Modifier = Modifier) {
|
||||
val nip05 by baseUser.live().metadata.map {
|
||||
it.user.nip05()
|
||||
}.observeAsState(baseUser.nip05())
|
||||
fun ObserveDisplayNip05Status(
|
||||
baseUser: User,
|
||||
columnModifier: Modifier = Modifier,
|
||||
accountViewModel: AccountViewModel,
|
||||
nav: (String) -> Unit
|
||||
) {
|
||||
val nip05 by baseUser.live().nip05Changes.observeAsState(baseUser.nip05())
|
||||
|
||||
Crossfade(targetState = nip05, modifier = columnModifier) {
|
||||
if (it != null) {
|
||||
DisplayNIP05Line(it, baseUser, columnModifier)
|
||||
} else {
|
||||
Text(
|
||||
text = baseUser.pubkeyDisplayHex(),
|
||||
color = MaterialTheme.colors.placeholderText,
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
modifier = columnModifier
|
||||
)
|
||||
LoadStatuses(baseUser) { statuses ->
|
||||
Crossfade(targetState = nip05, modifier = columnModifier, label = "ObserveDisplayNip05StatusCrossfade") {
|
||||
VerifyAndDisplayNIP05OrStatusLine(it, statuses, baseUser, columnModifier, accountViewModel, nav)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun DisplayNIP05Line(nip05: String, baseUser: User, columnModifier: Modifier = Modifier) {
|
||||
private fun VerifyAndDisplayNIP05OrStatusLine(
|
||||
nip05: String?,
|
||||
statuses: ImmutableList<AddressableNote>,
|
||||
baseUser: User,
|
||||
columnModifier: Modifier = Modifier,
|
||||
accountViewModel: AccountViewModel,
|
||||
nav: (String) -> Unit
|
||||
) {
|
||||
Column(modifier = columnModifier) {
|
||||
val nip05Verified = nip05VerificationAsAState(baseUser.info!!, baseUser.pubkeyHex)
|
||||
Crossfade(targetState = nip05Verified) {
|
||||
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||
DisplayNIP05(nip05, it)
|
||||
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||
if (nip05 != null) {
|
||||
val nip05Verified = nip05VerificationAsAState(baseUser.info!!, baseUser.pubkeyHex)
|
||||
|
||||
if (nip05Verified.value != true) {
|
||||
DisplayNIP05(nip05, nip05Verified)
|
||||
} else if (!statuses.isEmpty()) {
|
||||
RotateStatuses(statuses, accountViewModel, nav)
|
||||
} else {
|
||||
DisplayNIP05(nip05, nip05Verified)
|
||||
}
|
||||
} else {
|
||||
if (!statuses.isEmpty()) {
|
||||
RotateStatuses(statuses, accountViewModel, nav)
|
||||
} else {
|
||||
DisplayUsersNpub(baseUser.pubkeyDisplayHex())
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun RotateStatuses(
|
||||
statuses: ImmutableList<AddressableNote>,
|
||||
accountViewModel: AccountViewModel,
|
||||
nav: (String) -> Unit
|
||||
) {
|
||||
var indexToDisplay by remember {
|
||||
mutableIntStateOf(0)
|
||||
}
|
||||
|
||||
DisplayStatus(statuses[indexToDisplay], accountViewModel, nav)
|
||||
|
||||
if (statuses.size > 1) {
|
||||
LaunchedEffect(Unit) {
|
||||
while (true) {
|
||||
delay(10.seconds)
|
||||
indexToDisplay = ((indexToDisplay + 1) % (statuses.size + 1))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun DisplayUsersNpub(npub: String) {
|
||||
Text(
|
||||
text = npub,
|
||||
fontSize = 14.sp,
|
||||
color = MaterialTheme.colors.placeholderText,
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun DisplayStatus(
|
||||
addressableNote: AddressableNote,
|
||||
accountViewModel: AccountViewModel,
|
||||
nav: (String) -> Unit
|
||||
) {
|
||||
val noteState by addressableNote.live().metadata.observeAsState()
|
||||
|
||||
val content = remember(noteState) { addressableNote.event?.content() ?: "" }
|
||||
val type = remember(noteState) {
|
||||
(addressableNote.event as? AddressableEvent)?.dTag() ?: ""
|
||||
}
|
||||
val url = remember(noteState) {
|
||||
addressableNote.event?.firstTaggedUrl()?.ifBlank { null }
|
||||
}
|
||||
val nostrATag = remember(noteState) {
|
||||
addressableNote.event?.firstTaggedAddress()
|
||||
}
|
||||
val nostrHexID = remember(noteState) {
|
||||
addressableNote.event?.firstTaggedEvent()?.ifBlank { null }
|
||||
}
|
||||
|
||||
when (type) {
|
||||
"music" -> Icon(
|
||||
painter = painterResource(id = R.drawable.tunestr),
|
||||
null,
|
||||
modifier = Size18Modifier,
|
||||
tint = MaterialTheme.colors.placeholderText
|
||||
)
|
||||
else -> {}
|
||||
}
|
||||
|
||||
Text(
|
||||
text = content,
|
||||
fontSize = Font14SP,
|
||||
color = MaterialTheme.colors.placeholderText,
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis
|
||||
)
|
||||
|
||||
if (url != null) {
|
||||
val uri = LocalUriHandler.current
|
||||
IconButton(
|
||||
modifier = Size15Modifier,
|
||||
onClick = { runCatching { uri.openUri(url.trim()) } }
|
||||
) {
|
||||
Icon(
|
||||
imageVector = Icons.Default.OpenInNew,
|
||||
null,
|
||||
modifier = Size15Modifier,
|
||||
tint = MaterialTheme.colors.lessImportantLink
|
||||
)
|
||||
}
|
||||
} else if (nostrATag != null) {
|
||||
LoadAddressableNote(nostrATag) { note ->
|
||||
if (note != null) {
|
||||
IconButton(
|
||||
modifier = Size15Modifier,
|
||||
onClick = {
|
||||
routeFor(
|
||||
note,
|
||||
accountViewModel.userProfile()
|
||||
)?.let { nav(it) }
|
||||
}
|
||||
) {
|
||||
Icon(
|
||||
imageVector = Icons.Default.OpenInNew,
|
||||
null,
|
||||
modifier = Size15Modifier,
|
||||
tint = MaterialTheme.colors.lessImportantLink
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if (nostrHexID != null) {
|
||||
LoadNote(baseNoteHex = nostrHexID) {
|
||||
if (it != null) {
|
||||
IconButton(
|
||||
modifier = Size15Modifier,
|
||||
onClick = {
|
||||
routeFor(
|
||||
it,
|
||||
accountViewModel.userProfile()
|
||||
)?.let { nav(it) }
|
||||
}
|
||||
) {
|
||||
Icon(
|
||||
imageVector = Icons.Default.OpenInNew,
|
||||
null,
|
||||
modifier = Size15Modifier,
|
||||
tint = MaterialTheme.colors.lessImportantLink
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -157,6 +323,7 @@ private fun DisplayNIP05(
|
||||
if (user != "_") {
|
||||
Text(
|
||||
text = remember(nip05) { AnnotatedString(user) },
|
||||
fontSize = Font14SP,
|
||||
color = MaterialTheme.colors.nip05,
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis
|
||||
@ -168,7 +335,7 @@ private fun DisplayNIP05(
|
||||
ClickableText(
|
||||
text = remember(nip05) { AnnotatedString(domain) },
|
||||
onClick = { runCatching { uri.openUri("https://$domain") } },
|
||||
style = LocalTextStyle.current.copy(color = MaterialTheme.colors.nip05),
|
||||
style = LocalTextStyle.current.copy(color = MaterialTheme.colors.nip05, fontSize = Font14SP),
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Visible
|
||||
)
|
||||
|
@ -124,7 +124,6 @@ import com.vitorpamplona.amethyst.ui.theme.DoubleVertSpacer
|
||||
import com.vitorpamplona.amethyst.ui.theme.Font14SP
|
||||
import com.vitorpamplona.amethyst.ui.theme.HalfPadding
|
||||
import com.vitorpamplona.amethyst.ui.theme.HalfStartPadding
|
||||
import com.vitorpamplona.amethyst.ui.theme.HalfVertSpacer
|
||||
import com.vitorpamplona.amethyst.ui.theme.HeaderPictureModifier
|
||||
import com.vitorpamplona.amethyst.ui.theme.QuoteBorder
|
||||
import com.vitorpamplona.amethyst.ui.theme.Size10dp
|
||||
@ -224,24 +223,10 @@ fun NoteCompose(
|
||||
accountViewModel: AccountViewModel,
|
||||
nav: (String) -> Unit
|
||||
) {
|
||||
val isBlank by baseNote.live().metadata.map {
|
||||
it.note.event == null
|
||||
}.distinctUntilChanged().observeAsState(baseNote.event == null)
|
||||
val hasEvent by baseNote.live().hasEvent.observeAsState(baseNote.event != null)
|
||||
|
||||
Crossfade(targetState = isBlank) {
|
||||
Crossfade(targetState = hasEvent) {
|
||||
if (it) {
|
||||
LongPressToQuickAction(baseNote = baseNote, accountViewModel = accountViewModel) { showPopup ->
|
||||
BlankNote(
|
||||
remember {
|
||||
modifier.combinedClickable(
|
||||
onClick = { },
|
||||
onLongClick = showPopup
|
||||
)
|
||||
},
|
||||
isBoostedNote || isQuotedNote
|
||||
)
|
||||
}
|
||||
} else {
|
||||
CheckHiddenNoteCompose(
|
||||
note = baseNote,
|
||||
routeForLastRead = routeForLastRead,
|
||||
@ -256,6 +241,18 @@ fun NoteCompose(
|
||||
accountViewModel = accountViewModel,
|
||||
nav = nav
|
||||
)
|
||||
} else {
|
||||
LongPressToQuickAction(baseNote = baseNote, accountViewModel = accountViewModel) { showPopup ->
|
||||
BlankNote(
|
||||
remember {
|
||||
modifier.combinedClickable(
|
||||
onClick = { },
|
||||
onLongClick = showPopup
|
||||
)
|
||||
},
|
||||
isBoostedNote || isQuotedNote
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -465,45 +462,86 @@ fun NormalNote(
|
||||
accountViewModel: AccountViewModel,
|
||||
nav: (String) -> Unit
|
||||
) {
|
||||
when (baseNote.event) {
|
||||
is ChannelCreateEvent, is ChannelMetadataEvent -> ChannelHeader(
|
||||
channelNote = baseNote,
|
||||
showVideo = !makeItShort,
|
||||
showBottomDiviser = true,
|
||||
sendToChannel = true,
|
||||
accountViewModel = accountViewModel,
|
||||
nav = nav
|
||||
)
|
||||
is CommunityDefinitionEvent -> (baseNote as? AddressableNote)?.let {
|
||||
CommunityHeader(
|
||||
baseNote = it,
|
||||
if (isQuotedNote || isBoostedNote) {
|
||||
when (baseNote.event) {
|
||||
is ChannelCreateEvent, is ChannelMetadataEvent -> ChannelHeader(
|
||||
channelNote = baseNote,
|
||||
showVideo = !makeItShort,
|
||||
showBottomDiviser = true,
|
||||
sendToCommunity = true,
|
||||
sendToChannel = true,
|
||||
accountViewModel = accountViewModel,
|
||||
nav = nav
|
||||
)
|
||||
}
|
||||
is BadgeDefinitionEvent -> BadgeDisplay(baseNote = baseNote)
|
||||
is FileHeaderEvent -> FileHeaderDisplay(baseNote, isQuotedNote, accountViewModel)
|
||||
is FileStorageHeaderEvent -> FileStorageHeaderDisplay(baseNote, isQuotedNote, accountViewModel)
|
||||
else ->
|
||||
LongPressToQuickAction(baseNote = baseNote, accountViewModel = accountViewModel) { showPopup ->
|
||||
CheckNewAndRenderNote(
|
||||
baseNote,
|
||||
routeForLastRead,
|
||||
modifier,
|
||||
isBoostedNote,
|
||||
isQuotedNote,
|
||||
unPackReply,
|
||||
makeItShort,
|
||||
addMarginTop,
|
||||
canPreview,
|
||||
parentBackgroundColor,
|
||||
accountViewModel,
|
||||
showPopup,
|
||||
nav
|
||||
is CommunityDefinitionEvent -> (baseNote as? AddressableNote)?.let {
|
||||
CommunityHeader(
|
||||
baseNote = it,
|
||||
showBottomDiviser = true,
|
||||
sendToCommunity = true,
|
||||
accountViewModel = accountViewModel,
|
||||
nav = nav
|
||||
)
|
||||
}
|
||||
is BadgeDefinitionEvent -> BadgeDisplay(baseNote = baseNote)
|
||||
else ->
|
||||
LongPressToQuickAction(baseNote = baseNote, accountViewModel = accountViewModel) { showPopup ->
|
||||
CheckNewAndRenderNote(
|
||||
baseNote,
|
||||
routeForLastRead,
|
||||
modifier,
|
||||
isBoostedNote,
|
||||
isQuotedNote,
|
||||
unPackReply,
|
||||
makeItShort,
|
||||
addMarginTop,
|
||||
canPreview,
|
||||
parentBackgroundColor,
|
||||
accountViewModel,
|
||||
showPopup,
|
||||
nav
|
||||
)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
when (baseNote.event) {
|
||||
is ChannelCreateEvent, is ChannelMetadataEvent -> ChannelHeader(
|
||||
channelNote = baseNote,
|
||||
showVideo = !makeItShort,
|
||||
showBottomDiviser = true,
|
||||
sendToChannel = true,
|
||||
accountViewModel = accountViewModel,
|
||||
nav = nav
|
||||
)
|
||||
is CommunityDefinitionEvent -> (baseNote as? AddressableNote)?.let {
|
||||
CommunityHeader(
|
||||
baseNote = it,
|
||||
showBottomDiviser = true,
|
||||
sendToCommunity = true,
|
||||
accountViewModel = accountViewModel,
|
||||
nav = nav
|
||||
)
|
||||
}
|
||||
is BadgeDefinitionEvent -> BadgeDisplay(baseNote = baseNote)
|
||||
is FileHeaderEvent -> FileHeaderDisplay(baseNote, false, accountViewModel)
|
||||
is FileStorageHeaderEvent -> FileStorageHeaderDisplay(baseNote, false, accountViewModel)
|
||||
else ->
|
||||
LongPressToQuickAction(baseNote = baseNote, accountViewModel = accountViewModel) { showPopup ->
|
||||
CheckNewAndRenderNote(
|
||||
baseNote,
|
||||
routeForLastRead,
|
||||
modifier,
|
||||
isBoostedNote,
|
||||
isQuotedNote,
|
||||
unPackReply,
|
||||
makeItShort,
|
||||
addMarginTop,
|
||||
canPreview,
|
||||
parentBackgroundColor,
|
||||
accountViewModel,
|
||||
showPopup,
|
||||
nav
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -1014,7 +1052,7 @@ private fun NoteBody(
|
||||
)
|
||||
}
|
||||
|
||||
Spacer(modifier = HalfVertSpacer)
|
||||
Spacer(modifier = Modifier.height(3.dp))
|
||||
|
||||
if (!makeItShort) {
|
||||
ReplyRow(
|
||||
@ -1144,6 +1182,14 @@ private fun RenderNoteRow(
|
||||
)
|
||||
}
|
||||
|
||||
is FileHeaderEvent -> {
|
||||
FileHeaderDisplay(baseNote, true, accountViewModel)
|
||||
}
|
||||
|
||||
is FileStorageHeaderEvent -> {
|
||||
FileStorageHeaderDisplay(baseNote, true, accountViewModel)
|
||||
}
|
||||
|
||||
is CommunityPostApprovalEvent -> {
|
||||
RenderPostApproval(
|
||||
baseNote,
|
||||
@ -2441,7 +2487,7 @@ fun SecondUserInfoRow(
|
||||
val noteAuthor = remember { note.author } ?: return
|
||||
|
||||
Row(verticalAlignment = CenterVertically, modifier = UserNameMaxRowHeight) {
|
||||
ObserveDisplayNip05Status(noteAuthor, remember { Modifier.weight(1f) })
|
||||
ObserveDisplayNip05Status(noteAuthor, remember { Modifier.weight(1f) }, accountViewModel, nav)
|
||||
|
||||
val geo = remember { noteEvent.getGeoHash() }
|
||||
if (geo != null) {
|
||||
@ -2460,6 +2506,30 @@ fun SecondUserInfoRow(
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun LoadStatuses(
|
||||
user: User,
|
||||
content: @Composable (ImmutableList<AddressableNote>) -> Unit
|
||||
) {
|
||||
var statuses: ImmutableList<AddressableNote> by remember {
|
||||
mutableStateOf(persistentListOf())
|
||||
}
|
||||
|
||||
val userStatus by user.live().statuses.observeAsState()
|
||||
|
||||
LaunchedEffect(key1 = userStatus) {
|
||||
launch(Dispatchers.IO) {
|
||||
val myUser = userStatus?.user ?: return@launch
|
||||
val newStatuses = LocalCache.findStatusesForUser(myUser)
|
||||
if (!equalImmutableLists(statuses, newStatuses)) {
|
||||
statuses = newStatuses
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
content(statuses)
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun DisplayLocation(geohash: String, nav: (String) -> Unit) {
|
||||
val context = LocalContext.current
|
||||
@ -3199,7 +3269,7 @@ private fun RenderBadge(
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun FileHeaderDisplay(note: Note, isQuotedNote: Boolean, accountViewModel: AccountViewModel) {
|
||||
fun FileHeaderDisplay(note: Note, roundedCorner: Boolean, accountViewModel: AccountViewModel) {
|
||||
val event = (note.event as? FileHeaderEvent) ?: return
|
||||
val fullUrl = event.url() ?: return
|
||||
|
||||
@ -3235,18 +3305,18 @@ fun FileHeaderDisplay(note: Note, isQuotedNote: Boolean, accountViewModel: Accou
|
||||
}
|
||||
|
||||
SensitivityWarning(note = note, accountViewModel = accountViewModel) {
|
||||
ZoomableContentView(content = content, roundedCorner = isQuotedNote, accountViewModel = accountViewModel)
|
||||
ZoomableContentView(content = content, roundedCorner = roundedCorner, accountViewModel = accountViewModel)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun FileStorageHeaderDisplay(baseNote: Note, isQuotedNote: Boolean, accountViewModel: AccountViewModel) {
|
||||
fun FileStorageHeaderDisplay(baseNote: Note, roundedCorner: Boolean, accountViewModel: AccountViewModel) {
|
||||
val eventHeader = (baseNote.event as? FileStorageHeaderEvent) ?: return
|
||||
val dataEventId = eventHeader.dataEventId() ?: return
|
||||
|
||||
LoadNote(baseNoteHex = dataEventId) { contentNote ->
|
||||
if (contentNote != null) {
|
||||
ObserverAndRenderNIP95(baseNote, contentNote, isQuotedNote, accountViewModel)
|
||||
ObserverAndRenderNIP95(baseNote, contentNote, roundedCorner, accountViewModel)
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -3255,7 +3325,7 @@ fun FileStorageHeaderDisplay(baseNote: Note, isQuotedNote: Boolean, accountViewM
|
||||
private fun ObserverAndRenderNIP95(
|
||||
header: Note,
|
||||
content: Note,
|
||||
isQuotedNote: Boolean,
|
||||
roundedCorner: Boolean,
|
||||
accountViewModel: AccountViewModel
|
||||
) {
|
||||
val eventHeader = (header.event as? FileStorageHeaderEvent) ?: return
|
||||
@ -3302,7 +3372,7 @@ private fun ObserverAndRenderNIP95(
|
||||
Crossfade(targetState = content) {
|
||||
if (it != null) {
|
||||
SensitivityWarning(note = header, accountViewModel = accountViewModel) {
|
||||
ZoomableContentView(content = it, roundedCorner = isQuotedNote, accountViewModel = accountViewModel)
|
||||
ZoomableContentView(content = it, roundedCorner = roundedCorner, accountViewModel = accountViewModel)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -419,14 +419,7 @@ private fun ReactionDetailGallery(
|
||||
val defaultBackgroundColor = MaterialTheme.colors.background
|
||||
val backgroundColor = remember { mutableStateOf<Color>(defaultBackgroundColor) }
|
||||
|
||||
val hasReactions by baseNote.live().zaps.combineWith(
|
||||
baseNote.live().boosts,
|
||||
baseNote.live().reactions
|
||||
) { zapState, boostState, reactionState ->
|
||||
zapState?.note?.zaps?.isNotEmpty() ?: false ||
|
||||
boostState?.note?.boosts?.isNotEmpty() ?: false ||
|
||||
reactionState?.note?.reactions?.isNotEmpty() ?: false
|
||||
}.distinctUntilChanged().observeAsState(
|
||||
val hasReactions by baseNote.live().hasReactions.observeAsState(
|
||||
baseNote.zaps.isNotEmpty() || baseNote.boosts.isNotEmpty() || baseNote.reactions.isNotEmpty()
|
||||
)
|
||||
|
||||
@ -603,9 +596,7 @@ fun ReplyReaction(
|
||||
|
||||
@Composable
|
||||
fun ReplyCounter(baseNote: Note, textColor: Color) {
|
||||
val repliesState by baseNote.live().replies.map {
|
||||
it.note.replies.size
|
||||
}.observeAsState(baseNote.replies.size)
|
||||
val repliesState by baseNote.live().replyCount.observeAsState(baseNote.replies.size)
|
||||
|
||||
SlidingAnimationCount(repliesState, textColor)
|
||||
}
|
||||
@ -775,9 +766,7 @@ fun BoostIcon(baseNote: Note, iconSize: Dp = Size20dp, grayTint: Color, accountV
|
||||
|
||||
@Composable
|
||||
fun BoostText(baseNote: Note, grayTint: Color) {
|
||||
val boostState by baseNote.live().boosts.map {
|
||||
it.note.boosts.size
|
||||
}.distinctUntilChanged().observeAsState(baseNote.boosts.size)
|
||||
val boostState by baseNote.live().boostCount.observeAsState(baseNote.boosts.size)
|
||||
|
||||
SlidingAnimationCount(boostState, grayTint)
|
||||
}
|
||||
|
@ -155,7 +155,7 @@ fun UpdateReactionTypeDialog(
|
||||
horizontalArrangement = Arrangement.SpaceBetween,
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
CloseButton(onCancel = {
|
||||
CloseButton(onPress = {
|
||||
postViewModel.cancel()
|
||||
onClose()
|
||||
})
|
||||
|
@ -250,7 +250,7 @@ fun UpdateZapAmountDialog(onClose: () -> Unit, nip47uri: String? = null, account
|
||||
horizontalArrangement = Arrangement.SpaceBetween,
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
CloseButton(onCancel = {
|
||||
CloseButton(onPress = {
|
||||
postViewModel.cancel()
|
||||
onClose()
|
||||
})
|
||||
|
@ -57,6 +57,7 @@ import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.asStateFlow
|
||||
import kotlinx.coroutines.flow.distinctUntilChanged
|
||||
import kotlinx.coroutines.flow.map
|
||||
import kotlinx.coroutines.launch
|
||||
import java.math.BigDecimal
|
||||
@ -187,10 +188,10 @@ class UserReactionsViewModel(val account: Account) : ViewModel() {
|
||||
private var takenIntoAccount = setOf<HexKey>()
|
||||
private val sdf = DateTimeFormatter.ofPattern("yyyy-MM-dd") // SimpleDateFormat()
|
||||
|
||||
val todaysReplyCount = _replies.map { showCount(it[today()]) }
|
||||
val todaysBoostCount = _boosts.map { showCount(it[today()]) }
|
||||
val todaysReactionCount = _reactions.map { showCount(it[today()]) }
|
||||
val todaysZapAmount = _zaps.map { showAmountAxis(it[today()]) }
|
||||
val todaysReplyCount = _replies.map { showCount(it[today()]) }.distinctUntilChanged()
|
||||
val todaysBoostCount = _boosts.map { showCount(it[today()]) }.distinctUntilChanged()
|
||||
val todaysReactionCount = _reactions.map { showCount(it[today()]) }.distinctUntilChanged()
|
||||
val todaysZapAmount = _zaps.map { showAmountAxis(it[today()]) }.distinctUntilChanged()
|
||||
|
||||
fun formatDate(createAt: Long): String {
|
||||
return sdf.format(
|
||||
|
@ -94,7 +94,7 @@ fun ZapCustomDialog(onClose: () -> Unit, accountViewModel: AccountViewModel, bas
|
||||
horizontalArrangement = Arrangement.SpaceBetween,
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
CloseButton(onCancel = {
|
||||
CloseButton(onPress = {
|
||||
postViewModel.cancel()
|
||||
onClose()
|
||||
})
|
||||
|
@ -61,7 +61,7 @@ fun ShowQRDialog(user: User, onScan: (String) -> Unit, onClose: () -> Unit) {
|
||||
horizontalArrangement = Arrangement.SpaceBetween,
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
CloseButton(onCancel = onClose)
|
||||
CloseButton(onPress = onClose)
|
||||
}
|
||||
|
||||
Column(
|
||||
|
@ -16,5 +16,5 @@ class RelayPoolViewModel : ViewModel() {
|
||||
|
||||
val isConnected = RelayPool.live.map {
|
||||
it.relays.connectedRelays() > 0
|
||||
}
|
||||
}.distinctUntilChanged()
|
||||
}
|
||||
|
@ -328,7 +328,7 @@ fun NoteMaster(
|
||||
}
|
||||
|
||||
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||
ObserveDisplayNip05Status(baseNote, remember { Modifier.weight(1f) })
|
||||
ObserveDisplayNip05Status(baseNote, remember { Modifier.weight(1f) }, accountViewModel, nav)
|
||||
|
||||
val geo = remember { noteEvent.getGeoHash() }
|
||||
if (geo != null) {
|
||||
|
@ -70,7 +70,7 @@ fun AccountBackupDialog(account: Account, onClose: () -> Unit) {
|
||||
horizontalArrangement = Arrangement.SpaceBetween,
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
CloseButton(onCancel = onClose)
|
||||
CloseButton(onPress = onClose)
|
||||
}
|
||||
|
||||
Column(
|
||||
|
@ -14,6 +14,7 @@ import androidx.lifecycle.viewModelScope
|
||||
import com.vitorpamplona.amethyst.R
|
||||
import com.vitorpamplona.amethyst.model.Account
|
||||
import com.vitorpamplona.amethyst.model.AccountState
|
||||
import com.vitorpamplona.amethyst.model.AddressableNote
|
||||
import com.vitorpamplona.amethyst.model.ConnectivityType
|
||||
import com.vitorpamplona.amethyst.model.Note
|
||||
import com.vitorpamplona.amethyst.model.User
|
||||
@ -384,6 +385,14 @@ class AccountViewModel(val account: Account) : ViewModel() {
|
||||
}
|
||||
}
|
||||
|
||||
fun createStatus(newStatus: String) {
|
||||
account.createStatus(newStatus)
|
||||
}
|
||||
|
||||
fun updateStatus(it: AddressableNote, newStatus: String) {
|
||||
account.updateStatus(it, newStatus)
|
||||
}
|
||||
|
||||
class Factory(val account: Account) : ViewModelProvider.Factory {
|
||||
override fun <AccountViewModel : ViewModel> create(modelClass: Class<AccountViewModel>): AccountViewModel {
|
||||
return AccountViewModel(account) as AccountViewModel
|
||||
|
@ -557,7 +557,7 @@ fun ChatroomHeader(
|
||||
|
||||
Column(modifier = Modifier.padding(start = 10.dp)) {
|
||||
UsernameDisplay(baseUser)
|
||||
ObserveDisplayNip05Status(baseUser)
|
||||
ObserveDisplayNip05Status(baseUser, accountViewModel = accountViewModel, nav = nav)
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -669,7 +669,7 @@ fun NewSubjectView(onClose: () -> Unit, accountViewModel: AccountViewModel, room
|
||||
horizontalArrangement = Arrangement.SpaceBetween,
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
CloseButton(onCancel = {
|
||||
CloseButton(onPress = {
|
||||
onClose()
|
||||
})
|
||||
|
||||
|
@ -56,7 +56,7 @@ fun ConnectOrbotDialog(onClose: () -> Unit, onPost: () -> Unit, portNumber: Muta
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
) {
|
||||
CloseButton(onCancel = {
|
||||
CloseButton(onPress = {
|
||||
onClose()
|
||||
})
|
||||
|
||||
|
@ -60,6 +60,7 @@ import com.vitorpamplona.amethyst.ui.note.ReplyReaction
|
||||
import com.vitorpamplona.amethyst.ui.note.ViewCountReaction
|
||||
import com.vitorpamplona.amethyst.ui.note.WatchForReports
|
||||
import com.vitorpamplona.amethyst.ui.note.ZapReaction
|
||||
import com.vitorpamplona.amethyst.ui.note.routeFor
|
||||
import com.vitorpamplona.amethyst.ui.screen.FeedEmpty
|
||||
import com.vitorpamplona.amethyst.ui.screen.FeedError
|
||||
import com.vitorpamplona.amethyst.ui.screen.FeedState
|
||||
@ -373,7 +374,9 @@ private fun RenderAuthorInformation(
|
||||
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||
ObserveDisplayNip05Status(
|
||||
remember { note.author!! },
|
||||
remember { Modifier.weight(1f) }
|
||||
remember { Modifier.weight(1f) },
|
||||
accountViewModel,
|
||||
nav = nav
|
||||
)
|
||||
}
|
||||
Row(
|
||||
@ -451,8 +454,14 @@ fun ReactionsColumn(baseNote: Note, accountViewModel: AccountViewModel, nav: (St
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
|
||||
Column(horizontalAlignment = Alignment.CenterHorizontally, modifier = Modifier.padding(bottom = 75.dp, end = 20.dp)) {
|
||||
val scope = rememberCoroutineScope()
|
||||
ReplyReaction(baseNote, grayTint = MaterialTheme.colors.onBackground, accountViewModel, iconSize = 40.dp) {
|
||||
wantsToReplyTo = baseNote
|
||||
scope.launch {
|
||||
routeFor(
|
||||
baseNote,
|
||||
accountViewModel.userProfile()
|
||||
)?.let { nav(it) }
|
||||
}
|
||||
}
|
||||
BoostReaction(baseNote, grayTint = MaterialTheme.colors.onBackground, accountViewModel, iconSize = 40.dp) {
|
||||
wantsToQuote = baseNote
|
||||
|
@ -126,7 +126,7 @@ val ChatHeadlineBorders = Modifier.padding(start = 12.dp, end = 12.dp, top = 10.
|
||||
|
||||
val VolumeBottomIconSize = Modifier.size(70.dp).padding(10.dp)
|
||||
val PinBottomIconSize = Modifier.size(70.dp).padding(10.dp)
|
||||
val NIP05IconSize = Modifier.size(14.dp).padding(top = 1.dp, start = 1.dp, end = 1.dp)
|
||||
val NIP05IconSize = Modifier.size(13.dp).padding(top = 1.dp, start = 1.dp, end = 1.dp)
|
||||
|
||||
val EditFieldModifier = Modifier
|
||||
.padding(start = 10.dp, end = 10.dp, bottom = 10.dp, top = 5.dp)
|
||||
|
552
app/src/main/res/values-th/strings.xml
Normal file
552
app/src/main/res/values-th/strings.xml
Normal file
@ -0,0 +1,552 @@
|
||||
<resources xmlns:tools="http://schemas.android.com/tools">
|
||||
<string name="app_name_release" translatable="false">Amethyst</string>
|
||||
<string name="app_name_debug" translatable="false">Amethyst Debug</string>
|
||||
<string name="point_to_the_qr_code">สแกน Qr Code</string><!--ไม่รู้ว่าอยู่ตรงไหน-->
|
||||
<string name="show_qr">โชว์ QR</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="channel_image">รูปของ channel</string><!--ไม่รู้ว่าอยู่ตรงไหน-->
|
||||
<string name="referenced_event_not_found">ไม่พบ event ที่อ้างอิง</string>
|
||||
<string name="could_not_decrypt_the_message">ไม่สามารถเข้ารหัสข้อความได้</string>
|
||||
<string name="group_picture">รูปภาพกลุ่ม</string><!--ไม่รู้ว่าอยู่ตรงไหน-->
|
||||
<string name="explicit_content">เนื้อหาที่มีความรุนแรง</string>
|
||||
<string name="spam">สแปม</string>
|
||||
<string name="impersonation">เลียนแบบ</string><!--ไม่รู้ว่าอยู่ตรงไหน-->
|
||||
<string name="illegal_behavior">พฤติกรรมที่ผิดกฎหมาย</string><!--ไม่รู้ว่าอยู่ตรงไหน-->
|
||||
<string name="unknown">ไม่ทราบ</string>
|
||||
<string name="relay_icon">รีเลย์ไอคอน</string>
|
||||
<string name="unknown_author">ไม่ทราบผู้เขียน</string>
|
||||
<string name="copy_text">คัดลอกข่้อความ</string>
|
||||
<string name="copy_user_pubkey">คัดลอก ID ผู้เขียน</string>
|
||||
<string name="copy_note_id">คัดลอก ID โน้ต</string>
|
||||
<string name="broadcast">เพยแพร่</string>
|
||||
<string name="request_deletion">ส่งคำขอให้ลบ</string>
|
||||
<string name="block_report">บล๊อก / รายงาน</string>
|
||||
<string name="block_hide_user"><![CDATA[บล๊อก & ซ่อนผู้ใช้นี้]]></string>
|
||||
<string name="report_spam_scam">รายงาน: สแปม / หลอกลวง</string>
|
||||
<string name="report_impersonation">รายงาน: การเลียนแบบ</string>
|
||||
<string name="report_explicit_content">รายงาน: เนื้อหารุนแรง</string>
|
||||
<string name="report_illegal_behaviour">รายงาน: พฤติกรรมที่ผิดกฎหมาย</string>
|
||||
<string name="login_with_a_private_key_to_be_able_to_reply">เข้าสู่ระบบด้วย private key เพื่อตอบกลับ</string>
|
||||
<string name="login_with_a_private_key_to_be_able_to_boost_posts">เข้าสู่ระบบด้วย private key เพื่อบูสโพสต์</string>
|
||||
<string name="login_with_a_private_key_to_like_posts">เข้าสู่ระบบด้วย private key เพื่อถูกใจโพสต์</string>
|
||||
<string name="no_zap_amount_setup_long_press_to_change">ไม่มีการตั้งค่าจำนวนเพื่อ zap กดค้างเพื่อตั้งค่า</string>
|
||||
<string name="login_with_a_private_key_to_be_able_to_send_zaps">เข้าสู่ระบบด้วย private key เพื่อส่ง zaps</string>
|
||||
<string name="login_with_a_private_key_to_be_able_to_follow">เข้าสู่ระบบด้วย private key เพื่อติดตาม</string>
|
||||
<string name="login_with_a_private_key_to_be_able_to_unfollow">เข้าสู่ระบบด้วย private key เพื่อเลิกติดตาม</string>
|
||||
<string name="zaps">Zaps</string>
|
||||
<string name="view_count">ยอดเข้าชม</string>
|
||||
<string name="boost">Boost</string>
|
||||
<string name="boosted">boosted</string>
|
||||
<string name="quote">โควท</string>
|
||||
<string name="new_amount_in_sats">ตั้งค่าจำนวนในหน่วย sat</string>
|
||||
<string name="add">เพิ่ม</string>
|
||||
<string name="replying_to">"ตอบกลับถึง "</string>
|
||||
<string name="and">" และ "</string>
|
||||
<string name="in_channel">" ในช่อง "</string> <!--ไม่รู้ว่าอยู่ตรงไหน-->
|
||||
<string name="profile_banner">หน้าปกโปรไฟล์</string>
|
||||
<string name="payment_successful">การชำรพเงินเสร็จสมบูรณ์</string>
|
||||
<string name="error_parsing_error_message">เกิดข้อผิดพลาดในการวิเคราะห์ข้อความแสดงข้อผิดพลาด</string>
|
||||
<string name="following">" กำลังติดตาม"</string>
|
||||
<string name="followers">" ผู้ติดตาม"</string>
|
||||
<string name="profile">โปรไฟล์</string>
|
||||
<string name="security_filters">ตัวกรอกความปลอดภัย</string>
|
||||
<string name="log_out">ออกจากระบบ</string>
|
||||
<string name="show_more">แสดงเพิ่มเติม</string>
|
||||
<string name="lightning_invoice">Lightning Invoice</string>
|
||||
<string name="pay">จ่าย</string>
|
||||
<string name="lightning_tips">Lightning Tips</string>
|
||||
<string name="note_to_receiver">โน้ตถึงผู้รับ</string>
|
||||
<string name="thank_you_so_much">ขอบคุณมาก!</string>
|
||||
<string name="amount_in_sats">จำนวนในหน่วย sat</string>
|
||||
<string name="send_sats">ส่ง Sats</string>
|
||||
<string name="error_parsing_preview_for">"ข้อผิดพลาดในการวิเคราะห์ตัวอย่างสําหรับ %1$s : %2$s"</string>
|
||||
<string name="preview_card_image_for">"รูปตัวอย่างสำหรับ %1$s"</string>
|
||||
<string name="new_channel">Channel ใหม่</string>
|
||||
<string name="channel_name">ชื่อ Channel</string>
|
||||
<string name="my_awesome_group">กลุ่มที่ยอดเยี่ยมของฉัน!</string><!--ไม่รู้ว่าอยู่ตรงไหน-->
|
||||
<string name="picture_url">Picture Url</string>
|
||||
<string name="description">รายละเอียด</string>
|
||||
<string name="about_us">"เกี่ยวกับเรา.. "</string>
|
||||
<string name="what_s_on_your_mind">คุณคิดอะไรอยู่ ?</string>
|
||||
<string name="post">โพสต์</string>
|
||||
<string name="save">บันทึก</string>
|
||||
<string name="create">สร้าง</string>
|
||||
<string name="cancel">ยกเลิก</string>
|
||||
<string name="failed_to_upload_the_image">ไม่สามารถโหลดรูปภาพได้</string>
|
||||
<string name="relay_address">Relay Address</string>
|
||||
<string name="posts">โพสต์</string>
|
||||
<string name="bytes">Bytes</string>
|
||||
<string name="errors">ข้อผิดพลาด</string>
|
||||
<string name="home_feed">ฟีดเริ่มต้น</string>
|
||||
<string name="private_message_feed">ฟีดข้อความส่วนตัว</string>
|
||||
<string name="public_chat_feed">ฟีดข้อความ</string>
|
||||
<string name="global_feed">ฟีดโลก</string>
|
||||
<string name="search_feed">ฟีดค้นหา</string>
|
||||
<string name="add_a_relay">เพิ่มรีเลย์</string>
|
||||
<string name="display_name">ชื่อที่แสดง</string>
|
||||
<string name="my_display_name">ชื่อที่แสดงของฉัน</string>
|
||||
<string name="username">ชื่อผู้ใช้</string>
|
||||
<string name="my_username">ชื่อผู้ใช้ของฉัน</string>
|
||||
<string name="about_me">เกี่ยวกับฉัน</string>
|
||||
<string name="avatar_url">Avatar URL</string>
|
||||
<string name="banner_url">Banner URL</string>
|
||||
<string name="website_url">Website URL</string>
|
||||
<string name="ln_address">LN Address</string>
|
||||
<string name="ln_url_outdated">LN URL (หมดอายุ)</string>
|
||||
<string name="image_saved_to_the_gallery">บันทึกรูปภาพลงแกลเลอรี</string>
|
||||
<string name="failed_to_save_the_image">เกิดข้อผิดพลาดในการบันทึกรูปภาพ</string>
|
||||
<string name="upload_image">อัพโหลดรูปภาพ</string>
|
||||
<string name="uploading">กำลังอัพโหลด</string>
|
||||
<string name="user_does_not_have_a_lightning_address_setup_to_receive_sats">ผู้ใช้นี้ไม่ได้ตั้งค่า lightning address เพื่อรับ sats</string>
|
||||
<string name="reply_here">"ตอบกลับที่นี่.. "</string>
|
||||
<string name="copies_the_note_id_to_the_clipboard_for_sharing">คัดลอกโน้ต ID ลงคลิปบอร์ดเพื่อแชร์ไปยัง Nostr</string>
|
||||
<string name="copy_channel_id_note_to_the_clipboard">คัดลอก Channel ID (โน้ต) ลงคลิปบอร์ด</string>
|
||||
<string name="edits_the_channel_metadata">แก้ไข Channel Metadata</string>
|
||||
<string name="join">เข้าร่วม</string>
|
||||
<string name="known">รู้</string><!--ไม่รู้ว่าอยู่ตรงไหน-->
|
||||
<string name="new_requests">ส่งคำขอใหม่</string>
|
||||
<string name="blocked_users">ผู้ใช้ที่ถูกบล็อก</string>
|
||||
<string name="new_threads">เธรดใหม่</string>
|
||||
<string name="conversations">การสนทนา</string>
|
||||
<string name="notes">โน้ต</string>
|
||||
<string name="replies">ตอบกลับ</string>
|
||||
<string name="follows">"ติดตาม"</string>
|
||||
<string name="reports">"รายงาน"</string>
|
||||
<string name="more_options">ตัวเลือกเพิ่มเติม</string>
|
||||
<string name="relays">" รีเลย์"</string>
|
||||
<string name="website">เว็บไซต์</string>
|
||||
<string name="lightning_address">Lightning Address</string>
|
||||
<string name="copies_the_nsec_id_your_password_to_the_clipboard_for_backup">คัดลอก Nsec ID (รหัสผ่านของคุณ) ลงคลิปบอร์ดเพื่อสำรองข้อมูล</string>
|
||||
<string name="copy_private_key_to_the_clipboard">คัดลอก Secret Key ลงคลิปบอร์ด</string>
|
||||
<string name="copies_the_public_key_to_the_clipboard_for_sharing">คัดลอก public key ลงคลิปบอร์ดเพื่อแชร์</string>
|
||||
<string name="copy_public_key_npub_to_the_clipboard">คัดลอก Public Key (NPub) ลงคลิปบอร์ด</string>
|
||||
<string name="send_a_direct_message">ส่งข้อความส่วนตัว</string>
|
||||
<string name="edits_the_user_s_metadata">แก้ไข the User\'s Metadata</string>
|
||||
<string name="follow">ติดตาม</string>
|
||||
<string name="follow_back">ติดตามกลับ</string>
|
||||
<string name="unblock">เลิกบล๊อก</string>
|
||||
<string name="copy_user_id">คัดลอก ID ผู้ใช้</string>
|
||||
<string name="unblock_user">เลิกบล๊อกผู้ใช้</string>
|
||||
<string name="npub_hex_username">"npub, ชื่อผู้ใช้, ข้อความ"</string>
|
||||
<string name="clear">เคลียร์</string>
|
||||
<string name="app_logo">App Logo</string>
|
||||
<string name="nsec_npub_hex_private_key">nsec.. or npub..</string>
|
||||
<string name="show_password">แสดงรหัสผ่าน</string>
|
||||
<string name="hide_password">ซ่อนรหัสผ่าน</string>
|
||||
<string name="invalid_key">key ไม่ถูกต้อง</string>
|
||||
<string name="i_accept_the">"ฉันยอมรับ "</string>
|
||||
<string name="terms_of_use">เงื่อนไขการใช้งาน</string>
|
||||
<string name="acceptance_of_terms_is_required">จําเป็นต้องยอมรับข้อกําหนด</string>
|
||||
<string name="key_is_required">ต้องใช้ key</string>
|
||||
<string name="login">เข้าสู่ระบบ</string>
|
||||
<string name="generate_a_new_key">สร้าง key ใหม่</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>
|
||||
<string name="created">สร้าง</string>
|
||||
<string name="with_description_of">พร้อมคําอธิบายของ</string>
|
||||
<string name="and_picture">และ รูปภาพ</string>
|
||||
<string name="changed_chat_name_to">เปลี่ยนชื่อ chat เป็น</string>
|
||||
<string name="description_to">คําอธิบายของ</string>
|
||||
<string name="and_picture_to">และ รูปภาพของ</string>
|
||||
<string name="leave">ออก</string><!--ไม่รู้ว่าอยู่ตรงไหน-->
|
||||
<string name="unfollow">เลิกติดตาม</string>
|
||||
<string name="channel_created">สร้าง Channel</string>
|
||||
<string name="channel_information_changed_to">"ข้อมูลของ Channel เปลี่ยนเป็น"</string>
|
||||
<string name="public_chat">แชทสาธรณะ</string>
|
||||
<string name="posts_received">ได้รับโพสต์แล้ว</string>
|
||||
<string name="remove">ลบ</string>
|
||||
<string name="sats" translatable="false">sats</string>
|
||||
<string name="translations_auto">อัตโนมัติ</string>
|
||||
<string name="translations_translated_from">แปลจาก</string>
|
||||
<string name="translations_to">ถึง</string>
|
||||
<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">Nostr Address</string>
|
||||
<string name="lnurl" translatable="false">LNURL...</string>
|
||||
<string name="never">ไม่เคย</string>
|
||||
<string name="now">ตอนนี้</string>
|
||||
<string name="h">h</string>
|
||||
<string name="m">m</string>
|
||||
<string name="d">d</string>
|
||||
<string name="nudity">ภาพเปลือย/สื่อลามก</string>
|
||||
<string name="profanity_hateful_speech">คําหยาบคาย / คําพูดที่แสดงความเกลียดชัง</string>
|
||||
<string name="report_hateful_speech">รายงาน: คําพูดที่แสดงความเกลียดชัง</string>
|
||||
<string name="report_nudity_porn">รายงาน: ภาพเปลือย/สื่อลามก</string>
|
||||
<string name="others">อื่น ๆ</string>
|
||||
<string name="mark_all_known_as_read">ทำเครื่องหมายอ่านแล้วในแชทที่รู้จักทั้งหมด</string>
|
||||
<string name="mark_all_new_as_read">ทำเครื่องหมายว่าอ่านแล้วทั้งหมดสำหรับข้อความใหม่</string>
|
||||
<string name="mark_all_as_read">ทพเครื่องหมายว่าอ่านแล้วทั้งหมด</string>
|
||||
<string name="backup_keys">สพรองข้อมูล Keys</string>
|
||||
<string name="account_backup_tips_md" tools:ignore="Typos">
|
||||
## เคล็ดลับการสํารองข้อมูลและความปลอดภัยที่สําคัญ
|
||||
\n\n บัญชีของคุณมีความปลอดภัยด้วยรหัสลับ key คือสตริงสุ่มยาวที่ขึ้นต้นด้วย **nsec1** ทุกคนที่มีสิทธิ์เข้าถึงรหัสลับของคุณสามารถเผยแพร่เนื้อหาโดยใช้บัญชีของคุณได้
|
||||
\n\n- **อย่า** ใส่รหัสลับของคุณในเว็บไซต์หรือซอฟต์แวร์ที่คุณไม่เชื่อถือ
|
||||
\n- นักพัฒนา Amethyst จะ **ไม่เคย** ขอรหัสลับของคุณ
|
||||
\n- **เก็บ** สําเนาสํารองคีย์ลับของคุณไว้อย่างปลอดภัยสําหรับการกู้คืนบัญชี เราขอแนะนําให้ใช้ password manager.
|
||||
</string>
|
||||
<string name="secret_key_copied_to_clipboard">คัดลอก Secret key (nsec) ลงคลิปบอร์ด</string>
|
||||
<string name="copy_my_secret_key">คัดลอก secret key ของฉัน</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_user_id_to_clipboard" tools:ignore="Typos">คัดลอก @npub ของผู้เขียนลงคลิปบอร์ด</string>
|
||||
<string name="copied_note_id_to_clipboard" tools:ignore="Typos">คัดลอกโน้ต ID (@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>
|
||||
<string name="mastodon" translatable="false">Mastodon Post ID w/ Proof</string>
|
||||
<string name="twitter" translatable="false">Twitter Status w/ Proof</string>
|
||||
<string name="github_proof_url_template" translatable="false">https://gist.github.com/<user>/<gist></string>
|
||||
<string name="telegram_proof_url_template" translatable="false">https://t.me/<proof post></string>
|
||||
<string name="mastodon_proof_url_template" translatable="false">https://<server>/<user>/<proof post></string>
|
||||
<string name="twitter_proof_url_template" translatable="false">https://twitter.com/<user>/status/<proof post></string>
|
||||
<string name="private_conversation_notification">"<ไม่สามารถถอดรหัสข้อความส่วนตัวได้>\n\nคุณถูกอ้างถึงในการสนทนาส่วนตัว/เข้ารหัสระหว่าง %1$s และ %2$s."</string><!--ไม่รู้ว่าอยู่ตรงไหน-->
|
||||
<string name="account_switch_add_account_dialog_title">เพิ่มบัญชีใหม่</string>
|
||||
<string name="drawer_accounts">บัญชี</string>
|
||||
<string name="account_switch_select_account">เลือกบัญชี</string>
|
||||
<string name="account_switch_add_account_btn">เพิ่มบัญชีใหม่</string>
|
||||
<string name="account_switch_active_account">บัญชีที่ใช้งานอยู่</string>
|
||||
<string name="account_switch_has_private_key">มี private key</string>
|
||||
<string name="account_switch_pubkey_only">อ่านเท่านั้น, ไม่มี private key</string>
|
||||
<string name="back">ย้อนกลับ</string>
|
||||
<string name="quick_action_select">เลือก</string>
|
||||
<string name="quick_action_share_browser_link">แชร์ Browser Link</string>
|
||||
<string name="quick_action_share">แชร์</string>
|
||||
<string name="quick_action_copy_user_id">ID ผู้เขียน</string>
|
||||
<string name="quick_action_copy_note_id">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">ส่งคำขอให้ลบ</string>
|
||||
<string name="quick_action_request_deletion_alert_body">Amethyst จะขอให้ลบโน้ตของคุณออกจากรีเลย์ที่คุณเชื่อมต่ออยู่ ไม่มีการรับประกันว่าโน้ตของคุณจะถูกลบออกอย่างถาวรจากรีเลย์เหล่านั้น หรือ จากรีเลย์อื่น ๆ ที่อาจเก็บไว้</string>
|
||||
<string name="quick_action_block_dialog_btn">บล๊อก</string>
|
||||
<string name="quick_action_delete_dialog_btn">ลบ</string>
|
||||
<string name="quick_action_block">บล๊อก</string>
|
||||
<string name="quick_action_report">รายงาน</string>
|
||||
<string name="quick_action_delete_button">ลบ</string>
|
||||
<string name="quick_action_dont_show_again_button">ไม่แสดงข้อความนี้อีก</string><!--ไม่เจอว่าตรงไหน-->
|
||||
<string name="report_dialog_spam">สแปม หรือ หลอกลวง</string>
|
||||
<string name="report_dialog_profanity">คําหยาบคายหรือการแสดงความเกลียดชัง</string>
|
||||
<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_block_hide_user_btn"><![CDATA[บล๊อก & ซ่อนผู้ใช้]]></string>
|
||||
<string name="report_dialog_report_btn">รายงานการล่วงละเมิด</string>
|
||||
<string name="report_dialog_reminder_public">การรายงานที่โพสต์ทั้งหมดจะปรากฏต่อสาธารณะ</string>
|
||||
<string name="report_dialog_additional_reason_placeholder">ระบุข้อความเพิ่มเติมเกี่ยวกับรายงานของคุณ (ไม่บังคับ)</string>
|
||||
<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_title">บล๊อกและรายงาน</string>
|
||||
<string name="block_only">บล๊อก</string>
|
||||
|
||||
<string name="bookmarks">บุ๊คมาร์ค</string>
|
||||
<string name="private_bookmarks">บุ๊คมาร์คส่วนตัว</string>
|
||||
<string name="public_bookmarks">บุ๊คมาร์คสาธรณะ</string>
|
||||
<string name="add_to_private_bookmarks">เพิ่มไปยังบุ๊คมาร์คส่วนตัว</string>
|
||||
<string name="add_to_public_bookmarks">เพิ่มไปยังบุ๊คมาร์คสาธรณะ</string>
|
||||
<string name="remove_from_private_bookmarks">ลบออกจากบุ๊คมาร์คส่วนตัว</string>
|
||||
<string name="remove_from_public_bookmarks">ลบออกจากบุ๊คมาร์คสาธรณะ</string>
|
||||
|
||||
<string name="wallet_connect_service">บริการเชื่อมต่อ wallet</string>
|
||||
<string name="wallet_connect_service_explainer">อนุญาตให้ Nostr Secret จ่าย zaps โดยไม่ต้องออกจากแอป เพื่อรักษาความลับให้ปลอดภัยและใช้รีเลย์ส่วนตัวหากเป็นไปได้</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">แสดง secret key</string>
|
||||
<string name="wallet_connect_service_secret_placeholder">nsec / hex private key</string>
|
||||
|
||||
<string name="pledge_amount_in_sats">จำนวนเงินที่นำเข้าในหน่วย sat</string>
|
||||
<string name="post_poll">สร้างโพลล์</string>
|
||||
<string name="poll_heading_required">ส่วนที่จำเป็น:</string>
|
||||
<string name="poll_zap_recipients">ผู้รับ Zap</string>
|
||||
<string name="poll_primary_description">คำอธิบายโพลล์…</string>
|
||||
<string name="poll_option_index">ตัวเลือก %s</string>
|
||||
<string name="poll_option_description">คำอธิบายตัวเลือกในโพลล์</string>
|
||||
<string name="poll_heading_optional">ตำอธิบายเพิ่มเติม:</string>
|
||||
<string name="poll_zap_value_min">Zap ขั้นต่ำ</string>
|
||||
<string name="poll_zap_value_max">Zap สูงสุด</string>
|
||||
<string name="poll_consensus_threshold">ฉันทามติ</string>
|
||||
<string name="poll_consensus_threshold_percent">(0–100)%</string>
|
||||
<string name="poll_closing_time">ปิดหลังจาก</string>
|
||||
<string name="poll_closing_time_days">วัน</string>
|
||||
<string name="poll_is_closed">โพลปิดรับการลงคะแนนเพิ่มเติม</string>
|
||||
<string name="poll_zap_amount">จำนวน Zap</string>
|
||||
<string name="one_vote_per_user_on_atomic_votes">อนุญาตให้โหวตได้เพียงครั้งเดียวต่อผู้ใช้หนึ่งคนสำหรับการสำรวจประเภทนี้</string>
|
||||
|
||||
<string name="looking_for_event">"กำลังมองหา Event %1$s"</string>
|
||||
|
||||
<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">เพิ่มใน invoice message</string>
|
||||
|
||||
<string name="custom_zaps_add_a_message_example">ขอบคุณสำหรับทุกงานของคุณ!</string>
|
||||
|
||||
<string name="lightning_create_and_add_invoice">สร้างและเพิ่ม</string>
|
||||
<string name="poll_author_no_vote">ผู้เขียนโพลล์ไม่สามารถลงคะแนนในโพล์ของตนเองได้</string>
|
||||
<string name="poll_hashtag" translatable="false">#zappoll</string>
|
||||
|
||||
<string name="hash_verification_passed">เนื้อหานี้เหมือนเดิมตั้งแต่โพสต์</string>
|
||||
<string name="hash_verification_failed">เนื้อหามีการเปลี่ยนแปลง ผู้เขียนอาจไม่เห็นหรืออนุมัติการเปลี่ยนแปลง</string>
|
||||
|
||||
<string name="content_description_add_image">เพิ่มรูปภาพ</string>
|
||||
<string name="content_description_add_video">เพิ่มวีดีโอ</string>
|
||||
<string name="content_description_add_document">เพิ่มเอกสาร</string>
|
||||
|
||||
<string name="add_content">เพิ่มไปยังข้อความ</string>
|
||||
<string name="content_description">คำอธิบายของเนื้อหา</string>
|
||||
<string name="content_description_example">เรือสีฟ้าในหาดทรายสีขาวยามพระอาทิตย์ตก</string>
|
||||
|
||||
<string name="zap_type">ประเภทการ Zap</string>
|
||||
<string name="zap_type_explainer">ตัวเลือกทั้งหมดสำหรับการ zap</string>
|
||||
|
||||
<string name="zap_type_public">สาธรณะ</string>
|
||||
<string name="zap_type_public_explainer">ทุกคนสามารถเห็นธุรกรรมและการข้อความ</string>
|
||||
|
||||
<string name="zap_type_private">ส่วนตัว</string>
|
||||
<string name="zap_type_private_explainer">ผู้ส่งและผู้รับสามารถเห็นธุรกรรมและข้อความ</string>
|
||||
|
||||
<string name="zap_type_anonymous">ไม่ระบุตัวตน</string>
|
||||
<string name="zap_type_anonymous_explainer">ไม่มีใครรู้ว่าใครเป็นคนทำธุรกรรม</string>
|
||||
|
||||
<string name="zap_type_nonzap">Non-Zap</string>
|
||||
<string name="zap_type_nonzap_explainer">ไม่มีการบันทึกบน Nostr, มีแค่ในระบบ Lightning</string>
|
||||
|
||||
|
||||
<string name="file_server">File Server</string>
|
||||
<string name="zap_forward_lnAddress">LnAddress or @User</string>
|
||||
|
||||
<string name="upload_server_imgur">imgur.com - trusted</string>
|
||||
<string name="upload_server_imgur_explainer">Imgur สามารถแก้ไขไฟล์ได้</string>
|
||||
|
||||
<string name="upload_server_nostrimg">nostrimg.com - trusted</string>
|
||||
<string name="upload_server_nostrimg_explainer">NostrImg สามารถแก้ไขไฟล์ได้</string>
|
||||
|
||||
<string name="upload_server_nostrbuild">nostr.build - trusted</string>
|
||||
<string name="upload_server_nostrbuild_explainer">Nostr.build สามารถแก้ไขไฟล์ได้</string>
|
||||
|
||||
<string name="upload_server_nostrfilesdev">nostrfiles.dev - trusted</string>
|
||||
<string name="upload_server_nostrfilesdev_explainer">Nostrfiles.dev สามารถแก้ไขไฟล์ได้</string>
|
||||
|
||||
<string name="upload_server_nostrcheckme">nostrcheck.me - trusted</string>
|
||||
<string name="upload_server_nostrcheckme_explainer">nostrcheck.me สามารถแก้ไขไฟล์ได้</string>
|
||||
|
||||
|
||||
<string name="upload_server_imgur_nip94">Verifiable Imgur (NIP-94)</string>
|
||||
<string name="upload_server_imgur_nip94_explainer">ตวรจสอบว่า Imgur แก้ไขไฟล์ New NIP: ไคลเอนต์อื่น ๆ จะไม่เห็นมัน</string>
|
||||
|
||||
<string name="upload_server_nostrimg_nip94">Verifiable NostrImg (NIP-94)</string>
|
||||
<string name="upload_server_nostrimg_nip94_explainer">ตวรจสอบว่า NostrImg แก้ไขไฟล์ New NIP: ไคลเอนต์อื่น ๆ จะไม่เห็นมัน</string>
|
||||
|
||||
<string name="upload_server_nostrbuild_nip94">Verifiable Nostr.build (NIP-94)</string>
|
||||
<string name="upload_server_nostrbuild_nip94_explainer">ตวรจสอบว่า Nostr.build แก้ไขไฟล์ New NIP: ไคลเอนต์อื่น ๆ จะไม่เห็นมัน</string>
|
||||
|
||||
<string name="upload_server_nostrfilesdev_nip94">Verifiable Nostrfiles.dev (NIP-94)</string>
|
||||
<string name="upload_server_nostrfilesdev_nip94_explainer">ตวรจสอบว่า Nostrfiles.dev แก้ไขไฟล์ New NIP: ไคลเอนต์อื่น ๆ จะไม่เห็นมัน</string>
|
||||
|
||||
<string name="upload_server_nostrcheckme_nip94">Verifiable Nostrcheck.me (NIP-94)</string>
|
||||
<string name="upload_server_nostrcheckme_nip94_explainer">ตวรจสอบว่า Nostrcheck.me แก้ไขไฟล์ New NIP: ไคลเอนต์อื่น ๆ จะไม่เห็นมัน</string>
|
||||
|
||||
<string name="upload_server_relays_nip95">รีเลย์ของคุณ (NIP-95)</string>
|
||||
<string name="upload_server_relays_nip95_explainer">ไฟล์อยู่บนรีเลย์ของคุณ New 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">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, ตรวจสอบe Socks port. ค่าเริ่มต้นเป็น 9050
|
||||
\n4. หากจําเป็นให้เปลี่ยนพอร์ตใน Orbot
|
||||
\n5. กําหนดค่า Socks port บนหน้าจอนี้
|
||||
\n6. กดปุ่มเปิดใช้งานเพื่อใช้ Orbot เป็น proxy
|
||||
</string>
|
||||
<string name="orbot_socks_port">Orbot Socks Port</string>
|
||||
<string name="invalid_port_number">เลข port ไม่ถูกต้องr</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">New notification arrived</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">ได้รับ Zaps</string>
|
||||
<string name="app_notification_zaps_channel_description">แจ้งเตือนเมื่อมีคน zaps</string>
|
||||
<string name="app_notification_zaps_channel_message">%1$s sats</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">ผู้ใช้ หรือ Group\'s 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>
|
||||
|
||||
<string name="recommended_apps">แนะนำ: </string>
|
||||
<string name="filter_spam_from_strangers">กรองสแปมจากคนแปลกหน้า</string>
|
||||
<string name="warn_when_posts_have_reports_from_your_follows">เตือนเมื่อมีการรายงานโพสต์จากผู้ติดตามของคุณ</string>
|
||||
|
||||
<string name="new_reaction_symbol">สัญลักษณ์ Reaction ใหม่</string>
|
||||
<string name="no_reaction_type_setup_long_press_to_change">ไม่มีการเลือกประเภทของ reaction กดค้างเพื่อเปลี่ยน</string>
|
||||
|
||||
<string name="zapraiser">Zapraiser</string>
|
||||
<string name="zapraiser_explainer">เพิ่มจํานวนเป้าหมายของ sats ที่จะเพิ่มสําหรับโพสต์นี้ การสนับสนุนไคลเอนต์อาจแสดงสิ่งนี้เป็นแถบความคืบหน้าเพื่อจูงใจการบริจาค</string>
|
||||
<string name="zapraiser_target_amount_in_sats">จำนวนเป้าหมายในหน่วย Sats</string>
|
||||
|
||||
<string name="sats_to_complete">Zapraiser at %1$s. %2$s sats เพื่อไปถึงเป้าหมาย</string>
|
||||
<string name="read_from_relay">อ่านจาก Relay</string>
|
||||
<string name="write_to_relay">เขียนบน Relay</string>
|
||||
<string name="an_error_occurred_trying_to_get_relay_information">เกิดข้อผิดพลาดในการรับข้อมูลจาก %1$s</string>
|
||||
<string name="owner">เจ้าของ</string>
|
||||
<string name="version">Version</string>
|
||||
<string name="software">Software</string>
|
||||
<string name="contact">ติดต่อ</string>
|
||||
<string name="supports">รับรอง NIPs</string>
|
||||
<string name="admission_fees">ค่าใช้จ่ายสำหรับการใช้งาน</string>
|
||||
<string name="payments_url">url การชำระเงิน</string>
|
||||
<string name="limitations">ข้อจำกัด</string>
|
||||
<string name="countries">ประเทศ</string>
|
||||
<string name="languages">ภาษา</string>
|
||||
<string name="tags">Tags</string>
|
||||
<string name="posting_policy">นโยบายการโพสต์</string>
|
||||
<string name="message_length">ความยาวข้อความ</string>
|
||||
<string name="subscriptions">การสมัครสมาชิก</string>
|
||||
<string name="filters">ฟิลเตอร์</string>
|
||||
<string name="subscription_id_length">ความยาวของรหัสการสมัครสมาชิก</string>
|
||||
<string name="minimum_prefix">คำนำหน้าขั้นต่ำ</string>
|
||||
<string name="maximum_event_tags">event tags สูงสุด</string>
|
||||
<string name="content_length">ความยาวของเนื้อหา</string>
|
||||
<string name="minimum_pow">PoW ขั้นต่ำ</string>
|
||||
<string name="auth">Auth</string>
|
||||
<string name="payment">การชำระเงิน</string>
|
||||
<string name="cashu">โทเคน Cashu</string>
|
||||
<string name="cashu_redeem">แลกคืน</string>
|
||||
<string name="no_lightning_address_set">ไม่ได้ตั้งค่า Lightning Address</string>
|
||||
<string name="copied_token_to_clipboard">คัดลอกโทเคนลงคลิปบอร์ด</string>
|
||||
|
||||
<string name="live_stream_live_tag">LIVE</string>
|
||||
<string name="live_stream_offline_tag">OFFLINE</string>
|
||||
<string name="live_stream_ended_tag">ENDED</string>
|
||||
<string name="live_stream_planned_tag">SCHEDULED</string>
|
||||
|
||||
<string name="live_stream_is_offline">Livestream is Offline</string>
|
||||
<string name="live_stream_has_ended">Livestream Ended</string>
|
||||
<string name="are_you_sure_you_want_to_log_out">การออกจากระบบจะลบข้อมูลทั้งหมดของคุณ ตรวจสอบให้แน่ใจว่าได้สํารองข้อมูล private key ไว้เพื่อหลีกเลี่ยงการสูญเสียบัญชีของคุณ คุณต้องการดําเนินการต่อหรือไม่?</string>
|
||||
<string name="followed_tags">Followed Tags</string>
|
||||
|
||||
<string name="relay_setup">รีเลย์</string>
|
||||
|
||||
<string name="discover_live">Live</string>
|
||||
<string name="discover_community">ชุมชน</string>
|
||||
<string name="discover_chat">ช่องสนทนา</string>
|
||||
<string name="community_approved_posts">อนุมัติโพสต์</string>
|
||||
|
||||
<string name="groups_no_descriptor">ชุมชนนี้ไม่มีคำอธิบายหรือกฏ พูดคุยกับเจ้าของเพื่อเพิ่มเติมสิ่งนี้</string>
|
||||
<string name="community_no_descriptor">ชุมชนนี้ไม่มีคำอธิบาย พูดคุยกับเจ้าของเพื่อเพิ่มเติมสิ่งนี้</string>
|
||||
|
||||
<string name="add_sensitive_content_label">เนื้อหาที่ละเอียดอ่อน</string>
|
||||
<string name="add_sensitive_content_description">เพิ่มการแจ้งเตือนเนื้อหาที่ละเอียดอ่อนก่อนโชว์เนื้อหานี้</string>
|
||||
<string name="settings">การตั้งค่า</string>
|
||||
<string name="connectivity_type_always">Aตลอดเวลา</string>
|
||||
<string name="connectivity_type_wifi_only">Wifi เท่านั้น</string>
|
||||
<string name="connectivity_type_never">ไม่ต้องแสดง</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">ภาพตัวอย่าง</string>
|
||||
<string name="automatically_play_videos">การเล่นวิดีโอ</string>
|
||||
<string name="automatically_show_url_preview">การแสดงตัวอย่าง URL</string>
|
||||
<string name="load_image">โหลดรูปภาพ</string>
|
||||
|
||||
<string name="spamming_users">Spammers</string>
|
||||
|
||||
<string name="muted_button">ปิดการมองเห็น คลิกเพื่อปลดออก</string>
|
||||
<string name="mute_button">เปิดการมองเห็นอยู่ คลิกเพื่อปิด</string>
|
||||
<string name="search_button">ค้นหาบันทึกการเข้าถึงแบบ local และ remote</string>
|
||||
|
||||
<string name="nip05_verified">Nostr address ได้รับการยืนยัน</string>
|
||||
<string name="nip05_failed">ไม่สามารถยืนยัน Nostr address ได้</string>
|
||||
<string name="nip05_checking">ตรวจสอบ Nostr address</string>
|
||||
<string name="select_deselect_all">เลือก/ไม่เลือก ทั้งหมด</string>
|
||||
<string name="default_relays">ค่าเริ่มต้น</string>
|
||||
<string name="select_a_relay_to_continue">เลือกรีเลย์เพื่อดำเนินการต่อ</string>
|
||||
|
||||
<string name="zap_forward_title">ส่งต่อ Zaps ถึง:</string>
|
||||
<string name="zap_forward_explainer">ไคลเอนต์ที่สนับสนุนจะส่งต่อ zaps ไปยัง LNAddress หรือโปรไฟล์ผู้ใช้ด้านล่างแทนที่จะเป็นของคุณ</string>
|
||||
|
||||
<string name="geohash_title">เปิดเผยตําแหน่งที่ตั้ง </string>
|
||||
<string name="geohash_explainer">เพิ่ม Geohash ของตําแหน่งของคุณลงในโพสต์ สาธารณชนจะรู้ว่าคุณอยู่ภายใน 5 กม. (3 ไมล์) จากตําแหน่งปัจจุบัน</string>
|
||||
|
||||
<string name="add_sensitive_content_explainer">เพิ่มคําเตือนเนื้อหาที่ละเอียดอ่อนก่อนแสดงเนื้อหาของคุณ สิ่งนี้เหมาะสําหรับเนื้อหา NSFW หรือเนื้อหาใด ๆ ที่บางคนอาจพบว่าไม่เหมาะสมหรือรบกวน</string>
|
||||
|
||||
<string name="new_feature_nip24_might_not_be_available_title">ฟีเจอร์ใหม่</string>
|
||||
<string name="new_feature_nip24_might_not_be_available_description">การเปิดใช้งานโหมดนี้ต้องใช้ Amethyst เพื่อส่งข้อความ NIP-24 (GiftWrapped, Sealed Direct และ Group Messages) NIP-24 เป็นของใหม่และไคลเอ็นต์ส่วนใหญ่ยังไม่ได้รองรับ ตรวจสอบให้แน่ใจว่าเครื่องรับใช้ไคลเอ็นต์ที่รองรับ</string>
|
||||
<string name="new_feature_nip24_activate">เปิดใช้งาน</string>
|
||||
|
||||
<string name="messages_create_public_chat">สาธรณะ</string>
|
||||
<string name="messages_new_message">ส่วนตัว</string>
|
||||
<string name="messages_new_message_to">ถึง</string>
|
||||
<string name="messages_new_message_subject">ชื่อเรื่อง</string>
|
||||
<string name="messages_new_message_subject_caption">หัวข้อการสนทนา</string>
|
||||
<string name="messages_new_message_to_caption">"@User1, @User2, @User3"</string>
|
||||
|
||||
<string name="messages_group_descriptor">ชมาชิกในกลุ่ม</string>
|
||||
<string name="messages_new_subject_message">คําอธิบายต่อสมาชิก</string>
|
||||
<string name="messages_new_subject_message_placeholder">เปลี่ยนชื่อสําหรับเป้าหมายใหม่</string>
|
||||
|
||||
<string name="language_description">สำหรับ App\'s Interface</string>
|
||||
<string name="theme_description">ธีม: มืด, สว่าง หรือระบบ</string>
|
||||
<string name="automatically_load_images_gifs_description">โหลดรูปภาพและ GIFs โดยอัตโนมัติ</string>
|
||||
<string name="automatically_play_videos_description">เล่นวีดีโอและ GIFs โดยอัตโนมัติ</string>
|
||||
<string name="automatically_show_url_preview_description">แสดงจัวอย่าง URL</string>
|
||||
<string name="load_image_description">ควรโหลดรูปภาพเมื่อใด</string>
|
||||
|
||||
<string name="copy_url_to_clipboard">คัดลอก URL ลงคลิปบอร์ด</string>
|
||||
<string name="copy_the_note_id_to_the_clipboard">คัดลอก โน้ต ID ลงคลิปบอร์ดd</string>
|
||||
|
||||
<string name="created_at">สร้างโดย</string>
|
||||
<string name="rules">กฏ</string>
|
||||
</resources>
|
@ -551,5 +551,8 @@
|
||||
|
||||
<string name="created_at">Created at</string>
|
||||
<string name="rules">Rules</string>
|
||||
|
||||
<string name="login_with_amber">Login with Amber</string>
|
||||
|
||||
<string name="status_update">Update your status</string>
|
||||
</resources>
|
||||
|
@ -16,6 +16,7 @@
|
||||
<locale android:name="ru"/>
|
||||
<locale android:name="sv-SE"/>
|
||||
<locale android:name="ta"/>
|
||||
<locale android:name="th"/>
|
||||
<locale android:name="tr"/>
|
||||
<locale android:name="uk"/>
|
||||
<locale android:name="zh"/>
|
||||
|
@ -69,6 +69,16 @@ open class Event(
|
||||
|
||||
override fun taggedUrls() = tags.filter { it.size > 1 && it[0] == "r" }.map { it[1] }
|
||||
|
||||
override fun firstTaggedUser() = tags.firstOrNull { it.size > 1 && it[0] == "p" }?.let { it[1] }
|
||||
override fun firstTaggedEvent() = tags.firstOrNull { it.size > 1 && it[0] == "e" }?.let { it[1] }
|
||||
override fun firstTaggedUrl() = tags.firstOrNull { it.size > 1 && it[0] == "r" }?.let { it[1] }
|
||||
override fun firstTaggedAddress() = tags.firstOrNull { it.size > 1 && it[0] == "r" }?.let {
|
||||
val aTagValue = it[1]
|
||||
val relay = it.getOrNull(2)
|
||||
|
||||
ATag.parse(aTagValue, relay)
|
||||
}
|
||||
|
||||
override fun taggedEmojis() = tags.filter { it.size > 2 && it[0] == "emoji" }.map { EmojiUrl(it[1], it[2]) }
|
||||
|
||||
override fun isSensitive() = tags.any {
|
||||
@ -120,6 +130,14 @@ open class Event(
|
||||
return tags.any { it.size > 1 && it[0] == "a" && it[1].startsWith(kindStr) }
|
||||
}
|
||||
|
||||
override fun expiration() = try {
|
||||
tags.firstOrNull { it.size > 1 && it[0] == "expiration" }?.get(1)?.toLongOrNull()
|
||||
} catch (_: Exception) {
|
||||
null
|
||||
}
|
||||
|
||||
override fun isExpired() = (expiration() ?: Long.MAX_VALUE) < TimeUtils.now()
|
||||
|
||||
override fun getTagOfAddressableKind(kind: Int): ATag? {
|
||||
val kindStr = kind.toString()
|
||||
val aTag = tags
|
||||
|
@ -74,6 +74,7 @@ class EventFactory {
|
||||
RelaySetEvent.kind -> RelaySetEvent(id, pubKey, createdAt, tags, content, sig)
|
||||
ReportEvent.kind -> ReportEvent(id, pubKey, createdAt, tags, content, sig)
|
||||
RepostEvent.kind -> RepostEvent(id, pubKey, createdAt, tags, content, sig)
|
||||
StatusEvent.kind -> StatusEvent(id, pubKey, createdAt, tags, content, sig)
|
||||
TextNoteEvent.kind -> TextNoteEvent(id, pubKey, createdAt, tags, content, sig)
|
||||
else -> Event(id, pubKey, createdAt, kind, tags, content, sig)
|
||||
}
|
||||
|
@ -49,6 +49,8 @@ interface EventInterface {
|
||||
fun isTaggedAddressableKind(kind: Int): Boolean
|
||||
fun getTagOfAddressableKind(kind: Int): ATag?
|
||||
|
||||
fun expiration(): Long?
|
||||
|
||||
fun hashtags(): List<String>
|
||||
fun geohashes(): List<String>
|
||||
|
||||
@ -66,6 +68,12 @@ interface EventInterface {
|
||||
fun taggedEvents(): List<HexKey>
|
||||
fun taggedUrls(): List<String>
|
||||
|
||||
fun firstTaggedAddress(): ATag?
|
||||
fun firstTaggedUser(): HexKey?
|
||||
fun firstTaggedEvent(): HexKey?
|
||||
fun firstTaggedUrl(): String?
|
||||
|
||||
fun taggedEmojis(): List<EmojiUrl>
|
||||
fun matchTag1With(text: String): Boolean
|
||||
fun isExpired(): Boolean
|
||||
}
|
||||
|
@ -0,0 +1,54 @@
|
||||
package com.vitorpamplona.quartz.events
|
||||
|
||||
import androidx.compose.runtime.Immutable
|
||||
import com.vitorpamplona.quartz.utils.TimeUtils
|
||||
import com.vitorpamplona.quartz.encoders.toHexKey
|
||||
import com.vitorpamplona.quartz.crypto.CryptoUtils
|
||||
import com.vitorpamplona.quartz.encoders.ATag
|
||||
import com.vitorpamplona.quartz.encoders.HexKey
|
||||
|
||||
@Immutable
|
||||
class StatusEvent(
|
||||
id: HexKey,
|
||||
pubKey: HexKey,
|
||||
createdAt: Long,
|
||||
tags: List<List<String>>,
|
||||
content: String,
|
||||
sig: HexKey
|
||||
) : BaseAddressableEvent(id, pubKey, createdAt, kind, tags, content, sig) {
|
||||
|
||||
companion object {
|
||||
const val kind = 30315
|
||||
|
||||
fun create(
|
||||
msg: String,
|
||||
type: String,
|
||||
expiration: Long?,
|
||||
privateKey: ByteArray,
|
||||
createdAt: Long = TimeUtils.now()
|
||||
): StatusEvent {
|
||||
val tags = mutableListOf<List<String>>()
|
||||
|
||||
tags.add(listOf("d", type))
|
||||
expiration?.let { tags.add(listOf("expiration", it.toString())) }
|
||||
|
||||
val pubKey = CryptoUtils.pubkeyCreate(privateKey).toHexKey()
|
||||
val id = generateId(pubKey, createdAt, kind, tags, msg)
|
||||
val sig = CryptoUtils.sign(id, privateKey)
|
||||
return StatusEvent(id.toHexKey(), pubKey, createdAt, tags, msg, sig.toHexKey())
|
||||
}
|
||||
|
||||
fun update(
|
||||
event: StatusEvent,
|
||||
newStatus: String,
|
||||
privateKey: ByteArray,
|
||||
createdAt: Long = TimeUtils.now()
|
||||
): StatusEvent {
|
||||
val tags = event.tags
|
||||
val pubKey = event.pubKey()
|
||||
val id = generateId(pubKey, createdAt, kind, tags, newStatus)
|
||||
val sig = CryptoUtils.sign(id, privateKey)
|
||||
return StatusEvent(id.toHexKey(), pubKey, createdAt, tags, newStatus, sig.toHexKey())
|
||||
}
|
||||
}
|
||||
}
|
@ -13,6 +13,7 @@ object TimeUtils {
|
||||
fun now() = System.currentTimeMillis() / 1000
|
||||
fun fiveMinutesAgo() = now() - fiveMinutes
|
||||
fun oneHourAgo() = now() - oneHour
|
||||
fun oneHourAhead() = now() + oneHour
|
||||
fun oneDayAgo() = now() - oneDay
|
||||
fun eightHoursAgo() = now() - eightHours
|
||||
fun oneWeekAgo() = now() - oneWeek
|
||||
|
Loading…
x
Reference in New Issue
Block a user