Merge branch 'main' into amber

This commit is contained in:
greenart7c3 2023-08-25 05:27:14 -03:00 committed by GitHub
commit a0308938de
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
50 changed files with 1643 additions and 459 deletions

View File

@ -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)

View File

@ -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 {

View File

@ -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"

View File

@ -133,6 +133,7 @@ object ServiceManager {
LocalCache.pruneContactLists(accounts)
LocalCache.pruneRepliesAndReactions(accounts)
LocalCache.prunePastVersionsOfReplaceables()
LocalCache.pruneExpiredEvents()
}
}
}

View File

@ -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

View File

@ -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())
}

View File

@ -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() ||

View File

@ -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()
}
}

View File

@ -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
)
)
}

View File

@ -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) {

View File

@ -120,7 +120,7 @@ fun JoinUserOrChannelView(searchBarViewModel: SearchBarViewModel, onClose: () ->
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically
) {
CloseButton(onCancel = {
CloseButton(onPress = {
searchBarViewModel.clear()
NostrSearchEventOrUserDataSource.clear()
onClose()

View File

@ -51,7 +51,7 @@ fun NewChannelView(onClose: () -> Unit, accountViewModel: AccountViewModel, chan
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically
) {
CloseButton(onCancel = {
CloseButton(onPress = {
postViewModel.clear()
onClose()
})

View File

@ -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()
})

View File

@ -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

View File

@ -157,7 +157,7 @@ fun NewRelayListView(onClose: () -> Unit, accountViewModel: AccountViewModel, re
},
navigationIcon = {
Spacer(modifier = StdHorzSpacer)
CloseButton(onCancel = {
CloseButton(onPress = {
postViewModel.clear()
onClose()
})

View File

@ -97,7 +97,7 @@ fun NewUserMetadataView(onClose: () -> Unit, account: Account) {
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically
) {
CloseButton(onCancel = {
CloseButton(onPress = {
postViewModel.clear()
onClose()
})

View File

@ -74,7 +74,7 @@ fun RelayInformationDialog(
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically
) {
CloseButton(onCancel = {
CloseButton(onPress = {
onClose()
})
}

View File

@ -110,7 +110,7 @@ fun RelaySelectionDialog(
verticalAlignment = Alignment.CenterVertically
) {
CloseButton(
onCancel = {
onPress = {
onClose()
}
)

View File

@ -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() {

View File

@ -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) {

View File

@ -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)
}

View File

@ -95,7 +95,7 @@ fun AddBountyAmountDialog(bounty: Note, accountViewModel: AccountViewModel, onCl
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier.fillMaxWidth()
) {
CloseButton(onCancel = {
CloseButton(onPress = {
postViewModel.cancel()
onClose()
})

View File

@ -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
)
}
}
}
}

View File

@ -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)
}
}

View File

@ -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
)
}
}
}

View File

@ -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)
}

View File

@ -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
)

View File

@ -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)
}
}
}

View File

@ -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)
}

View File

@ -155,7 +155,7 @@ fun UpdateReactionTypeDialog(
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically
) {
CloseButton(onCancel = {
CloseButton(onPress = {
postViewModel.cancel()
onClose()
})

View File

@ -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()
})

View File

@ -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(

View File

@ -94,7 +94,7 @@ fun ZapCustomDialog(onClose: () -> Unit, accountViewModel: AccountViewModel, bas
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically
) {
CloseButton(onCancel = {
CloseButton(onPress = {
postViewModel.cancel()
onClose()
})

View File

@ -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(

View File

@ -16,5 +16,5 @@ class RelayPoolViewModel : ViewModel() {
val isConnected = RelayPool.live.map {
it.relays.connectedRelays() > 0
}
}.distinctUntilChanged()
}

View File

@ -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) {

View File

@ -70,7 +70,7 @@ fun AccountBackupDialog(account: Account, onClose: () -> Unit) {
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically
) {
CloseButton(onCancel = onClose)
CloseButton(onPress = onClose)
}
Column(

View File

@ -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

View File

@ -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()
})

View File

@ -56,7 +56,7 @@ fun ConnectOrbotDialog(onClose: () -> Unit, onPost: () -> Unit, portNumber: Muta
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier.fillMaxWidth()
) {
CloseButton(onCancel = {
CloseButton(onPress = {
onClose()
})

View File

@ -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

View File

@ -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)

View 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/&lt;user&gt;/&lt;gist&gt;</string>
<string name="telegram_proof_url_template" translatable="false">https://t.me/&lt;proof post&gt;</string>
<string name="mastodon_proof_url_template" translatable="false">https://&lt;server&gt;/&lt;user&gt;/&lt;proof post&gt;</string>
<string name="twitter_proof_url_template" translatable="false">https://twitter.com/&lt;user&gt;/status/&lt;proof post&gt;</string>
<string name="private_conversation_notification">"&lt;ไม่สามารถถอดรหัสข้อความส่วนตัวได้&gt;\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">(0100)%</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>

View File

@ -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>

View File

@ -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"/>

View File

@ -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

View File

@ -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)
}

View File

@ -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
}

View File

@ -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())
}
}
}

View File

@ -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