kopia lustrzana https://github.com/vitorpamplona/amethyst
Porównaj commity
74 Commity
dfdae1093f
...
e7fc6e4efe
Autor | SHA1 | Data |
---|---|---|
Tony Giorgio | e7fc6e4efe | |
Vitor Pamplona | 8125a7dabb | |
Vitor Pamplona | e898d58239 | |
Vitor Pamplona | 29a43f82e6 | |
Vitor Pamplona | 469b9c6acb | |
Vitor Pamplona | 38d1bf9aec | |
Vitor Pamplona | 18b57b8ac8 | |
Vitor Pamplona | 7fc43c96d6 | |
Vitor Pamplona | fc7d3a9519 | |
Crowdin Bot | 1667a78bb9 | |
Vitor Pamplona | 5fbd6c25d0 | |
Vitor Pamplona | d079d511e8 | |
Vitor Pamplona | 6e1418cd54 | |
Vitor Pamplona | cd84c07fcc | |
jiftechnify | 6eb2fbfa2f | |
jiftechnify | fc6f460063 | |
jiftechnify | 442cdfdf2a | |
Vitor Pamplona | 539433014e | |
Vitor Pamplona | d3f54a7082 | |
Vitor Pamplona | d3a0ae743a | |
Vitor Pamplona | 6690d5391c | |
Vitor Pamplona | d61d684a27 | |
jiftechnify | 4f84fad0cd | |
jiftechnify | a71ce69cab | |
greenart7c3 | 6e6fa66c53 | |
jiftechnify | bffb9f3778 | |
jiftechnify | e11961695f | |
jiftechnify | 042579ddfb | |
jiftechnify | d0aa7430ca | |
jiftechnify | 3434c31487 | |
greenart7c3 | ed4d867622 | |
greenart7c3 | 27db2b91ab | |
greenart7c3 | a2316b6ed0 | |
greenart7c3 | 62a114b981 | |
greenart7c3 | 8d7a3f4d5e | |
greenart7c3 | c087042f7d | |
greenart7c3 | 644d2fc2bb | |
greenart7c3 | ea33cc77ed | |
greenart7c3 | 090b643f43 | |
greenart7c3 | 220ce75f19 | |
greenart7c3 | bc180ae210 | |
greenart7c3 | 5910ef199f | |
greenart7c3 | 499939ed68 | |
greenart7c3 | 940fa2ee8d | |
greenart7c3 | f6e5af3e98 | |
greenart7c3 | 8b3e3e7af8 | |
greenart7c3 | 84faa7557e | |
greenart7c3 | f7ab925b1d | |
greenart7c3 | 204eaa4606 | |
greenart7c3 | 1c249eed20 | |
greenart7c3 | 3cc32ecd9a | |
greenart7c3 | 0a20d5484b | |
greenart7c3 | 6e4f1269dd | |
greenart7c3 | eba0837e52 | |
greenart7c3 | d682518ddb | |
greenart7c3 | f949d5624e | |
greenart7c3 | 2bc2890d08 | |
greenart7c3 | f3f8bc1b65 | |
greenart7c3 | 99e9514d6c | |
greenart7c3 | 8ade5b7e5f | |
greenart7c3 | e292affbe6 | |
greenart7c3 | fa5d992010 | |
greenart7c3 | 0d47e8b823 | |
greenart7c3 | 91b0d5b7fc | |
greenart7c3 | 4938ba03a6 | |
greenart7c3 | 4d2c17cd1c | |
greenart7c3 | 53987336c0 | |
greenart7c3 | cdd620987b | |
greenart7c3 | 2c086f76e2 | |
greenart7c3 | 99965ecd2d | |
greenart7c3 | ba7c59fdd5 | |
greenart7c3 | 76a93f84c3 | |
greenart7c3 | 26a1624399 | |
Tony Giorgio | 08f1b43908 |
|
@ -84,7 +84,7 @@ jobs:
|
|||
id: create_release
|
||||
uses: actions/create-release@v1
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
GITHUB_TOKEN: ${{ secrets.PERSONAL_TOKEN }}
|
||||
with:
|
||||
tag_name: ${{ github.ref }}
|
||||
release_name: Release ${{ github.ref }}
|
||||
|
@ -96,7 +96,7 @@ jobs:
|
|||
id: upload-release-asset-play-universal-apk
|
||||
uses: actions/upload-release-asset@v1
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
GITHUB_TOKEN: ${{ secrets.PERSONAL_TOKEN }}
|
||||
with:
|
||||
upload_url: ${{ steps.create_release.outputs.upload_url }}
|
||||
asset_path: app/build/outputs/apk/play/release/app-play-universal-release-unsigned-signed.apk
|
||||
|
@ -107,7 +107,7 @@ jobs:
|
|||
id: upload-release-asset-play-x86-apk
|
||||
uses: actions/upload-release-asset@v1
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
GITHUB_TOKEN: ${{ secrets.PERSONAL_TOKEN }}
|
||||
with:
|
||||
upload_url: ${{ steps.create_release.outputs.upload_url }}
|
||||
asset_path: app/build/outputs/apk/play/release/app-play-x86-release-unsigned-signed.apk
|
||||
|
@ -118,7 +118,7 @@ jobs:
|
|||
id: upload-release-asset-play-x86-64-apk
|
||||
uses: actions/upload-release-asset@v1
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
GITHUB_TOKEN: ${{ secrets.PERSONAL_TOKEN }}
|
||||
with:
|
||||
upload_url: ${{ steps.create_release.outputs.upload_url }}
|
||||
asset_path: app/build/outputs/apk/play/release/app-play-x86_64-release-unsigned-signed.apk
|
||||
|
@ -152,7 +152,7 @@ jobs:
|
|||
id: upload-release-asset-fdroid-universal-apk
|
||||
uses: actions/upload-release-asset@v1
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
GITHUB_TOKEN: ${{ secrets.PERSONAL_TOKEN }}
|
||||
with:
|
||||
upload_url: ${{ steps.create_release.outputs.upload_url }}
|
||||
asset_path: app/build/outputs/apk/fdroid/release/app-fdroid-universal-release-unsigned-signed.apk
|
||||
|
@ -163,7 +163,7 @@ jobs:
|
|||
id: upload-release-asset-fdroid-x86-apk
|
||||
uses: actions/upload-release-asset@v1
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
GITHUB_TOKEN: ${{ secrets.PERSONAL_TOKEN }}
|
||||
with:
|
||||
upload_url: ${{ steps.create_release.outputs.upload_url }}
|
||||
asset_path: app/build/outputs/apk/fdroid/release/app-fdroid-x86-release-unsigned-signed.apk
|
||||
|
@ -174,7 +174,7 @@ jobs:
|
|||
id: upload-release-asset-fdroid-x86-64-apk
|
||||
uses: actions/upload-release-asset@v1
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
GITHUB_TOKEN: ${{ secrets.PERSONAL_TOKEN }}
|
||||
with:
|
||||
upload_url: ${{ steps.create_release.outputs.upload_url }}
|
||||
asset_path: app/build/outputs/apk/fdroid/release/app-fdroid-x86_64-release-unsigned-signed.apk
|
||||
|
@ -210,7 +210,7 @@ jobs:
|
|||
id: upload-release-asset-play-aab
|
||||
uses: actions/upload-release-asset@v1
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
GITHUB_TOKEN: ${{ secrets.PERSONAL_TOKEN }}
|
||||
with:
|
||||
upload_url: ${{ steps.create_release.outputs.upload_url }}
|
||||
asset_path: app/build/outputs/bundle/playRelease/app-play-release.aab
|
||||
|
@ -222,7 +222,7 @@ jobs:
|
|||
id: upload-release-asset-fdroid-aab
|
||||
uses: actions/upload-release-asset@v1
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
GITHUB_TOKEN: ${{ secrets.PERSONAL_TOKEN }}
|
||||
with:
|
||||
upload_url: ${{ steps.create_release.outputs.upload_url }}
|
||||
asset_path: app/build/outputs/bundle/fdroidRelease/app-fdroid-release.aab
|
||||
|
|
|
@ -205,9 +205,6 @@ dependencies {
|
|||
// Websockets API
|
||||
implementation libs.okhttp
|
||||
|
||||
// HTML Parsing for Link Preview
|
||||
implementation libs.jsoup
|
||||
|
||||
// Encrypted Key Storage
|
||||
implementation libs.androidx.security.crypto.ktx
|
||||
|
||||
|
|
|
@ -56,10 +56,12 @@ import com.vitorpamplona.quartz.events.ClassifiedsEvent
|
|||
import com.vitorpamplona.quartz.events.Contact
|
||||
import com.vitorpamplona.quartz.events.ContactListEvent
|
||||
import com.vitorpamplona.quartz.events.DeletionEvent
|
||||
import com.vitorpamplona.quartz.events.DraftEvent
|
||||
import com.vitorpamplona.quartz.events.EmojiPackEvent
|
||||
import com.vitorpamplona.quartz.events.EmojiPackSelectionEvent
|
||||
import com.vitorpamplona.quartz.events.EmojiUrl
|
||||
import com.vitorpamplona.quartz.events.Event
|
||||
import com.vitorpamplona.quartz.events.EventInterface
|
||||
import com.vitorpamplona.quartz.events.FileHeaderEvent
|
||||
import com.vitorpamplona.quartz.events.FileServersEvent
|
||||
import com.vitorpamplona.quartz.events.FileStorageEvent
|
||||
|
@ -844,17 +846,18 @@ class Account(
|
|||
}
|
||||
}
|
||||
|
||||
suspend fun delete(note: Note) {
|
||||
return delete(listOf(note))
|
||||
fun delete(note: Note) {
|
||||
delete(listOf(note))
|
||||
}
|
||||
|
||||
suspend fun delete(notes: List<Note>) {
|
||||
fun delete(notes: List<Note>) {
|
||||
if (!isWriteable()) return
|
||||
|
||||
val myNotes = notes.filter { it.author == userProfile() }.mapNotNull { it.event?.id() }
|
||||
val myEvents = notes.filter { it.author == userProfile() }
|
||||
val myNoteVersions = myEvents.mapNotNull { it.event as? Event }
|
||||
|
||||
if (myNotes.isNotEmpty()) {
|
||||
DeletionEvent.create(myNotes, signer) {
|
||||
if (myNoteVersions.isNotEmpty()) {
|
||||
DeletionEvent.create(myNoteVersions, signer) {
|
||||
Client.send(it)
|
||||
LocalCache.justConsume(it, null)
|
||||
}
|
||||
|
@ -929,6 +932,7 @@ class Account(
|
|||
|
||||
fun timestamp(note: Note) {
|
||||
if (!isWriteable()) return
|
||||
if (note.isDraft()) return
|
||||
|
||||
val id = note.event?.id() ?: note.idHex
|
||||
|
||||
|
@ -1318,6 +1322,7 @@ class Account(
|
|||
relayList: List<Relay>? = null,
|
||||
geohash: String? = null,
|
||||
nip94attachments: List<Event>? = null,
|
||||
draftTag: String?,
|
||||
) {
|
||||
if (!isWriteable()) return
|
||||
|
||||
|
@ -1345,14 +1350,26 @@ class Account(
|
|||
geohash = geohash,
|
||||
nip94attachments = nip94attachments,
|
||||
signer = signer,
|
||||
isDraft = draftTag != null,
|
||||
) {
|
||||
Client.send(it, relayList = relayList)
|
||||
LocalCache.justConsume(it, null)
|
||||
if (draftTag != null) {
|
||||
if (message.isBlank()) {
|
||||
deleteDraft(draftTag)
|
||||
} else {
|
||||
DraftEvent.create(draftTag, it, emptyList(), signer) { draftEvent ->
|
||||
Client.send(draftEvent, relayList = relayList)
|
||||
LocalCache.justConsume(draftEvent, null)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
Client.send(it, relayList = relayList)
|
||||
LocalCache.justConsume(it, null)
|
||||
|
||||
replyTo?.forEach { it.event?.let { Client.send(it, relayList = relayList) } }
|
||||
addresses?.forEach {
|
||||
LocalCache.getAddressableNoteIfExists(it.toTag())?.event?.let {
|
||||
Client.send(it, relayList = relayList)
|
||||
replyTo?.forEach { it.event?.let { Client.send(it, relayList = relayList) } }
|
||||
addresses?.forEach {
|
||||
LocalCache.getAddressableNoteIfExists(it.toTag())?.event?.let {
|
||||
Client.send(it, relayList = relayList)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1373,6 +1390,7 @@ class Account(
|
|||
relayList: List<Relay>? = null,
|
||||
geohash: String? = null,
|
||||
nip94attachments: List<FileHeaderEvent>? = null,
|
||||
draftTag: String?,
|
||||
) {
|
||||
if (!isWriteable()) return
|
||||
|
||||
|
@ -1396,26 +1414,52 @@ class Account(
|
|||
nip94attachments = nip94attachments,
|
||||
forkedFrom = forkedFrom,
|
||||
signer = signer,
|
||||
isDraft = draftTag != null,
|
||||
) {
|
||||
Client.send(it, relayList = relayList)
|
||||
LocalCache.justConsume(it, null)
|
||||
|
||||
// broadcast replied notes
|
||||
replyingTo?.let {
|
||||
LocalCache.getNoteIfExists(replyingTo)?.event?.let {
|
||||
Client.send(it, relayList = relayList)
|
||||
if (draftTag != null) {
|
||||
if (message.isBlank()) {
|
||||
deleteDraft(draftTag)
|
||||
} else {
|
||||
DraftEvent.create(draftTag, it, signer) { draftEvent ->
|
||||
Client.send(draftEvent, relayList = relayList)
|
||||
LocalCache.justConsume(draftEvent, null)
|
||||
}
|
||||
}
|
||||
}
|
||||
replyTo?.forEach { it.event?.let { Client.send(it, relayList = relayList) } }
|
||||
addresses?.forEach {
|
||||
LocalCache.getAddressableNoteIfExists(it.toTag())?.event?.let {
|
||||
Client.send(it, relayList = relayList)
|
||||
} else {
|
||||
Client.send(it, relayList = relayList)
|
||||
LocalCache.justConsume(it, null)
|
||||
|
||||
// broadcast replied notes
|
||||
replyingTo?.let {
|
||||
LocalCache.getNoteIfExists(replyingTo)?.event?.let {
|
||||
Client.send(it, relayList = relayList)
|
||||
}
|
||||
}
|
||||
replyTo?.forEach { it.event?.let { Client.send(it, relayList = relayList) } }
|
||||
addresses?.forEach {
|
||||
LocalCache.getAddressableNoteIfExists(it.toTag())?.event?.let {
|
||||
Client.send(it, relayList = relayList)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun sendPost(
|
||||
fun deleteDraft(draftTag: String) {
|
||||
val key = DraftEvent.createAddressTag(userProfile().pubkeyHex, draftTag)
|
||||
LocalCache.getAddressableNoteIfExists(key)?.let {
|
||||
val noteEvent = it.event
|
||||
if (noteEvent is DraftEvent) {
|
||||
noteEvent.createDeletedEvent(signer) {
|
||||
Client.send(it)
|
||||
LocalCache.justConsume(it, null)
|
||||
}
|
||||
}
|
||||
delete(it)
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun sendPost(
|
||||
message: String,
|
||||
replyTo: List<Note>?,
|
||||
mentions: List<User>?,
|
||||
|
@ -1430,6 +1474,7 @@ class Account(
|
|||
relayList: List<Relay>? = null,
|
||||
geohash: String? = null,
|
||||
nip94attachments: List<FileHeaderEvent>? = null,
|
||||
draftTag: String?,
|
||||
) {
|
||||
if (!isWriteable()) return
|
||||
|
||||
|
@ -1453,20 +1498,32 @@ class Account(
|
|||
nip94attachments = nip94attachments,
|
||||
forkedFrom = forkedFrom,
|
||||
signer = signer,
|
||||
isDraft = draftTag != null,
|
||||
) {
|
||||
Client.send(it, relayList = relayList)
|
||||
LocalCache.justConsume(it, null)
|
||||
|
||||
// broadcast replied notes
|
||||
replyingTo?.let {
|
||||
LocalCache.getNoteIfExists(replyingTo)?.event?.let {
|
||||
Client.send(it, relayList = relayList)
|
||||
if (draftTag != null) {
|
||||
if (message.isBlank()) {
|
||||
deleteDraft(draftTag)
|
||||
} else {
|
||||
DraftEvent.create(draftTag, it, signer) { draftEvent ->
|
||||
Client.send(draftEvent, relayList = relayList)
|
||||
LocalCache.justConsume(draftEvent, null)
|
||||
}
|
||||
}
|
||||
}
|
||||
replyTo?.forEach { it.event?.let { Client.send(it, relayList = relayList) } }
|
||||
addresses?.forEach {
|
||||
LocalCache.getAddressableNoteIfExists(it.toTag())?.event?.let {
|
||||
Client.send(it, relayList = relayList)
|
||||
} else {
|
||||
Client.send(it, relayList = relayList)
|
||||
LocalCache.justConsume(it, null)
|
||||
|
||||
// broadcast replied notes
|
||||
replyingTo?.let {
|
||||
LocalCache.getNoteIfExists(replyingTo)?.event?.let {
|
||||
Client.send(it, relayList = relayList)
|
||||
}
|
||||
}
|
||||
replyTo?.forEach { it.event?.let { Client.send(it, relayList = relayList) } }
|
||||
addresses?.forEach {
|
||||
LocalCache.getAddressableNoteIfExists(it.toTag())?.event?.let {
|
||||
Client.send(it, relayList = relayList)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1510,6 +1567,7 @@ class Account(
|
|||
relayList: List<Relay>? = null,
|
||||
geohash: String? = null,
|
||||
nip94attachments: List<FileHeaderEvent>? = null,
|
||||
draftTag: String?,
|
||||
) {
|
||||
if (!isWriteable()) return
|
||||
|
||||
|
@ -1533,15 +1591,27 @@ class Account(
|
|||
zapRaiserAmount = zapRaiserAmount,
|
||||
geohash = geohash,
|
||||
nip94attachments = nip94attachments,
|
||||
isDraft = draftTag != null,
|
||||
) {
|
||||
Client.send(it, relayList = relayList)
|
||||
LocalCache.justConsume(it, null)
|
||||
if (draftTag != null) {
|
||||
if (message.isBlank()) {
|
||||
deleteDraft(draftTag)
|
||||
} else {
|
||||
DraftEvent.create(draftTag, it, signer) { draftEvent ->
|
||||
Client.send(draftEvent, relayList = relayList)
|
||||
LocalCache.justConsume(draftEvent, null)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
Client.send(it, relayList = relayList)
|
||||
LocalCache.justConsume(it, null)
|
||||
|
||||
// Rebroadcast replies and tags to the current relay set
|
||||
replyTo?.forEach { it.event?.let { Client.send(it, relayList = relayList) } }
|
||||
addresses?.forEach {
|
||||
LocalCache.getAddressableNoteIfExists(it.toTag())?.event?.let {
|
||||
Client.send(it, relayList = relayList)
|
||||
// Rebroadcast replies and tags to the current relay set
|
||||
replyTo?.forEach { it.event?.let { Client.send(it, relayList = relayList) } }
|
||||
addresses?.forEach {
|
||||
LocalCache.getAddressableNoteIfExists(it.toTag())?.event?.let {
|
||||
Client.send(it, relayList = relayList)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1557,6 +1627,7 @@ class Account(
|
|||
zapRaiserAmount: Long? = null,
|
||||
geohash: String? = null,
|
||||
nip94attachments: List<FileHeaderEvent>? = null,
|
||||
draftTag: String?,
|
||||
) {
|
||||
if (!isWriteable()) return
|
||||
|
||||
|
@ -1574,9 +1645,21 @@ class Account(
|
|||
geohash = geohash,
|
||||
nip94attachments = nip94attachments,
|
||||
signer = signer,
|
||||
isDraft = draftTag != null,
|
||||
) {
|
||||
Client.send(it)
|
||||
LocalCache.justConsume(it, null)
|
||||
if (draftTag != null) {
|
||||
if (message.isBlank()) {
|
||||
deleteDraft(draftTag)
|
||||
} else {
|
||||
DraftEvent.create(draftTag, it, signer) { draftEvent ->
|
||||
Client.send(draftEvent)
|
||||
LocalCache.justConsume(draftEvent, null)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
Client.send(it)
|
||||
LocalCache.justConsume(it, null)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1590,6 +1673,7 @@ class Account(
|
|||
zapRaiserAmount: Long? = null,
|
||||
geohash: String? = null,
|
||||
nip94attachments: List<FileHeaderEvent>? = null,
|
||||
draftTag: String?,
|
||||
) {
|
||||
if (!isWriteable()) return
|
||||
|
||||
|
@ -1608,9 +1692,21 @@ class Account(
|
|||
geohash = geohash,
|
||||
nip94attachments = nip94attachments,
|
||||
signer = signer,
|
||||
isDraft = draftTag != null,
|
||||
) {
|
||||
Client.send(it)
|
||||
LocalCache.justConsume(it, null)
|
||||
if (draftTag != null) {
|
||||
if (message.isBlank()) {
|
||||
deleteDraft(draftTag)
|
||||
} else {
|
||||
DraftEvent.create(draftTag, it, signer) { draftEvent ->
|
||||
Client.send(draftEvent)
|
||||
LocalCache.justConsume(draftEvent, null)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
Client.send(it)
|
||||
LocalCache.justConsume(it, null)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1624,6 +1720,7 @@ class Account(
|
|||
zapRaiserAmount: Long? = null,
|
||||
geohash: String? = null,
|
||||
nip94attachments: List<FileHeaderEvent>? = null,
|
||||
draftTag: String?,
|
||||
) {
|
||||
sendPrivateMessage(
|
||||
message,
|
||||
|
@ -1635,6 +1732,7 @@ class Account(
|
|||
zapRaiserAmount,
|
||||
geohash,
|
||||
nip94attachments,
|
||||
draftTag,
|
||||
)
|
||||
}
|
||||
|
||||
|
@ -1648,6 +1746,7 @@ class Account(
|
|||
zapRaiserAmount: Long? = null,
|
||||
geohash: String? = null,
|
||||
nip94attachments: List<FileHeaderEvent>? = null,
|
||||
draftTag: String?,
|
||||
) {
|
||||
if (!isWriteable()) return
|
||||
|
||||
|
@ -1667,9 +1766,21 @@ class Account(
|
|||
nip94attachments = nip94attachments,
|
||||
signer = signer,
|
||||
advertiseNip18 = false,
|
||||
isDraft = draftTag != null,
|
||||
) {
|
||||
Client.send(it)
|
||||
LocalCache.consume(it, null)
|
||||
if (draftTag != null) {
|
||||
if (message.isBlank()) {
|
||||
deleteDraft(draftTag)
|
||||
} else {
|
||||
DraftEvent.create(draftTag, it, emptyList(), signer) { draftEvent ->
|
||||
Client.send(draftEvent)
|
||||
LocalCache.justConsume(draftEvent, null)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
Client.send(it)
|
||||
LocalCache.consume(it, null)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1684,6 +1795,7 @@ class Account(
|
|||
zapRaiserAmount: Long? = null,
|
||||
geohash: String? = null,
|
||||
nip94attachments: List<FileHeaderEvent>? = null,
|
||||
draftTag: String? = null,
|
||||
) {
|
||||
if (!isWriteable()) return
|
||||
|
||||
|
@ -1701,9 +1813,21 @@ class Account(
|
|||
zapRaiserAmount = zapRaiserAmount,
|
||||
geohash = geohash,
|
||||
nip94attachments = nip94attachments,
|
||||
draftTag = draftTag,
|
||||
signer = signer,
|
||||
) {
|
||||
broadcastPrivately(it)
|
||||
if (draftTag != null) {
|
||||
if (message.isBlank()) {
|
||||
deleteDraft(draftTag)
|
||||
} else {
|
||||
DraftEvent.create(draftTag, it.msg, emptyList(), signer) { draftEvent ->
|
||||
Client.send(draftEvent)
|
||||
LocalCache.justConsume(draftEvent, null)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
broadcastPrivately(it)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1785,7 +1909,7 @@ class Account(
|
|||
Client.send(event)
|
||||
LocalCache.justConsume(event, null)
|
||||
|
||||
DeletionEvent.create(listOf(event.id), signer) { event2 ->
|
||||
DeletionEvent.createForVersionOnly(listOf(event), signer) { event2 ->
|
||||
Client.send(event2)
|
||||
LocalCache.justConsume(event2, null)
|
||||
}
|
||||
|
@ -1851,6 +1975,7 @@ class Account(
|
|||
isPrivate: Boolean,
|
||||
) {
|
||||
if (!isWriteable()) return
|
||||
if (note.isDraft()) return
|
||||
|
||||
if (note is AddressableNote) {
|
||||
BookmarkListEvent.addReplaceable(
|
||||
|
@ -2217,7 +2342,12 @@ class Account(
|
|||
}
|
||||
|
||||
fun cachedDecryptContent(note: Note): String? {
|
||||
val event = note.event
|
||||
return cachedDecryptContent(note.event)
|
||||
}
|
||||
|
||||
fun cachedDecryptContent(event: EventInterface?): String? {
|
||||
if (event == null) return null
|
||||
|
||||
return if (event is PrivateDmEvent && isWriteable()) {
|
||||
event.cachedContentFor(signer)
|
||||
} else if (event is LnZapRequestEvent && event.isPrivateZap() && isWriteable()) {
|
||||
|
|
|
@ -62,6 +62,7 @@ import com.vitorpamplona.quartz.events.CommunityListEvent
|
|||
import com.vitorpamplona.quartz.events.CommunityPostApprovalEvent
|
||||
import com.vitorpamplona.quartz.events.ContactListEvent
|
||||
import com.vitorpamplona.quartz.events.DeletionEvent
|
||||
import com.vitorpamplona.quartz.events.DraftEvent
|
||||
import com.vitorpamplona.quartz.events.EmojiPackEvent
|
||||
import com.vitorpamplona.quartz.events.EmojiPackSelectionEvent
|
||||
import com.vitorpamplona.quartz.events.Event
|
||||
|
@ -104,6 +105,7 @@ import com.vitorpamplona.quartz.events.TextNoteModificationEvent
|
|||
import com.vitorpamplona.quartz.events.VideoHorizontalEvent
|
||||
import com.vitorpamplona.quartz.events.VideoVerticalEvent
|
||||
import com.vitorpamplona.quartz.events.WikiNoteEvent
|
||||
import com.vitorpamplona.quartz.events.WrappedEvent
|
||||
import com.vitorpamplona.quartz.utils.TimeUtils
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
import kotlinx.collections.immutable.persistentSetOf
|
||||
|
@ -128,7 +130,6 @@ object LocalCache {
|
|||
val users = LargeCache<HexKey, User>()
|
||||
val notes = LargeCache<HexKey, Note>()
|
||||
val addressables = LargeCache<String, AddressableNote>()
|
||||
|
||||
val channels = LargeCache<HexKey, Channel>()
|
||||
val awaitingPaymentRequests = ConcurrentHashMap<HexKey, Pair<Note?, (LnZapPaymentResponseEvent) -> Unit>>(10)
|
||||
|
||||
|
@ -350,7 +351,7 @@ object LocalCache {
|
|||
return
|
||||
}
|
||||
|
||||
val replyTo = event.tagsWithoutCitations().mapNotNull { checkGetOrCreateNote(it) }
|
||||
val replyTo = computeReplyTo(event)
|
||||
|
||||
note.loadEvent(event, author, replyTo)
|
||||
|
||||
|
@ -433,13 +434,7 @@ object LocalCache {
|
|||
return
|
||||
}
|
||||
|
||||
val repository = event.repository()?.toTag()
|
||||
|
||||
val replyTo =
|
||||
event
|
||||
.tagsWithoutCitations()
|
||||
.filter { it != repository }
|
||||
.mapNotNull { checkGetOrCreateNote(it) }
|
||||
val replyTo = computeReplyTo(event)
|
||||
|
||||
// println("New GitReply ${event.id} for ${replyTo.firstOrNull()?.event?.id()} ${event.tagsWithoutCitations().filter { it != event.repository()?.toTag() }.firstOrNull()}")
|
||||
|
||||
|
@ -477,7 +472,7 @@ object LocalCache {
|
|||
return
|
||||
}
|
||||
|
||||
val replyTo = event.tagsWithoutCitations().mapNotNull { checkGetOrCreateNote(it) }
|
||||
val replyTo = computeReplyTo(event)
|
||||
|
||||
if (event.createdAt > (note.createdAt() ?: 0)) {
|
||||
note.loadEvent(event, author, replyTo)
|
||||
|
@ -512,7 +507,7 @@ object LocalCache {
|
|||
return
|
||||
}
|
||||
|
||||
val replyTo = event.tagsWithoutCitations().mapNotNull { checkGetOrCreateNote(it) }
|
||||
val replyTo = computeReplyTo(event)
|
||||
|
||||
if (event.createdAt > (note.createdAt() ?: 0)) {
|
||||
note.loadEvent(event, author, replyTo)
|
||||
|
@ -521,6 +516,58 @@ object LocalCache {
|
|||
}
|
||||
}
|
||||
|
||||
fun computeReplyTo(event: Event): List<Note> {
|
||||
return when (event) {
|
||||
is PollNoteEvent -> event.tagsWithoutCitations().mapNotNull { checkGetOrCreateNote(it) }
|
||||
is WikiNoteEvent -> event.tagsWithoutCitations().mapNotNull { checkGetOrCreateNote(it) }
|
||||
is LongTextNoteEvent -> event.tagsWithoutCitations().mapNotNull { checkGetOrCreateNote(it) }
|
||||
is GitReplyEvent -> event.tagsWithoutCitations().filter { it != event.repository()?.toTag() }.mapNotNull { checkGetOrCreateNote(it) }
|
||||
is TextNoteEvent -> event.tagsWithoutCitations().mapNotNull { checkGetOrCreateNote(it) }
|
||||
is ChatMessageEvent -> event.taggedEvents().mapNotNull { checkGetOrCreateNote(it) }
|
||||
is LnZapEvent ->
|
||||
event.zappedPost().mapNotNull { checkGetOrCreateNote(it) } +
|
||||
event.taggedAddresses().map { getOrCreateAddressableNote(it) } +
|
||||
(event.zapRequest?.taggedAddresses()?.map { getOrCreateAddressableNote(it) } ?: emptyList())
|
||||
is LnZapRequestEvent ->
|
||||
event.zappedPost().mapNotNull { checkGetOrCreateNote(it) } +
|
||||
event.taggedAddresses().map { getOrCreateAddressableNote(it) }
|
||||
is BadgeProfilesEvent ->
|
||||
event.badgeAwardEvents().mapNotNull { checkGetOrCreateNote(it) } +
|
||||
event.badgeAwardDefinitions().map { getOrCreateAddressableNote(it) }
|
||||
is BadgeAwardEvent -> event.awardDefinition().map { getOrCreateAddressableNote(it) }
|
||||
is PrivateDmEvent -> event.taggedEvents().mapNotNull { checkGetOrCreateNote(it) }
|
||||
is RepostEvent ->
|
||||
event.boostedPost().mapNotNull { checkGetOrCreateNote(it) } +
|
||||
event.taggedAddresses().map { getOrCreateAddressableNote(it) }
|
||||
is GenericRepostEvent ->
|
||||
event.boostedPost().mapNotNull { checkGetOrCreateNote(it) } +
|
||||
event.taggedAddresses().map { getOrCreateAddressableNote(it) }
|
||||
is CommunityPostApprovalEvent -> event.approvedEvents().mapNotNull { checkGetOrCreateNote(it) }
|
||||
is ReactionEvent ->
|
||||
event.originalPost().mapNotNull { checkGetOrCreateNote(it) } +
|
||||
event.taggedAddresses().map { getOrCreateAddressableNote(it) }
|
||||
is ReportEvent ->
|
||||
event.reportedPost().mapNotNull { checkGetOrCreateNote(it.key) } +
|
||||
event.taggedAddresses().map { getOrCreateAddressableNote(it) }
|
||||
is ChannelMessageEvent ->
|
||||
event
|
||||
.tagsWithoutCitations()
|
||||
.filter { it != event.channel() }
|
||||
.mapNotNull { checkGetOrCreateNote(it) }
|
||||
is LiveActivitiesChatMessageEvent ->
|
||||
event
|
||||
.tagsWithoutCitations()
|
||||
.filter { it != event.activity()?.toTag() }
|
||||
.mapNotNull { checkGetOrCreateNote(it) }
|
||||
|
||||
is DraftEvent -> {
|
||||
event.taggedEvents().mapNotNull { checkGetOrCreateNote(it) } + event.taggedAddresses().mapNotNull { checkGetOrCreateAddressableNote(it.toTag()) }
|
||||
}
|
||||
|
||||
else -> emptyList<Note>()
|
||||
}
|
||||
}
|
||||
|
||||
fun consume(
|
||||
event: PollNoteEvent,
|
||||
relay: Relay? = null,
|
||||
|
@ -541,7 +588,7 @@ object LocalCache {
|
|||
return
|
||||
}
|
||||
|
||||
val replyTo = event.tagsWithoutCitations().mapNotNull { checkGetOrCreateNote(it) }
|
||||
val replyTo = computeReplyTo(event)
|
||||
|
||||
note.loadEvent(event, author, replyTo)
|
||||
|
||||
|
@ -762,9 +809,7 @@ object LocalCache {
|
|||
// Already processed this event.
|
||||
if (note.event?.id() == event.id()) return
|
||||
|
||||
val replyTo =
|
||||
event.badgeAwardEvents().mapNotNull { checkGetOrCreateNote(it) } +
|
||||
event.badgeAwardDefinitions().map { getOrCreateAddressableNote(it) }
|
||||
val replyTo = computeReplyTo(event)
|
||||
|
||||
if (event.createdAt > (note.createdAt() ?: 0)) {
|
||||
note.loadEvent(event, author, replyTo)
|
||||
|
@ -783,7 +828,7 @@ object LocalCache {
|
|||
// ${formattedDateTime(event.createdAt)}")
|
||||
|
||||
val author = getOrCreateUser(event.pubKey)
|
||||
val awardDefinition = event.awardDefinition().map { getOrCreateAddressableNote(it) }
|
||||
val awardDefinition = computeReplyTo(event)
|
||||
|
||||
note.loadEvent(event, author, awardDefinition)
|
||||
|
||||
|
@ -843,6 +888,8 @@ object LocalCache {
|
|||
val note = getOrCreateAddressableNote(event.address())
|
||||
val author = getOrCreateUser(event.pubKey)
|
||||
|
||||
val replyTos = computeReplyTo(event)
|
||||
|
||||
if (version.event == null) {
|
||||
version.loadEvent(event, author, emptyList())
|
||||
version.moveAllReferencesTo(note)
|
||||
|
@ -857,7 +904,7 @@ object LocalCache {
|
|||
if (note.event?.id() == event.id()) return
|
||||
|
||||
if (event.createdAt > (note.createdAt() ?: 0)) {
|
||||
note.loadEvent(event, author, emptyList())
|
||||
note.loadEvent(event, author, replyTos)
|
||||
|
||||
refreshObservers(note)
|
||||
}
|
||||
|
@ -894,7 +941,7 @@ object LocalCache {
|
|||
|
||||
// Log.d("PM", "${author.toBestDisplayName()} to ${recipient?.toBestDisplayName()}")
|
||||
|
||||
val repliesTo = event.taggedEvents().mapNotNull { checkGetOrCreateNote(it) }
|
||||
val repliesTo = computeReplyTo(event)
|
||||
|
||||
note.loadEvent(event, author, repliesTo)
|
||||
|
||||
|
@ -918,52 +965,112 @@ object LocalCache {
|
|||
// must be the same author
|
||||
if (deleteNote.author?.pubkeyHex == event.pubKey) {
|
||||
// reverts the add
|
||||
val mentions =
|
||||
deleteNote.event
|
||||
?.tags()
|
||||
?.filter { it.firstOrNull() == "p" }
|
||||
?.mapNotNull { it.getOrNull(1) }
|
||||
?.mapNotNull { checkGetOrCreateUser(it) }
|
||||
|
||||
mentions?.forEach { user -> user.removeReport(deleteNote) }
|
||||
|
||||
// Counts the replies
|
||||
deleteNote.replyTo?.forEach { masterNote ->
|
||||
masterNote.removeReply(deleteNote)
|
||||
masterNote.removeBoost(deleteNote)
|
||||
masterNote.removeReaction(deleteNote)
|
||||
masterNote.removeZap(deleteNote)
|
||||
masterNote.removeZapPayment(deleteNote)
|
||||
masterNote.removeReport(deleteNote)
|
||||
}
|
||||
|
||||
deleteNote.channelHex()?.let { getChannelIfExists(it)?.removeNote(deleteNote) }
|
||||
|
||||
(deleteNote.event as? LiveActivitiesChatMessageEvent)?.activity()?.let {
|
||||
getChannelIfExists(it.toTag())?.removeNote(deleteNote)
|
||||
}
|
||||
|
||||
if (deleteNote.event is PrivateDmEvent) {
|
||||
val author = deleteNote.author
|
||||
val recipient =
|
||||
(deleteNote.event as? PrivateDmEvent)?.verifiedRecipientPubKey()?.let {
|
||||
checkGetOrCreateUser(it)
|
||||
}
|
||||
|
||||
if (recipient != null && author != null) {
|
||||
author.removeMessage(recipient, deleteNote)
|
||||
recipient.removeMessage(author, deleteNote)
|
||||
}
|
||||
}
|
||||
|
||||
notes.remove(deleteNote.idHex)
|
||||
deleteNote(deleteNote)
|
||||
|
||||
deletedAtLeastOne = true
|
||||
}
|
||||
}
|
||||
|
||||
val addressList = event.deleteAddresses()
|
||||
val addressSet = addressList.toSet()
|
||||
|
||||
addressList
|
||||
.mapNotNull { getAddressableNoteIfExists(it.toTag()) }
|
||||
.forEach { deleteNote ->
|
||||
// must be the same author
|
||||
if (deleteNote.author?.pubkeyHex == event.pubKey && (deleteNote.createdAt() ?: 0) < event.createdAt) {
|
||||
// Counts the replies
|
||||
deleteNote(deleteNote)
|
||||
|
||||
addressables.remove(deleteNote.idHex)
|
||||
|
||||
deletedAtLeastOne = true
|
||||
}
|
||||
}
|
||||
|
||||
notes.forEach { key, note ->
|
||||
val noteEvent = note.event
|
||||
if (noteEvent is AddressableEvent && noteEvent.address() in addressSet) {
|
||||
if (noteEvent.pubKey() == event.pubKey && noteEvent.createdAt() <= event.createdAt) {
|
||||
deleteNote(note)
|
||||
deletedAtLeastOne = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (deletedAtLeastOne) {
|
||||
// refreshObservers()
|
||||
val note = Note(event.id)
|
||||
note.loadEvent(event, getOrCreateUser(event.pubKey), emptyList())
|
||||
refreshObservers(note)
|
||||
}
|
||||
}
|
||||
|
||||
private fun deleteNote(deleteNote: Note) {
|
||||
val deletedEvent = deleteNote.event
|
||||
|
||||
val mentions =
|
||||
deleteNote.event
|
||||
?.tags()
|
||||
?.filter { it.firstOrNull() == "p" }
|
||||
?.mapNotNull { it.getOrNull(1) }
|
||||
?.mapNotNull { checkGetOrCreateUser(it) }
|
||||
|
||||
mentions?.forEach { user -> user.removeReport(deleteNote) }
|
||||
|
||||
// Counts the replies
|
||||
deleteNote.replyTo?.forEach { masterNote ->
|
||||
masterNote.removeReply(deleteNote)
|
||||
masterNote.removeBoost(deleteNote)
|
||||
masterNote.removeReaction(deleteNote)
|
||||
masterNote.removeZap(deleteNote)
|
||||
masterNote.removeZapPayment(deleteNote)
|
||||
masterNote.removeReport(deleteNote)
|
||||
}
|
||||
|
||||
deleteNote.channelHex()?.let { getChannelIfExists(it)?.removeNote(deleteNote) }
|
||||
|
||||
(deletedEvent as? LiveActivitiesChatMessageEvent)?.activity()?.let {
|
||||
getChannelIfExists(it.toTag())?.removeNote(deleteNote)
|
||||
}
|
||||
|
||||
if (deletedEvent is PrivateDmEvent) {
|
||||
val author = deleteNote.author
|
||||
val recipient =
|
||||
deletedEvent.verifiedRecipientPubKey()?.let {
|
||||
checkGetOrCreateUser(it)
|
||||
}
|
||||
|
||||
if (recipient != null && author != null) {
|
||||
author.removeMessage(recipient, deleteNote)
|
||||
recipient.removeMessage(author, deleteNote)
|
||||
}
|
||||
}
|
||||
|
||||
if (deletedEvent is DraftEvent) {
|
||||
deletedEvent.allCache().forEach {
|
||||
it?.let {
|
||||
deindexDraftAsRealEvent(deleteNote, it)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (deletedEvent is WrappedEvent) {
|
||||
deleteWraps(deletedEvent)
|
||||
}
|
||||
|
||||
notes.remove(deleteNote.idHex)
|
||||
}
|
||||
|
||||
fun deleteWraps(event: WrappedEvent) {
|
||||
event.host?.let {
|
||||
// seal
|
||||
getNoteIfExists(it.id)?.let {
|
||||
val noteEvent = it.event
|
||||
if (noteEvent is WrappedEvent) {
|
||||
deleteWraps(noteEvent)
|
||||
}
|
||||
}
|
||||
notes.remove(it.id)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -977,9 +1084,7 @@ object LocalCache {
|
|||
// ${formattedDateTime(event.createdAt)}")
|
||||
|
||||
val author = getOrCreateUser(event.pubKey)
|
||||
val repliesTo =
|
||||
event.boostedPost().mapNotNull { checkGetOrCreateNote(it) } +
|
||||
event.taggedAddresses().map { getOrCreateAddressableNote(it) }
|
||||
val repliesTo = computeReplyTo(event)
|
||||
|
||||
note.loadEvent(event, author, repliesTo)
|
||||
|
||||
|
@ -999,9 +1104,7 @@ object LocalCache {
|
|||
// ${formattedDateTime(event.createdAt)}")
|
||||
|
||||
val author = getOrCreateUser(event.pubKey)
|
||||
val repliesTo =
|
||||
event.boostedPost().mapNotNull { checkGetOrCreateNote(it) } +
|
||||
event.taggedAddresses().map { getOrCreateAddressableNote(it) }
|
||||
val repliesTo = computeReplyTo(event)
|
||||
|
||||
note.loadEvent(event, author, repliesTo)
|
||||
|
||||
|
@ -1023,7 +1126,7 @@ object LocalCache {
|
|||
val author = getOrCreateUser(event.pubKey)
|
||||
|
||||
val communities = event.communities()
|
||||
val eventsApproved = event.approvedEvents().mapNotNull { checkGetOrCreateNote(it) }
|
||||
val eventsApproved = computeReplyTo(event)
|
||||
|
||||
val repliesTo = communities.map { getOrCreateAddressableNote(it) }
|
||||
|
||||
|
@ -1042,9 +1145,7 @@ object LocalCache {
|
|||
if (note.event != null) return
|
||||
|
||||
val author = getOrCreateUser(event.pubKey)
|
||||
val repliesTo =
|
||||
event.originalPost().mapNotNull { checkGetOrCreateNote(it) } +
|
||||
event.taggedAddresses().map { getOrCreateAddressableNote(it) }
|
||||
val repliesTo = computeReplyTo(event)
|
||||
|
||||
note.loadEvent(event, author, repliesTo)
|
||||
|
||||
|
@ -1072,9 +1173,7 @@ object LocalCache {
|
|||
if (note.event != null) return
|
||||
|
||||
val mentions = event.reportedAuthor().mapNotNull { checkGetOrCreateUser(it.key) }
|
||||
val repliesTo =
|
||||
event.reportedPost().mapNotNull { checkGetOrCreateNote(it.key) } +
|
||||
event.taggedAddresses().map { getOrCreateAddressableNote(it) }
|
||||
val repliesTo = computeReplyTo(event)
|
||||
|
||||
note.loadEvent(event, author, repliesTo)
|
||||
|
||||
|
@ -1173,11 +1272,7 @@ object LocalCache {
|
|||
return
|
||||
}
|
||||
|
||||
val replyTo =
|
||||
event
|
||||
.tagsWithoutCitations()
|
||||
.filter { it != event.channel() }
|
||||
.mapNotNull { checkGetOrCreateNote(it) }
|
||||
val replyTo = computeReplyTo(event)
|
||||
|
||||
note.loadEvent(event, author, replyTo)
|
||||
|
||||
|
@ -1216,11 +1311,7 @@ object LocalCache {
|
|||
return
|
||||
}
|
||||
|
||||
val replyTo =
|
||||
event
|
||||
.tagsWithoutCitations()
|
||||
.filter { it != event.activity()?.toTag() }
|
||||
.mapNotNull { checkGetOrCreateNote(it) }
|
||||
val replyTo = computeReplyTo(event)
|
||||
|
||||
note.loadEvent(event, author, replyTo)
|
||||
|
||||
|
@ -1250,15 +1341,7 @@ object LocalCache {
|
|||
|
||||
val author = getOrCreateUser(event.pubKey)
|
||||
val mentions = event.zappedAuthor().mapNotNull { checkGetOrCreateUser(it) }
|
||||
val repliesTo =
|
||||
event.zappedPost().mapNotNull { checkGetOrCreateNote(it) } +
|
||||
event.taggedAddresses().map { getOrCreateAddressableNote(it) } +
|
||||
(
|
||||
(zapRequest.event as? LnZapRequestEvent)?.taggedAddresses()?.map {
|
||||
getOrCreateAddressableNote(it)
|
||||
}
|
||||
?: emptySet<Note>()
|
||||
)
|
||||
val repliesTo = computeReplyTo(event)
|
||||
|
||||
note.loadEvent(event, author, repliesTo)
|
||||
|
||||
|
@ -1279,9 +1362,7 @@ object LocalCache {
|
|||
|
||||
val author = getOrCreateUser(event.pubKey)
|
||||
val mentions = event.zappedAuthor().mapNotNull { checkGetOrCreateUser(it) }
|
||||
val repliesTo =
|
||||
event.zappedPost().mapNotNull { checkGetOrCreateNote(it) } +
|
||||
event.taggedAddresses().map { getOrCreateAddressableNote(it) }
|
||||
val repliesTo = computeReplyTo(event)
|
||||
|
||||
note.loadEvent(event, author, repliesTo)
|
||||
|
||||
|
@ -1483,7 +1564,7 @@ object LocalCache {
|
|||
|
||||
// Log.d("PM", "${author.toBestDisplayName()} to ${recipient?.toBestDisplayName()}")
|
||||
|
||||
val repliesTo = event.taggedEvents().mapNotNull { checkGetOrCreateNote(it) }
|
||||
val repliesTo = computeReplyTo(event)
|
||||
|
||||
note.loadEvent(event, author, repliesTo)
|
||||
|
||||
|
@ -2013,6 +2094,118 @@ object LocalCache {
|
|||
}
|
||||
}
|
||||
|
||||
private fun consume(
|
||||
event: DraftEvent,
|
||||
relay: Relay?,
|
||||
) {
|
||||
if (!event.isDeleted()) {
|
||||
consumeBaseReplaceable(event, relay)
|
||||
|
||||
event.allCache().forEach {
|
||||
it?.let {
|
||||
indexDraftAsRealEvent(event, it)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun indexDraftAsRealEvent(
|
||||
draftWrap: DraftEvent,
|
||||
draft: Event,
|
||||
) {
|
||||
val note = getOrCreateAddressableNote(draftWrap.address())
|
||||
val author = getOrCreateUser(draftWrap.pubKey)
|
||||
|
||||
when (draft) {
|
||||
is PrivateDmEvent -> {
|
||||
draft.verifiedRecipientPubKey()?.let { getOrCreateUser(it) }?.let { recipient ->
|
||||
author.addMessage(recipient, note)
|
||||
recipient.addMessage(author, note)
|
||||
}
|
||||
}
|
||||
is ChatMessageEvent -> {
|
||||
val recipientsHex = draft.recipientsPubKey().plus(draftWrap.pubKey).toSet()
|
||||
val recipients = recipientsHex.mapNotNull { checkGetOrCreateUser(it) }.toSet()
|
||||
|
||||
if (recipients.isNotEmpty()) {
|
||||
recipients.forEach {
|
||||
val groupMinusRecipient = recipientsHex.minus(it.pubkeyHex)
|
||||
|
||||
val authorGroup =
|
||||
if (groupMinusRecipient.isEmpty()) {
|
||||
// note to self
|
||||
ChatroomKey(persistentSetOf(it.pubkeyHex))
|
||||
} else {
|
||||
ChatroomKey(groupMinusRecipient.toImmutableSet())
|
||||
}
|
||||
|
||||
it.addMessage(authorGroup, note)
|
||||
}
|
||||
}
|
||||
}
|
||||
is ChannelMessageEvent -> {
|
||||
draft.channel()?.let { channelId ->
|
||||
checkGetOrCreateChannel(channelId)?.let { channel ->
|
||||
channel.addNote(note)
|
||||
}
|
||||
}
|
||||
}
|
||||
is TextNoteEvent -> {
|
||||
val replyTo = computeReplyTo(draft)
|
||||
val author = getOrCreateUser(draftWrap.pubKey)
|
||||
note.loadEvent(draftWrap, author, replyTo)
|
||||
replyTo.forEach { it.addReply(note) }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun deindexDraftAsRealEvent(
|
||||
draftWrap: Note,
|
||||
draft: Event,
|
||||
) {
|
||||
val author = draftWrap.author ?: return
|
||||
|
||||
when (draft) {
|
||||
is PrivateDmEvent -> {
|
||||
draft.verifiedRecipientPubKey()?.let { getOrCreateUser(it) }?.let { recipient ->
|
||||
author.removeMessage(recipient, draftWrap)
|
||||
recipient.removeMessage(author, draftWrap)
|
||||
}
|
||||
}
|
||||
is ChatMessageEvent -> {
|
||||
val recipientsHex = draft.recipientsPubKey().plus(author.pubkeyHex).toSet()
|
||||
val recipients = recipientsHex.mapNotNull { checkGetOrCreateUser(it) }.toSet()
|
||||
|
||||
if (recipients.isNotEmpty()) {
|
||||
recipients.forEach {
|
||||
val groupMinusRecipient = recipientsHex.minus(it.pubkeyHex)
|
||||
|
||||
val authorGroup =
|
||||
if (groupMinusRecipient.isEmpty()) {
|
||||
// note to self
|
||||
ChatroomKey(persistentSetOf(it.pubkeyHex))
|
||||
} else {
|
||||
ChatroomKey(groupMinusRecipient.toImmutableSet())
|
||||
}
|
||||
|
||||
it.removeMessage(authorGroup, draftWrap)
|
||||
}
|
||||
}
|
||||
}
|
||||
is ChannelMessageEvent -> {
|
||||
draft.channel()?.let { channelId ->
|
||||
checkGetOrCreateChannel(channelId)?.let { channel ->
|
||||
channel.removeNote(draftWrap)
|
||||
}
|
||||
}
|
||||
}
|
||||
is TextNoteEvent -> {
|
||||
val replyTo = computeReplyTo(draft)
|
||||
replyTo.forEach { it.removeReply(draftWrap) }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun justConsume(
|
||||
event: Event,
|
||||
relay: Relay?,
|
||||
|
@ -2050,6 +2243,7 @@ object LocalCache {
|
|||
}
|
||||
is ContactListEvent -> consume(event)
|
||||
is DeletionEvent -> consume(event)
|
||||
is DraftEvent -> consume(event, relay)
|
||||
is EmojiPackEvent -> consume(event, relay)
|
||||
is EmojiPackSelectionEvent -> consume(event, relay)
|
||||
is SealedGossipEvent -> consume(event, relay)
|
||||
|
|
|
@ -47,6 +47,7 @@ import com.vitorpamplona.quartz.events.ChannelCreateEvent
|
|||
import com.vitorpamplona.quartz.events.ChannelMessageEvent
|
||||
import com.vitorpamplona.quartz.events.ChannelMetadataEvent
|
||||
import com.vitorpamplona.quartz.events.CommunityPostApprovalEvent
|
||||
import com.vitorpamplona.quartz.events.DraftEvent
|
||||
import com.vitorpamplona.quartz.events.Event
|
||||
import com.vitorpamplona.quartz.events.EventInterface
|
||||
import com.vitorpamplona.quartz.events.GenericRepostEvent
|
||||
|
@ -97,6 +98,14 @@ class AddressableNote(val address: ATag) : Note(address.toTag()) {
|
|||
fun dTag(): String? {
|
||||
return (event as? AddressableEvent)?.dTag()
|
||||
}
|
||||
|
||||
override fun wasOrShouldBeDeletedBy(
|
||||
deletionEvents: Set<HexKey>,
|
||||
deletionAddressables: Set<ATag>,
|
||||
): Boolean {
|
||||
val thisEvent = event
|
||||
return deletionAddressables.contains(address) || (thisEvent != null && deletionEvents.contains(thisEvent.id()))
|
||||
}
|
||||
}
|
||||
|
||||
@Stable
|
||||
|
@ -184,6 +193,8 @@ open class Note(val idHex: String) {
|
|||
|
||||
open fun createdAt() = event?.createdAt()
|
||||
|
||||
fun isDraft() = event is DraftEvent
|
||||
|
||||
fun loadEvent(
|
||||
event: Event,
|
||||
author: User,
|
||||
|
@ -928,6 +939,14 @@ open class Note(val idHex: String) {
|
|||
createOrDestroyFlowSync(false)
|
||||
}
|
||||
}
|
||||
|
||||
open fun wasOrShouldBeDeletedBy(
|
||||
deletionEvents: Set<HexKey>,
|
||||
deletionAddressables: Set<ATag>,
|
||||
): Boolean {
|
||||
val thisEvent = event
|
||||
return deletionEvents.contains(idHex) || (thisEvent is AddressableEvent && deletionAddressables.contains(thisEvent.address()))
|
||||
}
|
||||
}
|
||||
|
||||
@Stable
|
||||
|
|
|
@ -21,6 +21,8 @@
|
|||
package com.vitorpamplona.amethyst.model
|
||||
|
||||
import com.vitorpamplona.amethyst.service.checkNotInMainThread
|
||||
import com.vitorpamplona.quartz.encoders.ATag
|
||||
import com.vitorpamplona.quartz.events.AddressableEvent
|
||||
import com.vitorpamplona.quartz.events.GenericRepostEvent
|
||||
import com.vitorpamplona.quartz.events.RepostEvent
|
||||
import kotlin.time.measureTimedValue
|
||||
|
@ -78,7 +80,7 @@ class ThreadAssembler {
|
|||
val note = LocalCache.checkGetOrCreateNote(noteId) ?: return emptySet<Note>()
|
||||
|
||||
if (note.event != null) {
|
||||
val thread = mutableSetOf<Note>()
|
||||
val thread = OnlyLatestVersionSet()
|
||||
|
||||
val threadRoot = searchRoot(note, thread) ?: note
|
||||
|
||||
|
@ -87,7 +89,7 @@ class ThreadAssembler {
|
|||
// did not added them.
|
||||
note.replies.forEach { loadDown(it, thread) }
|
||||
|
||||
thread.toSet()
|
||||
thread
|
||||
} else {
|
||||
setOf(note)
|
||||
}
|
||||
|
@ -109,3 +111,87 @@ class ThreadAssembler {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
class OnlyLatestVersionSet : MutableSet<Note> {
|
||||
val map = hashMapOf<ATag, Long>()
|
||||
val set = hashSetOf<Note>()
|
||||
|
||||
override fun add(element: Note): Boolean {
|
||||
val loadedCreatedAt = element.createdAt()
|
||||
val noteEvent = element.event
|
||||
|
||||
return if (element is AddressableNote && loadedCreatedAt != null) {
|
||||
innerAdd(element.address, element, loadedCreatedAt)
|
||||
} else if (noteEvent is AddressableEvent && loadedCreatedAt != null) {
|
||||
innerAdd(noteEvent.address(), element, loadedCreatedAt)
|
||||
} else {
|
||||
set.add(element)
|
||||
}
|
||||
}
|
||||
|
||||
private fun innerAdd(
|
||||
address: ATag,
|
||||
element: Note,
|
||||
loadedCreatedAt: Long,
|
||||
): Boolean {
|
||||
val existing = map.get(address)
|
||||
return if (existing == null) {
|
||||
map.put(address, loadedCreatedAt)
|
||||
set.add(element)
|
||||
} else {
|
||||
if (loadedCreatedAt > existing) {
|
||||
map.put(address, loadedCreatedAt)
|
||||
set.add(element)
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun addAll(elements: Collection<Note>): Boolean {
|
||||
return elements.map { add(it) }.any()
|
||||
}
|
||||
|
||||
override val size: Int
|
||||
get() = set.size
|
||||
|
||||
override fun clear() {
|
||||
set.clear()
|
||||
map.clear()
|
||||
}
|
||||
|
||||
override fun isEmpty(): Boolean {
|
||||
return set.isEmpty()
|
||||
}
|
||||
|
||||
override fun containsAll(elements: Collection<Note>): Boolean {
|
||||
return set.containsAll(elements)
|
||||
}
|
||||
|
||||
override fun contains(element: Note): Boolean {
|
||||
return set.contains(element)
|
||||
}
|
||||
|
||||
override fun iterator(): MutableIterator<Note> {
|
||||
return set.iterator()
|
||||
}
|
||||
|
||||
override fun retainAll(elements: Collection<Note>): Boolean {
|
||||
return set.retainAll(elements)
|
||||
}
|
||||
|
||||
override fun removeAll(elements: Collection<Note>): Boolean {
|
||||
return elements.map { remove(it) }.any()
|
||||
}
|
||||
|
||||
override fun remove(element: Note): Boolean {
|
||||
element.address()?.let {
|
||||
map.remove(it)
|
||||
}
|
||||
(element.event as? AddressableEvent)?.address()?.let {
|
||||
map.remove(it)
|
||||
}
|
||||
|
||||
return set.remove(element)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -277,6 +277,18 @@ class User(val pubkeyHex: String) {
|
|||
}
|
||||
}
|
||||
|
||||
fun removeMessage(
|
||||
room: ChatroomKey,
|
||||
msg: Note,
|
||||
) {
|
||||
checkNotInMainThread()
|
||||
val privateChatroom = getOrCreatePrivateChatroom(room)
|
||||
if (msg in privateChatroom.roomMessages) {
|
||||
privateChatroom.removeMessageSync(msg)
|
||||
liveSet?.innerMessages?.invalidateData()
|
||||
}
|
||||
}
|
||||
|
||||
fun addRelayBeingUsed(
|
||||
relay: Relay,
|
||||
eventTime: Long,
|
||||
|
|
|
@ -200,8 +200,6 @@ class Nip96Uploader(val account: Account?) {
|
|||
|
||||
nip98Header(server.apiUrl)?.let { requestBuilder.addHeader("Authorization", it) }
|
||||
|
||||
println(server.apiUrl.removeSuffix("/") + "/$hash.$extension")
|
||||
|
||||
val request =
|
||||
requestBuilder
|
||||
.header("User-Agent", "Amethyst/${BuildConfig.VERSION_NAME}")
|
||||
|
|
|
@ -40,6 +40,7 @@ import com.vitorpamplona.quartz.events.CalendarRSVPEvent
|
|||
import com.vitorpamplona.quartz.events.CalendarTimeSlotEvent
|
||||
import com.vitorpamplona.quartz.events.ChannelMessageEvent
|
||||
import com.vitorpamplona.quartz.events.ContactListEvent
|
||||
import com.vitorpamplona.quartz.events.DraftEvent
|
||||
import com.vitorpamplona.quartz.events.EmojiPackSelectionEvent
|
||||
import com.vitorpamplona.quartz.events.Event
|
||||
import com.vitorpamplona.quartz.events.EventInterface
|
||||
|
@ -144,7 +145,7 @@ object NostrAccountDataSource : NostrDataSource("AccountData") {
|
|||
types = COMMON_FEED_TYPES,
|
||||
filter =
|
||||
JsonFilter(
|
||||
kinds = listOf(ReportEvent.KIND),
|
||||
kinds = listOf(DraftEvent.KIND, ReportEvent.KIND),
|
||||
authors = listOf(account.userProfile().pubkeyHex),
|
||||
since =
|
||||
latestEOSEs.users[account.userProfile()]
|
||||
|
@ -262,22 +263,53 @@ object NostrAccountDataSource : NostrDataSource("AccountData") {
|
|||
checkNotInMainThread()
|
||||
|
||||
if (LocalCache.justVerify(event)) {
|
||||
if (event is GiftWrapEvent) {
|
||||
// Avoid decrypting over and over again if the event already exist.
|
||||
val note = LocalCache.getNoteIfExists(event.id)
|
||||
if (note != null && relay.brief in note.relays) return
|
||||
when (event) {
|
||||
is DraftEvent -> {
|
||||
// Avoid decrypting over and over again if the event already exist.
|
||||
|
||||
event.cachedGift(account.signer) { this.consume(it, relay) }
|
||||
}
|
||||
if (!event.isDeleted()) {
|
||||
val note = LocalCache.getNoteIfExists(event.id)
|
||||
if (note != null && relay.brief in note.relays) return
|
||||
|
||||
if (event is SealedGossipEvent) {
|
||||
// Avoid decrypting over and over again if the event already exist.
|
||||
val note = LocalCache.getNoteIfExists(event.id)
|
||||
if (note != null && relay.brief in note.relays) return
|
||||
// decrypts
|
||||
event.cachedDraft(account.signer) {}
|
||||
|
||||
event.cachedGossip(account.signer) { LocalCache.justConsume(it, relay) }
|
||||
} else {
|
||||
LocalCache.justConsume(event, relay)
|
||||
LocalCache.justConsume(event, relay)
|
||||
}
|
||||
}
|
||||
|
||||
is GiftWrapEvent -> {
|
||||
// Avoid decrypting over and over again if the event already exist.
|
||||
val note = LocalCache.getNoteIfExists(event.id)
|
||||
if (note != null && relay.brief in note.relays) return
|
||||
|
||||
event.cachedGift(account.signer) { this.consume(it, relay) }
|
||||
}
|
||||
|
||||
is SealedGossipEvent -> {
|
||||
// Avoid decrypting over and over again if the event already exist.
|
||||
val note = LocalCache.getNoteIfExists(event.id)
|
||||
if (note != null && relay.brief in note.relays) return
|
||||
|
||||
event.cachedGossip(account.signer) { LocalCache.justConsume(it, relay) }
|
||||
}
|
||||
|
||||
is LnZapEvent -> {
|
||||
// Avoid decrypting over and over again if the event already exist.
|
||||
|
||||
val note = LocalCache.getNoteIfExists(event.id)
|
||||
if (note != null && relay.brief in note.relays) return
|
||||
|
||||
event.zapRequest?.let {
|
||||
if (it.isPrivateZap()) {
|
||||
it.decryptPrivateZap(account.signer) {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
else -> {
|
||||
LocalCache.justConsume(event, relay)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -26,6 +26,7 @@ import com.vitorpamplona.amethyst.service.relays.Client
|
|||
import com.vitorpamplona.amethyst.service.relays.Relay
|
||||
import com.vitorpamplona.amethyst.service.relays.Subscription
|
||||
import com.vitorpamplona.amethyst.ui.components.BundledUpdate
|
||||
import com.vitorpamplona.quartz.events.AddressableEvent
|
||||
import com.vitorpamplona.quartz.events.Event
|
||||
import com.vitorpamplona.quartz.utils.TimeUtils
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
|
@ -293,7 +294,13 @@ abstract class NostrDataSource(val debugName: String) {
|
|||
eventId: String,
|
||||
relay: Relay,
|
||||
) {
|
||||
LocalCache.getNoteIfExists(eventId)?.addRelay(relay)
|
||||
val note = LocalCache.getNoteIfExists(eventId)
|
||||
val noteEvent = note?.event
|
||||
if (noteEvent is AddressableEvent) {
|
||||
LocalCache.getAddressableNoteIfExists(noteEvent.address().toTag())?.addRelay(relay)
|
||||
} else {
|
||||
note?.addRelay(relay)
|
||||
}
|
||||
}
|
||||
|
||||
open fun markAsEOSE(
|
||||
|
|
|
@ -28,6 +28,7 @@ import com.vitorpamplona.amethyst.service.relays.EOSETime
|
|||
import com.vitorpamplona.amethyst.service.relays.JsonFilter
|
||||
import com.vitorpamplona.amethyst.service.relays.TypedFilter
|
||||
import com.vitorpamplona.quartz.events.CommunityPostApprovalEvent
|
||||
import com.vitorpamplona.quartz.events.DeletionEvent
|
||||
import com.vitorpamplona.quartz.events.GenericRepostEvent
|
||||
import com.vitorpamplona.quartz.events.GitReplyEvent
|
||||
import com.vitorpamplona.quartz.events.LiveActivitiesChatMessageEvent
|
||||
|
@ -57,29 +58,45 @@ object NostrSingleEventDataSource : NostrDataSource("SingleEventFeed") {
|
|||
}
|
||||
|
||||
return groupByEOSEPresence(addressesToWatch).map {
|
||||
TypedFilter(
|
||||
types = COMMON_FEED_TYPES,
|
||||
filter =
|
||||
JsonFilter(
|
||||
kinds =
|
||||
listOf(
|
||||
TextNoteEvent.KIND,
|
||||
ReactionEvent.KIND,
|
||||
RepostEvent.KIND,
|
||||
GenericRepostEvent.KIND,
|
||||
ReportEvent.KIND,
|
||||
LnZapEvent.KIND,
|
||||
PollNoteEvent.KIND,
|
||||
CommunityPostApprovalEvent.KIND,
|
||||
LiveActivitiesChatMessageEvent.KIND,
|
||||
),
|
||||
tags = mapOf("a" to it.mapNotNull { it.address()?.toTag() }),
|
||||
since = findMinimumEOSEs(it),
|
||||
// Max amount of "replies" to download on a specific event.
|
||||
limit = 1000,
|
||||
),
|
||||
listOf(
|
||||
TypedFilter(
|
||||
types = COMMON_FEED_TYPES,
|
||||
filter =
|
||||
JsonFilter(
|
||||
kinds =
|
||||
listOf(
|
||||
TextNoteEvent.KIND,
|
||||
ReactionEvent.KIND,
|
||||
RepostEvent.KIND,
|
||||
GenericRepostEvent.KIND,
|
||||
ReportEvent.KIND,
|
||||
LnZapEvent.KIND,
|
||||
PollNoteEvent.KIND,
|
||||
CommunityPostApprovalEvent.KIND,
|
||||
LiveActivitiesChatMessageEvent.KIND,
|
||||
),
|
||||
tags = mapOf("a" to it.mapNotNull { it.address()?.toTag() }),
|
||||
since = findMinimumEOSEs(it),
|
||||
// Max amount of "replies" to download on a specific event.
|
||||
limit = 1000,
|
||||
),
|
||||
),
|
||||
TypedFilter(
|
||||
types = COMMON_FEED_TYPES,
|
||||
filter =
|
||||
JsonFilter(
|
||||
kinds =
|
||||
listOf(
|
||||
DeletionEvent.KIND,
|
||||
),
|
||||
tags = mapOf("a" to it.mapNotNull { it.address()?.toTag() }),
|
||||
since = findMinimumEOSEs(it),
|
||||
// Max amount of "replies" to download on a specific event.
|
||||
limit = 10,
|
||||
),
|
||||
),
|
||||
)
|
||||
}
|
||||
}.flatten()
|
||||
}
|
||||
|
||||
private fun createAddressFilter(): List<TypedFilter>? {
|
||||
|
@ -147,6 +164,20 @@ object NostrSingleEventDataSource : NostrDataSource("SingleEventFeed") {
|
|||
limit = 1000,
|
||||
),
|
||||
),
|
||||
TypedFilter(
|
||||
types = COMMON_FEED_TYPES,
|
||||
filter =
|
||||
JsonFilter(
|
||||
kinds =
|
||||
listOf(
|
||||
DeletionEvent.KIND,
|
||||
),
|
||||
tags = mapOf("e" to it.map { it.idHex }),
|
||||
since = findMinimumEOSEs(it),
|
||||
// Max amount of "replies" to download on a specific event.
|
||||
limit = 10,
|
||||
),
|
||||
),
|
||||
)
|
||||
}.flatten()
|
||||
}
|
||||
|
|
|
@ -20,6 +20,8 @@
|
|||
*/
|
||||
package com.vitorpamplona.amethyst.service.previews
|
||||
|
||||
import com.vitorpamplona.amethyst.commons.preview.MetaTag
|
||||
import com.vitorpamplona.amethyst.commons.preview.MetaTagsParser
|
||||
import com.vitorpamplona.amethyst.service.HttpClientManager
|
||||
import com.vitorpamplona.amethyst.service.checkNotInMainThread
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
|
@ -27,60 +29,39 @@ import kotlinx.coroutines.withContext
|
|||
import okhttp3.MediaType
|
||||
import okhttp3.MediaType.Companion.toMediaType
|
||||
import okhttp3.Request
|
||||
import org.jsoup.Jsoup
|
||||
import org.jsoup.nodes.Document
|
||||
import okio.BufferedSource
|
||||
import okio.ByteString.Companion.decodeHex
|
||||
import okio.Options
|
||||
import java.nio.charset.Charset
|
||||
|
||||
private const val ELEMENT_TAG_META = "meta"
|
||||
private const val ATTRIBUTE_VALUE_PROPERTY = "property"
|
||||
private const val ATTRIBUTE_VALUE_NAME = "name"
|
||||
private const val ATTRIBUTE_VALUE_ITEMPROP = "itemprop"
|
||||
private const val ATTRIBUTE_VALUE_CHARSET = "charset"
|
||||
private const val ATTRIBUTE_VALUE_HTTP_EQUIV = "http-equiv"
|
||||
|
||||
// for <meta itemprop=... to get title
|
||||
private val META_X_TITLE =
|
||||
arrayOf(
|
||||
"og:title",
|
||||
"\"og:title\"",
|
||||
"'og:title'",
|
||||
"name",
|
||||
"\"name\"",
|
||||
"'name'",
|
||||
"twitter:title",
|
||||
"\"twitter:title\"",
|
||||
"'twitter:title'",
|
||||
"title",
|
||||
"\"title\"",
|
||||
"'title'",
|
||||
)
|
||||
|
||||
// for <meta itemprop=... to get description
|
||||
private val META_X_DESCRIPTION =
|
||||
arrayOf(
|
||||
"og:description",
|
||||
"\"og:description\"",
|
||||
"'og:description'",
|
||||
"description",
|
||||
"\"description\"",
|
||||
"'description'",
|
||||
"twitter:description",
|
||||
"\"twitter:description\"",
|
||||
"'twitter:description'",
|
||||
"description",
|
||||
"\"description\"",
|
||||
"'description'",
|
||||
)
|
||||
|
||||
// for <meta itemprop=... to get image
|
||||
private val META_X_IMAGE =
|
||||
arrayOf(
|
||||
"og:image",
|
||||
"\"og:image\"",
|
||||
"'og:image'",
|
||||
"image",
|
||||
"\"image\"",
|
||||
"'image'",
|
||||
"twitter:image",
|
||||
"\"twitter:image\"",
|
||||
"'twitter:image'",
|
||||
"image",
|
||||
)
|
||||
|
||||
private const val CONTENT = "content"
|
||||
|
@ -95,14 +76,12 @@ suspend fun getDocument(
|
|||
checkNotInMainThread()
|
||||
if (it.isSuccessful) {
|
||||
val mimeType =
|
||||
it.headers.get("Content-Type")?.toMediaType()
|
||||
it.headers["Content-Type"]?.toMediaType()
|
||||
?: throw IllegalArgumentException(
|
||||
"Website returned unknown mimetype: ${it.headers.get("Content-Type")}",
|
||||
"Website returned unknown mimetype: ${it.headers["Content-Type"]}",
|
||||
)
|
||||
|
||||
if (mimeType.type == "text" && mimeType.subtype == "html") {
|
||||
val document = Jsoup.parse(it.body.string())
|
||||
parseHtml(url, document, mimeType)
|
||||
parseHtml(url, it.body.source(), mimeType)
|
||||
} else if (mimeType.type == "image") {
|
||||
UrlInfoItem(url, image = url, mimeType = mimeType)
|
||||
} else if (mimeType.type == "video") {
|
||||
|
@ -120,65 +99,141 @@ suspend fun getDocument(
|
|||
|
||||
suspend fun parseHtml(
|
||||
url: String,
|
||||
document: Document,
|
||||
source: BufferedSource,
|
||||
type: MediaType,
|
||||
): UrlInfoItem =
|
||||
withContext(Dispatchers.IO) {
|
||||
val metaTags = document.getElementsByTag(ELEMENT_TAG_META)
|
||||
// sniff charset from Content-Type header or BOM
|
||||
val sniffedCharset = type.charset() ?: source.readBomAsCharset()
|
||||
if (sniffedCharset != null) {
|
||||
val metaTags = MetaTagsParser.parse(source.readByteArray().toString(sniffedCharset))
|
||||
return@withContext extractUrlInfo(url, metaTags, type)
|
||||
}
|
||||
|
||||
var title: String = ""
|
||||
var description: String = ""
|
||||
var image: String = ""
|
||||
// if sniffing was failed, detect charset from content
|
||||
val bodyBytes = source.readByteArray()
|
||||
val charset = detectCharset(bodyBytes)
|
||||
val metaTags = MetaTagsParser.parse(bodyBytes.toString(charset))
|
||||
return@withContext extractUrlInfo(url, metaTags, type)
|
||||
}
|
||||
|
||||
metaTags.forEach {
|
||||
when (it.attr(ATTRIBUTE_VALUE_PROPERTY)) {
|
||||
in META_X_TITLE ->
|
||||
if (title.isEmpty()) {
|
||||
title = it.attr(CONTENT)
|
||||
}
|
||||
in META_X_DESCRIPTION ->
|
||||
if (description.isEmpty()) {
|
||||
description = it.attr(CONTENT)
|
||||
}
|
||||
in META_X_IMAGE ->
|
||||
if (image.isEmpty()) {
|
||||
image = it.attr(CONTENT)
|
||||
}
|
||||
}
|
||||
// taken from okhttp
|
||||
private val UNICODE_BOMS =
|
||||
Options.of(
|
||||
// UTF-8
|
||||
"efbbbf".decodeHex(),
|
||||
// UTF-16BE
|
||||
"feff".decodeHex(),
|
||||
// UTF-16LE
|
||||
"fffe".decodeHex(),
|
||||
// UTF-32BE
|
||||
"0000ffff".decodeHex(),
|
||||
// UTF-32LE
|
||||
"ffff0000".decodeHex(),
|
||||
)
|
||||
|
||||
when (it.attr(ATTRIBUTE_VALUE_NAME)) {
|
||||
in META_X_TITLE ->
|
||||
if (title.isEmpty()) {
|
||||
title = it.attr(CONTENT)
|
||||
}
|
||||
in META_X_DESCRIPTION ->
|
||||
if (description.isEmpty()) {
|
||||
description = it.attr(CONTENT)
|
||||
}
|
||||
in META_X_IMAGE ->
|
||||
if (image.isEmpty()) {
|
||||
image = it.attr(CONTENT)
|
||||
}
|
||||
}
|
||||
private fun BufferedSource.readBomAsCharset(): Charset? {
|
||||
return when (select(UNICODE_BOMS)) {
|
||||
0 -> Charsets.UTF_8
|
||||
1 -> Charsets.UTF_16BE
|
||||
2 -> Charsets.UTF_16LE
|
||||
3 -> Charsets.UTF_32BE
|
||||
4 -> Charsets.UTF_32LE
|
||||
-1 -> null
|
||||
else -> throw AssertionError()
|
||||
}
|
||||
}
|
||||
|
||||
when (it.attr(ATTRIBUTE_VALUE_ITEMPROP)) {
|
||||
in META_X_TITLE ->
|
||||
if (title.isEmpty()) {
|
||||
title = it.attr(CONTENT)
|
||||
}
|
||||
in META_X_DESCRIPTION ->
|
||||
if (description.isEmpty()) {
|
||||
description = it.attr(CONTENT)
|
||||
}
|
||||
in META_X_IMAGE ->
|
||||
if (image.isEmpty()) {
|
||||
image = it.attr(CONTENT)
|
||||
}
|
||||
}
|
||||
private val RE_CONTENT_TYPE_CHARSET = Regex("""charset=([^;]+)""")
|
||||
|
||||
if (title.isNotEmpty() && description.isNotEmpty() && image.isNotEmpty()) {
|
||||
return@withContext UrlInfoItem(url, title, description, image, type)
|
||||
private fun detectCharset(bodyBytes: ByteArray): Charset {
|
||||
// try to detect charset from meta tags parsed from first 1024 bytes of body
|
||||
val firstPart = String(bodyBytes, 0, 1024, Charset.forName("utf-8"))
|
||||
val metaTags = MetaTagsParser.parse(firstPart)
|
||||
metaTags.forEach { meta ->
|
||||
val charsetAttr = meta.attr(ATTRIBUTE_VALUE_CHARSET)
|
||||
if (charsetAttr.isNotEmpty()) {
|
||||
runCatching { Charset.forName(charsetAttr) }.getOrNull()?.let {
|
||||
return it
|
||||
}
|
||||
}
|
||||
return@withContext UrlInfoItem(url, title, description, image, type)
|
||||
if (meta.attr(ATTRIBUTE_VALUE_HTTP_EQUIV).lowercase() == "content-type") {
|
||||
RE_CONTENT_TYPE_CHARSET.find(meta.attr(CONTENT))
|
||||
?.let {
|
||||
runCatching { Charset.forName(it.groupValues[1]) }.getOrNull()
|
||||
}?.let {
|
||||
return it
|
||||
}
|
||||
}
|
||||
}
|
||||
// defaults to UTF-8
|
||||
return Charset.forName("utf-8")
|
||||
}
|
||||
|
||||
private fun extractUrlInfo(
|
||||
url: String,
|
||||
metaTags: Sequence<MetaTag>,
|
||||
type: MediaType,
|
||||
): UrlInfoItem {
|
||||
var title: String = ""
|
||||
var description: String = ""
|
||||
var image: String = ""
|
||||
|
||||
metaTags.forEach {
|
||||
when (it.attr(ATTRIBUTE_VALUE_PROPERTY)) {
|
||||
in META_X_TITLE ->
|
||||
if (title.isEmpty()) {
|
||||
title = it.attr(CONTENT)
|
||||
}
|
||||
|
||||
in META_X_DESCRIPTION ->
|
||||
if (description.isEmpty()) {
|
||||
description = it.attr(CONTENT)
|
||||
}
|
||||
|
||||
in META_X_IMAGE ->
|
||||
if (image.isEmpty()) {
|
||||
image = it.attr(CONTENT)
|
||||
}
|
||||
}
|
||||
|
||||
when (it.attr(ATTRIBUTE_VALUE_NAME)) {
|
||||
in META_X_TITLE ->
|
||||
if (title.isEmpty()) {
|
||||
title = it.attr(CONTENT)
|
||||
}
|
||||
|
||||
in META_X_DESCRIPTION ->
|
||||
if (description.isEmpty()) {
|
||||
description = it.attr(CONTENT)
|
||||
}
|
||||
|
||||
in META_X_IMAGE ->
|
||||
if (image.isEmpty()) {
|
||||
image = it.attr(CONTENT)
|
||||
}
|
||||
}
|
||||
|
||||
when (it.attr(ATTRIBUTE_VALUE_ITEMPROP)) {
|
||||
in META_X_TITLE ->
|
||||
if (title.isEmpty()) {
|
||||
title = it.attr(CONTENT)
|
||||
}
|
||||
|
||||
in META_X_DESCRIPTION ->
|
||||
if (description.isEmpty()) {
|
||||
description = it.attr(CONTENT)
|
||||
}
|
||||
|
||||
in META_X_IMAGE ->
|
||||
if (image.isEmpty()) {
|
||||
image = it.attr(CONTENT)
|
||||
}
|
||||
}
|
||||
|
||||
if (title.isNotEmpty() && description.isNotEmpty() && image.isNotEmpty()) {
|
||||
return UrlInfoItem(url, title, description, image, type)
|
||||
}
|
||||
}
|
||||
return UrlInfoItem(url, title, description, image, type)
|
||||
}
|
||||
|
|
|
@ -45,7 +45,9 @@ fun NewPollOption(
|
|||
Row {
|
||||
val deleteIcon: @Composable (() -> Unit) = {
|
||||
IconButton(
|
||||
onClick = { pollViewModel.pollOptions.remove(optionIndex) },
|
||||
onClick = {
|
||||
pollViewModel.removePollOption(optionIndex)
|
||||
},
|
||||
) {
|
||||
Icon(
|
||||
imageVector = Icons.Default.Delete,
|
||||
|
@ -57,7 +59,9 @@ fun NewPollOption(
|
|||
OutlinedTextField(
|
||||
modifier = Modifier.weight(1F),
|
||||
value = pollViewModel.pollOptions[optionIndex] ?: "",
|
||||
onValueChange = { pollViewModel.pollOptions[optionIndex] = it },
|
||||
onValueChange = {
|
||||
pollViewModel.updatePollOption(optionIndex, it)
|
||||
},
|
||||
label = {
|
||||
Text(
|
||||
text = stringResource(R.string.poll_option_index).format(optionIndex + 1),
|
||||
|
|
|
@ -171,13 +171,17 @@ import kotlinx.collections.immutable.ImmutableList
|
|||
import kotlinx.collections.immutable.toImmutableList
|
||||
import kotlinx.coroutines.CancellationException
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.FlowPreview
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.collectLatest
|
||||
import kotlinx.coroutines.flow.debounce
|
||||
import kotlinx.coroutines.flow.receiveAsFlow
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import java.lang.Math.round
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@OptIn(ExperimentalMaterial3Api::class, FlowPreview::class)
|
||||
@Composable
|
||||
fun NewPostView(
|
||||
onClose: () -> Unit,
|
||||
|
@ -185,6 +189,7 @@ fun NewPostView(
|
|||
quote: Note? = null,
|
||||
fork: Note? = null,
|
||||
version: Note? = null,
|
||||
draft: Note? = null,
|
||||
enableMessageInterface: Boolean = false,
|
||||
accountViewModel: AccountViewModel,
|
||||
nav: (String) -> Unit,
|
||||
|
@ -199,10 +204,21 @@ fun NewPostView(
|
|||
var showRelaysDialog by remember { mutableStateOf(false) }
|
||||
var relayList = remember { accountViewModel.account.activeWriteRelays().toImmutableList() }
|
||||
|
||||
LaunchedEffect(Unit) {
|
||||
postViewModel.load(accountViewModel, baseReplyTo, quote, fork, version)
|
||||
|
||||
LaunchedEffect(key1 = postViewModel.draftTag) {
|
||||
launch(Dispatchers.IO) {
|
||||
postViewModel.draftTextChanges
|
||||
.receiveAsFlow()
|
||||
.debounce(1000)
|
||||
.collectLatest {
|
||||
postViewModel.sendDraft(relayList = relayList)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
LaunchedEffect(Unit) {
|
||||
launch(Dispatchers.IO) {
|
||||
postViewModel.load(accountViewModel, baseReplyTo, quote, fork, version, draft)
|
||||
|
||||
postViewModel.imageUploadingError.collect { error ->
|
||||
withContext(Dispatchers.Main) { Toast.makeText(context, error, Toast.LENGTH_SHORT).show() }
|
||||
}
|
||||
|
@ -351,7 +367,7 @@ fun NewPostView(
|
|||
}
|
||||
}
|
||||
|
||||
if (enableMessageInterface) {
|
||||
if (postViewModel.wantsDirectMessage) {
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
modifier = Modifier.padding(vertical = Size5dp, horizontal = Size10dp),
|
||||
|
@ -581,7 +597,7 @@ private fun BottomRowActions(postViewModel: NewPostViewModel) {
|
|||
}
|
||||
|
||||
MarkAsSensitive(postViewModel) {
|
||||
postViewModel.wantsToMarkAsSensitive = !postViewModel.wantsToMarkAsSensitive
|
||||
postViewModel.toggleMarkAsSensitive()
|
||||
}
|
||||
|
||||
AddGeoHash(postViewModel) {
|
||||
|
@ -827,7 +843,9 @@ fun SellProduct(postViewModel: NewPostViewModel) {
|
|||
|
||||
MyTextField(
|
||||
value = postViewModel.title,
|
||||
onValueChange = { postViewModel.title = it },
|
||||
onValueChange = {
|
||||
postViewModel.updateTitle(it)
|
||||
},
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
placeholder = {
|
||||
Text(
|
||||
|
@ -863,13 +881,7 @@ fun SellProduct(postViewModel: NewPostViewModel) {
|
|||
modifier = Modifier.fillMaxWidth(),
|
||||
value = postViewModel.price,
|
||||
onValueChange = {
|
||||
runCatching {
|
||||
if (it.text.isEmpty()) {
|
||||
postViewModel.price = TextFieldValue("")
|
||||
} else if (it.text.toLongOrNull() != null) {
|
||||
postViewModel.price = it
|
||||
}
|
||||
}
|
||||
postViewModel.updatePrice(it)
|
||||
},
|
||||
placeholder = {
|
||||
Text(
|
||||
|
@ -934,7 +946,9 @@ fun SellProduct(postViewModel: NewPostViewModel) {
|
|||
TextSpinner(
|
||||
placeholder = conditionTypes.filter { it.first == postViewModel.condition }.first().second,
|
||||
options = conditionOptions,
|
||||
onSelect = { postViewModel.condition = conditionTypes[it].first },
|
||||
onSelect = {
|
||||
postViewModel.updateCondition(conditionTypes[it].first)
|
||||
},
|
||||
modifier =
|
||||
Modifier
|
||||
.weight(1f)
|
||||
|
@ -998,7 +1012,9 @@ fun SellProduct(postViewModel: NewPostViewModel) {
|
|||
categoryTypes.filter { it.second == postViewModel.category.text }.firstOrNull()?.second
|
||||
?: "",
|
||||
options = categoryOptions,
|
||||
onSelect = { postViewModel.category = TextFieldValue(categoryTypes[it].second) },
|
||||
onSelect = {
|
||||
postViewModel.updateCategory(TextFieldValue(categoryTypes[it].second))
|
||||
},
|
||||
modifier =
|
||||
Modifier
|
||||
.weight(1f)
|
||||
|
@ -1033,7 +1049,9 @@ fun SellProduct(postViewModel: NewPostViewModel) {
|
|||
|
||||
MyTextField(
|
||||
value = postViewModel.locationText,
|
||||
onValueChange = { postViewModel.locationText = it },
|
||||
onValueChange = {
|
||||
postViewModel.updateLocation(it)
|
||||
},
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
placeholder = {
|
||||
Text(
|
||||
|
|
|
@ -49,12 +49,15 @@ import com.vitorpamplona.amethyst.service.relays.Relay
|
|||
import com.vitorpamplona.amethyst.ui.components.MediaCompressor
|
||||
import com.vitorpamplona.amethyst.ui.components.Split
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
||||
import com.vitorpamplona.quartz.encoders.Hex
|
||||
import com.vitorpamplona.quartz.encoders.HexKey
|
||||
import com.vitorpamplona.quartz.encoders.toNpub
|
||||
import com.vitorpamplona.quartz.events.AddressableEvent
|
||||
import com.vitorpamplona.quartz.events.BaseTextNoteEvent
|
||||
import com.vitorpamplona.quartz.events.ChatMessageEvent
|
||||
import com.vitorpamplona.quartz.events.ClassifiedsEvent
|
||||
import com.vitorpamplona.quartz.events.CommunityDefinitionEvent
|
||||
import com.vitorpamplona.quartz.events.DraftEvent
|
||||
import com.vitorpamplona.quartz.events.Event
|
||||
import com.vitorpamplona.quartz.events.FileHeaderEvent
|
||||
import com.vitorpamplona.quartz.events.FileStorageEvent
|
||||
|
@ -69,10 +72,12 @@ import kotlinx.coroutines.CancellationException
|
|||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||
import kotlinx.coroutines.channels.BufferOverflow
|
||||
import kotlinx.coroutines.channels.Channel
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.MutableSharedFlow
|
||||
import kotlinx.coroutines.flow.mapLatest
|
||||
import kotlinx.coroutines.launch
|
||||
import java.util.UUID
|
||||
|
||||
enum class UserSuggestionAnchor {
|
||||
MAIN_MESSAGE,
|
||||
|
@ -82,6 +87,8 @@ enum class UserSuggestionAnchor {
|
|||
|
||||
@Stable
|
||||
open class NewPostViewModel() : ViewModel() {
|
||||
var draftTag: String by mutableStateOf(UUID.randomUUID().toString())
|
||||
|
||||
var accountViewModel: AccountViewModel? = null
|
||||
var account: Account? = null
|
||||
var requiresNIP24: Boolean = false
|
||||
|
@ -164,6 +171,8 @@ open class NewPostViewModel() : ViewModel() {
|
|||
// NIP24 Wrapped DMs / Group messages
|
||||
var nip24 by mutableStateOf(false)
|
||||
|
||||
val draftTextChanges = Channel<String>(Channel.CONFLATED)
|
||||
|
||||
fun lnAddress(): String? {
|
||||
return account?.userProfile()?.info?.lnAddress()
|
||||
}
|
||||
|
@ -182,134 +191,272 @@ open class NewPostViewModel() : ViewModel() {
|
|||
quote: Note?,
|
||||
fork: Note?,
|
||||
version: Note?,
|
||||
draft: Note?,
|
||||
) {
|
||||
this.accountViewModel = accountViewModel
|
||||
this.account = accountViewModel.account
|
||||
|
||||
originalNote = replyingTo
|
||||
replyingTo?.let { replyNote ->
|
||||
if (replyNote.event is BaseTextNoteEvent) {
|
||||
this.eTags = (replyNote.replyTo ?: emptyList()).plus(replyNote)
|
||||
} else {
|
||||
this.eTags = listOf(replyNote)
|
||||
val noteEvent = draft?.event
|
||||
val noteAuthor = draft?.author
|
||||
|
||||
if (draft != null && noteEvent is DraftEvent && noteAuthor != null) {
|
||||
accountViewModel.createTempDraftNote(noteEvent, noteAuthor) { innerNote ->
|
||||
val oldTag = (draft.event as? AddressableEvent)?.dTag()
|
||||
if (oldTag != null) {
|
||||
draftTag = oldTag
|
||||
}
|
||||
loadFromDraft(innerNote, accountViewModel)
|
||||
}
|
||||
} else {
|
||||
originalNote = replyingTo
|
||||
replyingTo?.let { replyNote ->
|
||||
if (replyNote.event is BaseTextNoteEvent) {
|
||||
this.eTags = (replyNote.replyTo ?: emptyList()).plus(replyNote)
|
||||
} else {
|
||||
this.eTags = listOf(replyNote)
|
||||
}
|
||||
|
||||
if (replyNote.event !is CommunityDefinitionEvent) {
|
||||
replyNote.author?.let { replyUser ->
|
||||
val currentMentions =
|
||||
(replyNote.event as? TextNoteEvent)?.mentions()?.map { LocalCache.getOrCreateUser(it) }
|
||||
?: emptyList()
|
||||
if (replyNote.event !is CommunityDefinitionEvent) {
|
||||
replyNote.author?.let { replyUser ->
|
||||
val currentMentions =
|
||||
(replyNote.event as? TextNoteEvent)?.mentions()?.map { LocalCache.getOrCreateUser(it) }
|
||||
?: emptyList()
|
||||
|
||||
if (currentMentions.contains(replyUser)) {
|
||||
this.pTags = currentMentions
|
||||
} else {
|
||||
this.pTags = currentMentions.plus(replyUser)
|
||||
if (currentMentions.contains(replyUser)) {
|
||||
this.pTags = currentMentions
|
||||
} else {
|
||||
this.pTags = currentMentions.plus(replyUser)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
?: run {
|
||||
eTags = null
|
||||
pTags = null
|
||||
?: run {
|
||||
eTags = null
|
||||
pTags = null
|
||||
}
|
||||
|
||||
canAddInvoice = accountViewModel.userProfile().info?.lnAddress() != null
|
||||
canAddZapRaiser = accountViewModel.userProfile().info?.lnAddress() != null
|
||||
canUsePoll = originalNote?.event !is PrivateDmEvent && originalNote?.channelHex() == null
|
||||
contentToAddUrl = null
|
||||
|
||||
quote?.let {
|
||||
message = TextFieldValue(message.text + "\nnostr:${it.toNEvent()}")
|
||||
urlPreview = findUrlInMessage()
|
||||
|
||||
it.author?.let { quotedUser ->
|
||||
if (quotedUser.pubkeyHex != accountViewModel.userProfile().pubkeyHex) {
|
||||
if (forwardZapTo.items.none { it.key.pubkeyHex == quotedUser.pubkeyHex }) {
|
||||
forwardZapTo.addItem(quotedUser)
|
||||
}
|
||||
if (forwardZapTo.items.none { it.key.pubkeyHex == accountViewModel.userProfile().pubkeyHex }) {
|
||||
forwardZapTo.addItem(accountViewModel.userProfile())
|
||||
}
|
||||
|
||||
val pos = forwardZapTo.items.indexOfFirst { it.key.pubkeyHex == quotedUser.pubkeyHex }
|
||||
forwardZapTo.updatePercentage(pos, 0.9f)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fork?.let {
|
||||
message = TextFieldValue(version?.event?.content() ?: it.event?.content() ?: "")
|
||||
urlPreview = findUrlInMessage()
|
||||
|
||||
it.event?.isSensitive()?.let {
|
||||
if (it) wantsToMarkAsSensitive = true
|
||||
}
|
||||
|
||||
it.event?.zapraiserAmount()?.let {
|
||||
zapRaiserAmount = it
|
||||
}
|
||||
|
||||
it.event?.zapSplitSetup()?.let {
|
||||
val totalWeight = it.sumOf { if (it.isLnAddress) 0.0 else it.weight }
|
||||
|
||||
it.forEach {
|
||||
if (!it.isLnAddress) {
|
||||
forwardZapTo.addItem(LocalCache.getOrCreateUser(it.lnAddressOrPubKeyHex), (it.weight / totalWeight).toFloat())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Only adds if it is not already set up.
|
||||
if (forwardZapTo.items.isEmpty()) {
|
||||
it.author?.let { forkedAuthor ->
|
||||
if (forkedAuthor.pubkeyHex != accountViewModel.userProfile().pubkeyHex) {
|
||||
if (forwardZapTo.items.none { it.key.pubkeyHex == forkedAuthor.pubkeyHex }) forwardZapTo.addItem(forkedAuthor)
|
||||
if (forwardZapTo.items.none { it.key.pubkeyHex == accountViewModel.userProfile().pubkeyHex }) forwardZapTo.addItem(accountViewModel.userProfile())
|
||||
|
||||
val pos = forwardZapTo.items.indexOfFirst { it.key.pubkeyHex == forkedAuthor.pubkeyHex }
|
||||
forwardZapTo.updatePercentage(pos, 0.8f)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
it.author?.let {
|
||||
if (this.pTags == null) {
|
||||
this.pTags = listOf(it)
|
||||
} else if (this.pTags?.contains(it) != true) {
|
||||
this.pTags = listOf(it) + (this.pTags ?: emptyList())
|
||||
}
|
||||
}
|
||||
|
||||
forkedFromNote = it
|
||||
} ?: run {
|
||||
forkedFromNote = null
|
||||
}
|
||||
|
||||
if (!forwardZapTo.items.isEmpty()) {
|
||||
wantsForwardZapTo = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun loadFromDraft(
|
||||
draft: Note,
|
||||
accountViewModel: AccountViewModel,
|
||||
) {
|
||||
Log.d("draft", draft.event!!.toJson())
|
||||
val draftEvent = draft.event ?: return
|
||||
|
||||
canAddInvoice = accountViewModel.userProfile().info?.lnAddress() != null
|
||||
canAddZapRaiser = accountViewModel.userProfile().info?.lnAddress() != null
|
||||
canUsePoll = originalNote?.event !is PrivateDmEvent && originalNote?.channelHex() == null
|
||||
contentToAddUrl = null
|
||||
|
||||
wantsForwardZapTo = false
|
||||
wantsToMarkAsSensitive = false
|
||||
wantsToAddGeoHash = false
|
||||
wantsZapraiser = false
|
||||
zapRaiserAmount = null
|
||||
val localfowardZapTo = draftEvent.tags().filter { it.size > 1 && it[0] == "zap" }
|
||||
forwardZapTo = Split()
|
||||
localfowardZapTo.forEach {
|
||||
val user = LocalCache.getOrCreateUser(it[1])
|
||||
val value = it.last().toFloatOrNull() ?: 0f
|
||||
forwardZapTo.addItem(user, value)
|
||||
}
|
||||
forwardZapToEditting = TextFieldValue("")
|
||||
wantsForwardZapTo = localfowardZapTo.isNotEmpty()
|
||||
|
||||
quote?.let {
|
||||
message = TextFieldValue(message.text + "\nnostr:${it.toNEvent()}")
|
||||
urlPreview = findUrlInMessage()
|
||||
|
||||
it.author?.let { quotedUser ->
|
||||
if (quotedUser.pubkeyHex != accountViewModel.userProfile().pubkeyHex) {
|
||||
if (forwardZapTo.items.none { it.key.pubkeyHex == quotedUser.pubkeyHex }) {
|
||||
forwardZapTo.addItem(quotedUser)
|
||||
}
|
||||
if (forwardZapTo.items.none { it.key.pubkeyHex == accountViewModel.userProfile().pubkeyHex }) {
|
||||
forwardZapTo.addItem(accountViewModel.userProfile())
|
||||
}
|
||||
|
||||
val pos = forwardZapTo.items.indexOfFirst { it.key.pubkeyHex == quotedUser.pubkeyHex }
|
||||
forwardZapTo.updatePercentage(pos, 0.9f)
|
||||
}
|
||||
}
|
||||
wantsToMarkAsSensitive = draftEvent.tags().any { it.size > 1 && it[0] == "content-warning" }
|
||||
wantsToAddGeoHash = draftEvent.tags().any { it.size > 1 && it[0] == "g" }
|
||||
val zapraiser = draftEvent.tags().filter { it.size > 1 && it[0] == "zapraiser" }
|
||||
wantsZapraiser = zapraiser.isNotEmpty()
|
||||
zapRaiserAmount = null
|
||||
if (wantsZapraiser) {
|
||||
zapRaiserAmount = zapraiser.first()[1].toLongOrNull() ?: 0
|
||||
}
|
||||
|
||||
fork?.let {
|
||||
message = TextFieldValue(version?.event?.content() ?: it.event?.content() ?: "")
|
||||
urlPreview = findUrlInMessage()
|
||||
|
||||
it.event?.isSensitive()?.let {
|
||||
if (it) wantsToMarkAsSensitive = true
|
||||
eTags =
|
||||
draftEvent.tags().filter { it.size > 1 && (it[0] == "e" || it[0] == "a") && it.getOrNull(3) != "fork" }.mapNotNull {
|
||||
val note = LocalCache.checkGetOrCreateNote(it[1])
|
||||
note
|
||||
}
|
||||
|
||||
it.event?.zapraiserAmount()?.let {
|
||||
zapRaiserAmount = it
|
||||
}
|
||||
|
||||
it.event?.zapSplitSetup()?.let {
|
||||
val totalWeight = it.sumOf { if (it.isLnAddress) 0.0 else it.weight }
|
||||
|
||||
it.forEach {
|
||||
if (!it.isLnAddress) {
|
||||
forwardZapTo.addItem(LocalCache.getOrCreateUser(it.lnAddressOrPubKeyHex), (it.weight / totalWeight).toFloat())
|
||||
}
|
||||
if (draftEvent !is PrivateDmEvent && draftEvent !is ChatMessageEvent) {
|
||||
pTags =
|
||||
draftEvent.tags().filter { it.size > 1 && it[0] == "p" }.map {
|
||||
LocalCache.getOrCreateUser(it[1])
|
||||
}
|
||||
}
|
||||
|
||||
// Only adds if it is not already set up.
|
||||
if (forwardZapTo.items.isEmpty()) {
|
||||
it.author?.let { forkedAuthor ->
|
||||
if (forkedAuthor.pubkeyHex != accountViewModel.userProfile().pubkeyHex) {
|
||||
if (forwardZapTo.items.none { it.key.pubkeyHex == forkedAuthor.pubkeyHex }) forwardZapTo.addItem(forkedAuthor)
|
||||
if (forwardZapTo.items.none { it.key.pubkeyHex == accountViewModel.userProfile().pubkeyHex }) forwardZapTo.addItem(accountViewModel.userProfile())
|
||||
|
||||
val pos = forwardZapTo.items.indexOfFirst { it.key.pubkeyHex == forkedAuthor.pubkeyHex }
|
||||
forwardZapTo.updatePercentage(pos, 0.8f)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
it.author?.let {
|
||||
if (this.pTags == null) {
|
||||
this.pTags = listOf(it)
|
||||
} else if (this.pTags?.contains(it) != true) {
|
||||
this.pTags = listOf(it) + (this.pTags ?: emptyList())
|
||||
}
|
||||
}
|
||||
|
||||
forkedFromNote = it
|
||||
} ?: run {
|
||||
forkedFromNote = null
|
||||
}
|
||||
|
||||
if (!forwardZapTo.items.isEmpty()) {
|
||||
draftEvent.tags().filter { it.size > 3 && (it[0] == "e" || it[0] == "a") && it.get(3) == "fork" }.forEach {
|
||||
val note = LocalCache.checkGetOrCreateNote(it[1])
|
||||
forkedFromNote = note
|
||||
}
|
||||
|
||||
originalNote =
|
||||
draftEvent.tags().filter { it.size > 1 && (it[0] == "e" || it[0] == "a") && it.getOrNull(3) == "reply" }.map {
|
||||
LocalCache.checkGetOrCreateNote(it[1])
|
||||
}.firstOrNull()
|
||||
|
||||
if (originalNote == null) {
|
||||
originalNote =
|
||||
draftEvent.tags().filter { it.size > 1 && (it[0] == "e" || it[0] == "a") && it.getOrNull(3) == "root" }.map {
|
||||
LocalCache.checkGetOrCreateNote(it[1])
|
||||
}.firstOrNull()
|
||||
}
|
||||
|
||||
canUsePoll = originalNote?.event !is PrivateDmEvent && originalNote?.channelHex() == null
|
||||
|
||||
if (forwardZapTo.items.isNotEmpty()) {
|
||||
wantsForwardZapTo = true
|
||||
}
|
||||
|
||||
val polls = draftEvent.tags().filter { it.size > 1 && it[0] == "poll_option" }
|
||||
wantsPoll = polls.isNotEmpty()
|
||||
|
||||
polls.forEach {
|
||||
pollOptions[it[1].toInt()] = it[2]
|
||||
}
|
||||
|
||||
val minMax = draftEvent.tags().filter { it.size > 1 && (it[0] == "value_minimum" || it[0] == "value_maximum") }
|
||||
minMax.forEach {
|
||||
if (it[0] == "value_maximum") {
|
||||
valueMaximum = it[1].toInt()
|
||||
} else if (it[0] == "value_minimum") {
|
||||
valueMinimum = it[1].toInt()
|
||||
}
|
||||
}
|
||||
|
||||
wantsProduct = draftEvent.kind() == 30402
|
||||
|
||||
title = TextFieldValue(draftEvent.tags().filter { it.size > 1 && it[0] == "title" }.map { it[1] }?.firstOrNull() ?: "")
|
||||
price = TextFieldValue(draftEvent.tags().filter { it.size > 1 && it[0] == "price" }.map { it[1] }?.firstOrNull() ?: "")
|
||||
category = TextFieldValue(draftEvent.tags().filter { it.size > 1 && it[0] == "t" }.map { it[1] }?.firstOrNull() ?: "")
|
||||
locationText = TextFieldValue(draftEvent.tags().filter { it.size > 1 && it[0] == "location" }.map { it[1] }?.firstOrNull() ?: "")
|
||||
condition = ClassifiedsEvent.CONDITION.entries.firstOrNull {
|
||||
it.value == draftEvent.tags().filter { it.size > 1 && it[0] == "condition" }.map { it[1] }.firstOrNull()
|
||||
} ?: ClassifiedsEvent.CONDITION.USED_LIKE_NEW
|
||||
|
||||
wantsDirectMessage = draftEvent is PrivateDmEvent || draftEvent is ChatMessageEvent
|
||||
|
||||
draftEvent.subject()?.let {
|
||||
subject = TextFieldValue()
|
||||
}
|
||||
|
||||
message =
|
||||
if (draftEvent is PrivateDmEvent) {
|
||||
val recepientNpub = draftEvent.verifiedRecipientPubKey()?.let { Hex.decode(it).toNpub() }
|
||||
toUsers = TextFieldValue("@$recepientNpub")
|
||||
TextFieldValue(draftEvent.cachedContentFor(accountViewModel.account.signer) ?: "")
|
||||
} else {
|
||||
TextFieldValue(draftEvent.content())
|
||||
}
|
||||
|
||||
requiresNIP24 = draftEvent is ChatMessageEvent
|
||||
nip24 = draftEvent is ChatMessageEvent
|
||||
|
||||
if (draftEvent is ChatMessageEvent) {
|
||||
toUsers =
|
||||
TextFieldValue(
|
||||
draftEvent.recipientsPubKey().mapNotNull { runCatching { Hex.decode(it).toNpub() }.getOrNull() }.joinToString(", ") { "@$it" },
|
||||
)
|
||||
}
|
||||
|
||||
urlPreview = findUrlInMessage()
|
||||
}
|
||||
|
||||
fun sendPost(relayList: List<Relay>? = null) {
|
||||
viewModelScope.launch(Dispatchers.IO) { innerSendPost(relayList) }
|
||||
viewModelScope.launch(Dispatchers.IO) {
|
||||
innerSendPost(relayList, null)
|
||||
accountViewModel?.deleteDraft(draftTag)
|
||||
cancel()
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun innerSendPost(relayList: List<Relay>? = null) {
|
||||
fun sendDraft(relayList: List<Relay>? = null) {
|
||||
viewModelScope.launch(Dispatchers.IO) {
|
||||
innerSendPost(relayList, draftTag)
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun innerSendPost(
|
||||
relayList: List<Relay>? = null,
|
||||
localDraft: String?,
|
||||
) {
|
||||
if (accountViewModel == null) {
|
||||
cancel()
|
||||
return
|
||||
}
|
||||
|
||||
val tagger =
|
||||
NewMessageTagger(message.text, pTags, eTags, originalNote?.channelHex(), accountViewModel!!)
|
||||
val tagger = NewMessageTagger(message.text, pTags, eTags, originalNote?.channelHex(), accountViewModel!!)
|
||||
tagger.run()
|
||||
|
||||
val toUsersTagger = NewMessageTagger(toUsers.text, null, null, null, accountViewModel!!)
|
||||
|
@ -363,6 +510,7 @@ open class NewPostViewModel() : ViewModel() {
|
|||
zapRaiserAmount = localZapRaiserAmount,
|
||||
geohash = geoHash,
|
||||
nip94attachments = usedAttachments,
|
||||
draftTag = localDraft,
|
||||
)
|
||||
} else {
|
||||
account?.sendChannelMessage(
|
||||
|
@ -375,6 +523,7 @@ open class NewPostViewModel() : ViewModel() {
|
|||
zapRaiserAmount = localZapRaiserAmount,
|
||||
geohash = geoHash,
|
||||
nip94attachments = usedAttachments,
|
||||
draftTag = localDraft,
|
||||
)
|
||||
}
|
||||
} else if (originalNote?.event is PrivateDmEvent) {
|
||||
|
@ -388,6 +537,7 @@ open class NewPostViewModel() : ViewModel() {
|
|||
zapRaiserAmount = localZapRaiserAmount,
|
||||
geohash = geoHash,
|
||||
nip94attachments = usedAttachments,
|
||||
draftTag = localDraft,
|
||||
)
|
||||
} else if (originalNote?.event is ChatMessageEvent) {
|
||||
val receivers =
|
||||
|
@ -409,6 +559,7 @@ open class NewPostViewModel() : ViewModel() {
|
|||
zapRaiserAmount = localZapRaiserAmount,
|
||||
geohash = geoHash,
|
||||
nip94attachments = usedAttachments,
|
||||
draftTag = localDraft,
|
||||
)
|
||||
} else if (!dmUsers.isNullOrEmpty()) {
|
||||
if (nip24 || dmUsers.size > 1) {
|
||||
|
@ -423,6 +574,7 @@ open class NewPostViewModel() : ViewModel() {
|
|||
zapRaiserAmount = localZapRaiserAmount,
|
||||
geohash = geoHash,
|
||||
nip94attachments = usedAttachments,
|
||||
draftTag = localDraft,
|
||||
)
|
||||
} else {
|
||||
account?.sendPrivateMessage(
|
||||
|
@ -435,6 +587,7 @@ open class NewPostViewModel() : ViewModel() {
|
|||
zapRaiserAmount = localZapRaiserAmount,
|
||||
geohash = geoHash,
|
||||
nip94attachments = usedAttachments,
|
||||
draftTag = localDraft,
|
||||
)
|
||||
}
|
||||
} else if (originalNote?.event is GitIssueEvent) {
|
||||
|
@ -475,24 +628,26 @@ open class NewPostViewModel() : ViewModel() {
|
|||
relayList = relayList,
|
||||
geohash = geoHash,
|
||||
nip94attachments = usedAttachments,
|
||||
draftTag = localDraft,
|
||||
)
|
||||
} else {
|
||||
if (wantsPoll) {
|
||||
account?.sendPoll(
|
||||
tagger.message,
|
||||
tagger.eTags,
|
||||
tagger.pTags,
|
||||
pollOptions,
|
||||
valueMaximum,
|
||||
valueMinimum,
|
||||
consensusThreshold,
|
||||
closedAt,
|
||||
zapReceiver,
|
||||
wantsToMarkAsSensitive,
|
||||
localZapRaiserAmount,
|
||||
relayList,
|
||||
geoHash,
|
||||
message = tagger.message,
|
||||
replyTo = tagger.eTags,
|
||||
mentions = tagger.pTags,
|
||||
pollOptions = pollOptions,
|
||||
valueMaximum = valueMaximum,
|
||||
valueMinimum = valueMinimum,
|
||||
consensusThreshold = consensusThreshold,
|
||||
closedAt = closedAt,
|
||||
zapReceiver = zapReceiver,
|
||||
wantsToMarkAsSensitive = wantsToMarkAsSensitive,
|
||||
zapRaiserAmount = localZapRaiserAmount,
|
||||
relayList = relayList,
|
||||
geohash = geoHash,
|
||||
nip94attachments = usedAttachments,
|
||||
draftTag = localDraft,
|
||||
)
|
||||
} else if (wantsProduct) {
|
||||
account?.sendClassifieds(
|
||||
|
@ -511,6 +666,7 @@ open class NewPostViewModel() : ViewModel() {
|
|||
relayList = relayList,
|
||||
geohash = geoHash,
|
||||
nip94attachments = usedAttachments,
|
||||
draftTag = localDraft,
|
||||
)
|
||||
} else {
|
||||
// adds markers
|
||||
|
@ -547,11 +703,10 @@ open class NewPostViewModel() : ViewModel() {
|
|||
relayList = relayList,
|
||||
geohash = geoHash,
|
||||
nip94attachments = usedAttachments,
|
||||
draftTag = localDraft,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
cancel()
|
||||
}
|
||||
|
||||
fun upload(
|
||||
|
@ -652,6 +807,9 @@ open class NewPostViewModel() : ViewModel() {
|
|||
|
||||
wantsProduct = false
|
||||
condition = ClassifiedsEvent.CONDITION.USED_LIKE_NEW
|
||||
locationText = TextFieldValue("")
|
||||
title = TextFieldValue("")
|
||||
category = TextFieldValue("")
|
||||
price = TextFieldValue("")
|
||||
|
||||
wantsForwardZapTo = false
|
||||
|
@ -664,9 +822,17 @@ open class NewPostViewModel() : ViewModel() {
|
|||
userSuggestionAnchor = null
|
||||
userSuggestionsMainMessage = null
|
||||
|
||||
draftTag = UUID.randomUUID().toString()
|
||||
|
||||
NostrSearchEventOrUserDataSource.clear()
|
||||
}
|
||||
|
||||
fun deleteDraft() {
|
||||
viewModelScope.launch(Dispatchers.IO) {
|
||||
accountViewModel?.deleteDraft(draftTag)
|
||||
}
|
||||
}
|
||||
|
||||
open fun findUrlInMessage(): String? {
|
||||
return message.text.split('\n').firstNotNullOfOrNull { paragraph ->
|
||||
paragraph.split(' ').firstOrNull { word: String ->
|
||||
|
@ -679,6 +845,10 @@ open class NewPostViewModel() : ViewModel() {
|
|||
pTags = pTags?.filter { it != userToRemove }
|
||||
}
|
||||
|
||||
private fun saveDraft() {
|
||||
draftTextChanges.trySend("")
|
||||
}
|
||||
|
||||
open fun updateMessage(it: TextFieldValue) {
|
||||
message = it
|
||||
urlPreview = findUrlInMessage()
|
||||
|
@ -701,6 +871,8 @@ open class NewPostViewModel() : ViewModel() {
|
|||
userSuggestions = emptyList()
|
||||
}
|
||||
}
|
||||
|
||||
saveDraft()
|
||||
}
|
||||
|
||||
open fun updateToUsers(it: TextFieldValue) {
|
||||
|
@ -724,10 +896,12 @@ open class NewPostViewModel() : ViewModel() {
|
|||
userSuggestions = emptyList()
|
||||
}
|
||||
}
|
||||
saveDraft()
|
||||
}
|
||||
|
||||
open fun updateSubject(it: TextFieldValue) {
|
||||
subject = it
|
||||
saveDraft()
|
||||
}
|
||||
|
||||
open fun updateZapForwardTo(it: TextFieldValue) {
|
||||
|
@ -754,6 +928,7 @@ open class NewPostViewModel() : ViewModel() {
|
|||
userSuggestions = emptyList()
|
||||
}
|
||||
}
|
||||
saveDraft()
|
||||
}
|
||||
|
||||
open fun autocompleteWithUser(item: User) {
|
||||
|
@ -799,6 +974,8 @@ open class NewPostViewModel() : ViewModel() {
|
|||
userSuggestionsMainMessage = null
|
||||
userSuggestions = emptyList()
|
||||
}
|
||||
|
||||
saveDraft()
|
||||
}
|
||||
|
||||
private fun newStateMapPollOptions(): SnapshotStateMap<Int, String> {
|
||||
|
@ -869,6 +1046,7 @@ open class NewPostViewModel() : ViewModel() {
|
|||
|
||||
message = message.insertUrlAtCursor(imageUrl)
|
||||
urlPreview = findUrlInMessage()
|
||||
saveDraft()
|
||||
}
|
||||
},
|
||||
onError = {
|
||||
|
@ -913,6 +1091,7 @@ open class NewPostViewModel() : ViewModel() {
|
|||
}
|
||||
|
||||
urlPreview = findUrlInMessage()
|
||||
saveDraft()
|
||||
}
|
||||
},
|
||||
onError = {
|
||||
|
@ -933,6 +1112,7 @@ open class NewPostViewModel() : ViewModel() {
|
|||
locUtil?.let {
|
||||
location =
|
||||
it.locationStateFlow.mapLatest { it.toGeoHash(GeohashPrecision.KM_5_X_5.digits).toString() }
|
||||
saveDraft()
|
||||
}
|
||||
viewModelScope.launch(Dispatchers.IO) { locUtil?.start() }
|
||||
}
|
||||
|
@ -957,6 +1137,9 @@ open class NewPostViewModel() : ViewModel() {
|
|||
} else {
|
||||
nip24 = !nip24
|
||||
}
|
||||
if (message.text.isNotBlank()) {
|
||||
saveDraft()
|
||||
}
|
||||
}
|
||||
|
||||
fun updateMinZapAmountForPoll(textMin: String) {
|
||||
|
@ -976,6 +1159,7 @@ open class NewPostViewModel() : ViewModel() {
|
|||
}
|
||||
|
||||
checkMinMax()
|
||||
saveDraft()
|
||||
}
|
||||
|
||||
fun updateMaxZapAmountForPoll(textMax: String) {
|
||||
|
@ -995,6 +1179,7 @@ open class NewPostViewModel() : ViewModel() {
|
|||
}
|
||||
|
||||
checkMinMax()
|
||||
saveDraft()
|
||||
}
|
||||
|
||||
fun checkMinMax() {
|
||||
|
@ -1013,6 +1198,60 @@ open class NewPostViewModel() : ViewModel() {
|
|||
) {
|
||||
forwardZapTo.updatePercentage(index, sliderValue)
|
||||
}
|
||||
|
||||
fun updateZapRaiserAmount(newAmount: Long?) {
|
||||
zapRaiserAmount = newAmount
|
||||
saveDraft()
|
||||
}
|
||||
|
||||
fun removePollOption(optionIndex: Int) {
|
||||
pollOptions.remove(optionIndex)
|
||||
saveDraft()
|
||||
}
|
||||
|
||||
fun updatePollOption(
|
||||
optionIndex: Int,
|
||||
text: String,
|
||||
) {
|
||||
pollOptions[optionIndex] = text
|
||||
saveDraft()
|
||||
}
|
||||
|
||||
fun toggleMarkAsSensitive() {
|
||||
wantsToMarkAsSensitive = !wantsToMarkAsSensitive
|
||||
saveDraft()
|
||||
}
|
||||
|
||||
fun updateTitle(it: TextFieldValue) {
|
||||
title = it
|
||||
saveDraft()
|
||||
}
|
||||
|
||||
fun updatePrice(it: TextFieldValue) {
|
||||
runCatching {
|
||||
if (it.text.isEmpty()) {
|
||||
price = TextFieldValue("")
|
||||
} else if (it.text.toLongOrNull() != null) {
|
||||
price = it
|
||||
}
|
||||
}
|
||||
saveDraft()
|
||||
}
|
||||
|
||||
fun updateCondition(newCondition: ClassifiedsEvent.CONDITION) {
|
||||
condition = newCondition
|
||||
saveDraft()
|
||||
}
|
||||
|
||||
fun updateCategory(value: TextFieldValue) {
|
||||
category = value
|
||||
saveDraft()
|
||||
}
|
||||
|
||||
fun updateLocation(it: TextFieldValue) {
|
||||
locationText = it
|
||||
saveDraft()
|
||||
}
|
||||
}
|
||||
|
||||
enum class GeohashPrecision(val digits: Int) {
|
||||
|
|
|
@ -408,7 +408,7 @@ fun GetVideoController(
|
|||
val keepPlaying =
|
||||
remember(videoUri) {
|
||||
mutableStateOf<Boolean>(
|
||||
keepPlayingMutex != null && controller == keepPlayingMutex,
|
||||
keepPlayingMutex != null && controller.value == keepPlayingMutex,
|
||||
)
|
||||
}
|
||||
|
||||
|
@ -883,13 +883,17 @@ fun ControlWhenPlayerIsActive(
|
|||
override fun onIsPlayingChanged(isPlaying: Boolean) {
|
||||
// doesn't consider the mutex because the screen can turn off if the video
|
||||
// being played in the mutex is not visible.
|
||||
view.keepScreenOn = isPlaying
|
||||
if (view.keepScreenOn != isPlaying) {
|
||||
view.keepScreenOn = isPlaying
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
controller.addListener(listener)
|
||||
onDispose {
|
||||
view.keepScreenOn = false
|
||||
if (view.keepScreenOn) {
|
||||
view.keepScreenOn = false
|
||||
}
|
||||
controller.removeListener(listener)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -94,9 +94,9 @@ fun ZapRaiserRequest(
|
|||
onValueChange = {
|
||||
runCatching {
|
||||
if (it.isEmpty()) {
|
||||
newPostViewModel.zapRaiserAmount = null
|
||||
newPostViewModel.updateZapRaiserAmount(null)
|
||||
} else {
|
||||
newPostViewModel.zapRaiserAmount = it.toLongOrNull()
|
||||
newPostViewModel.updateZapRaiserAmount(it.toLongOrNull())
|
||||
}
|
||||
}
|
||||
},
|
||||
|
|
|
@ -0,0 +1,56 @@
|
|||
/**
|
||||
* Copyright (c) 2024 Vitor Pamplona
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
* this software and associated documentation files (the "Software"), to deal in
|
||||
* the Software without restriction, including without limitation the rights to use,
|
||||
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
|
||||
* Software, and to permit persons to whom the Software is furnished to do so,
|
||||
* subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
|
||||
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
package com.vitorpamplona.amethyst.ui.dal
|
||||
|
||||
import com.vitorpamplona.amethyst.model.Account
|
||||
import com.vitorpamplona.amethyst.model.LocalCache
|
||||
import com.vitorpamplona.amethyst.model.Note
|
||||
import com.vitorpamplona.quartz.events.DraftEvent
|
||||
|
||||
class DraftEventsFeedFilter(val account: Account) : AdditiveFeedFilter<Note>() {
|
||||
override fun feedKey(): String {
|
||||
return account.userProfile().pubkeyHex
|
||||
}
|
||||
|
||||
override fun applyFilter(collection: Set<Note>): Set<Note> {
|
||||
return collection.filterTo(HashSet()) {
|
||||
acceptableEvent(it)
|
||||
}
|
||||
}
|
||||
|
||||
override fun feed(): List<Note> {
|
||||
val drafts =
|
||||
LocalCache.addressables.filterIntoSet { _, note ->
|
||||
acceptableEvent(note)
|
||||
}
|
||||
|
||||
return sort(drafts)
|
||||
}
|
||||
|
||||
fun acceptableEvent(it: Note): Boolean {
|
||||
val noteEvent = it.event
|
||||
return noteEvent is DraftEvent && noteEvent.pubKey == account.userProfile().pubkeyHex
|
||||
}
|
||||
|
||||
override fun sort(collection: Set<Note>): List<Note> {
|
||||
return collection.sortedWith(DefaultFeedOrder)
|
||||
}
|
||||
}
|
|
@ -88,7 +88,23 @@ class NotificationFeedFilter(val account: Account) : AdditiveFeedFilter<Note>()
|
|||
filterParams: FilterByListParams,
|
||||
): Boolean {
|
||||
val loggedInUserHex = account.userProfile().pubkeyHex
|
||||
val loggedInUser = account.userProfile()
|
||||
|
||||
val noteEvent = it.event
|
||||
val notifAuthor =
|
||||
if (noteEvent is LnZapEvent) {
|
||||
val zapRequest = noteEvent.zapRequest
|
||||
if (zapRequest != null) {
|
||||
if (noteEvent.zapRequest?.isPrivateZap() == true) {
|
||||
zapRequest.cachedPrivateZap()?.pubKey ?: zapRequest.pubKey
|
||||
} else {
|
||||
zapRequest.pubKey
|
||||
}
|
||||
} else {
|
||||
noteEvent.pubKey
|
||||
}
|
||||
} else {
|
||||
it.author?.pubkeyHex
|
||||
}
|
||||
|
||||
return it.event !is ChannelCreateEvent &&
|
||||
it.event !is ChannelMetadataEvent &&
|
||||
|
@ -96,10 +112,10 @@ class NotificationFeedFilter(val account: Account) : AdditiveFeedFilter<Note>()
|
|||
it.event !is BadgeDefinitionEvent &&
|
||||
it.event !is BadgeProfilesEvent &&
|
||||
it.event !is GiftWrapEvent &&
|
||||
(it.event is LnZapEvent || it.author !== loggedInUser) &&
|
||||
(filterParams.isGlobal || filterParams.followLists?.users?.contains(it.author?.pubkeyHex) == true) &&
|
||||
(it.event is LnZapEvent || notifAuthor != loggedInUserHex) &&
|
||||
(filterParams.isGlobal || filterParams.followLists?.users?.contains(notifAuthor) == true) &&
|
||||
it.event?.isTaggedUser(loggedInUserHex) ?: false &&
|
||||
(filterParams.isHiddenList || it.author == null || !account.isHidden(it.author!!.pubkeyHex)) &&
|
||||
(filterParams.isHiddenList || notifAuthor == null || !account.isHidden(notifAuthor)) &&
|
||||
tagsAnEventByUser(it, loggedInUserHex)
|
||||
}
|
||||
|
||||
|
|
|
@ -61,6 +61,7 @@ import com.vitorpamplona.amethyst.ui.screen.loggedIn.ChatroomScreen
|
|||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.ChatroomScreenByAuthor
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.CommunityScreen
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.DiscoverScreen
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.DraftListScreen
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.GeoHashScreen
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.HashtagScreen
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.HiddenUsersScreen
|
||||
|
@ -214,6 +215,7 @@ fun AppNavigation(
|
|||
|
||||
composable(Route.BlockedUsers.route, content = { HiddenUsersScreen(accountViewModel, nav) })
|
||||
composable(Route.Bookmarks.route, content = { BookmarkListScreen(accountViewModel, nav) })
|
||||
composable(Route.Drafts.route, content = { DraftListScreen(accountViewModel, nav) })
|
||||
|
||||
Route.Profile.let { route ->
|
||||
composable(
|
||||
|
|
|
@ -185,6 +185,8 @@ private fun RenderTopRouteBar(
|
|||
Route.Discover.base -> DiscoveryTopBar(followLists, drawerState, accountViewModel, nav)
|
||||
Route.Notification.base -> NotificationTopBar(followLists, drawerState, accountViewModel, nav)
|
||||
Route.Settings.base -> TopBarWithBackButton(stringResource(id = R.string.application_preferences), navPopBack)
|
||||
Route.Bookmarks.base -> TopBarWithBackButton(stringResource(id = R.string.bookmarks), navPopBack)
|
||||
Route.Drafts.base -> TopBarWithBackButton(stringResource(id = R.string.drafts), navPopBack)
|
||||
else -> {
|
||||
if (id != null) {
|
||||
when (currentRoute) {
|
||||
|
|
|
@ -159,7 +159,10 @@ fun DrawerContent(
|
|||
)
|
||||
|
||||
ListContent(
|
||||
modifier = Modifier.fillMaxWidth().weight(1f),
|
||||
modifier =
|
||||
Modifier
|
||||
.fillMaxWidth()
|
||||
.weight(1f),
|
||||
drawerState,
|
||||
openSheet,
|
||||
accountViewModel,
|
||||
|
@ -231,7 +234,8 @@ fun ProfileContentTemplate(
|
|||
model = profilePicture,
|
||||
contentDescription = stringResource(id = R.string.profile_image),
|
||||
modifier =
|
||||
Modifier.width(100.dp)
|
||||
Modifier
|
||||
.width(100.dp)
|
||||
.height(100.dp)
|
||||
.clip(shape = CircleShape)
|
||||
.border(3.dp, MaterialTheme.colorScheme.background, CircleShape)
|
||||
|
@ -244,7 +248,10 @@ fun ProfileContentTemplate(
|
|||
CreateTextWithEmoji(
|
||||
text = bestDisplayName,
|
||||
tags = tags,
|
||||
modifier = Modifier.padding(top = 7.dp).clickable(onClick = onClick),
|
||||
modifier =
|
||||
Modifier
|
||||
.padding(top = 7.dp)
|
||||
.clickable(onClick = onClick),
|
||||
fontWeight = FontWeight.Bold,
|
||||
fontSize = 18.sp,
|
||||
maxLines = 1,
|
||||
|
@ -455,7 +462,10 @@ fun ListContent(
|
|||
val context = LocalContext.current
|
||||
|
||||
Column(
|
||||
modifier = modifier.fillMaxHeight().verticalScroll(rememberScrollState()),
|
||||
modifier =
|
||||
modifier
|
||||
.fillMaxHeight()
|
||||
.verticalScroll(rememberScrollState()),
|
||||
) {
|
||||
NavigationRow(
|
||||
title = stringResource(R.string.profile),
|
||||
|
@ -475,6 +485,15 @@ fun ListContent(
|
|||
route = Route.Bookmarks.route,
|
||||
)
|
||||
|
||||
NavigationRow(
|
||||
title = stringResource(R.string.drafts),
|
||||
icon = Route.Drafts.icon,
|
||||
tint = MaterialTheme.colorScheme.onBackground,
|
||||
nav = nav,
|
||||
drawerState = drawerState,
|
||||
route = Route.Drafts.route,
|
||||
)
|
||||
|
||||
IconRowRelays(
|
||||
accountViewModel = accountViewModel,
|
||||
onClick = {
|
||||
|
@ -662,7 +681,8 @@ fun IconRow(
|
|||
) {
|
||||
Row(
|
||||
modifier =
|
||||
Modifier.fillMaxWidth()
|
||||
Modifier
|
||||
.fillMaxWidth()
|
||||
.combinedClickable(
|
||||
onClick = onClick,
|
||||
onLongClick = onLongClick,
|
||||
|
@ -693,10 +713,16 @@ fun IconRowRelays(
|
|||
onClick: () -> Unit,
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth().clickable { onClick() },
|
||||
modifier =
|
||||
Modifier
|
||||
.fillMaxWidth()
|
||||
.clickable { onClick() },
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth().padding(vertical = 15.dp, horizontal = 25.dp),
|
||||
modifier =
|
||||
Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(vertical = 15.dp, horizontal = 25.dp),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
Icon(
|
||||
|
@ -737,7 +763,10 @@ fun BottomContent(
|
|||
thickness = DividerThickness,
|
||||
)
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth().padding(horizontal = 15.dp),
|
||||
modifier =
|
||||
Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = 15.dp),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
ClickableText(
|
||||
|
|
|
@ -25,12 +25,14 @@ import com.vitorpamplona.amethyst.model.Note
|
|||
import com.vitorpamplona.amethyst.model.User
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
||||
import com.vitorpamplona.quartz.encoders.HexKey
|
||||
import com.vitorpamplona.quartz.events.AddressableEvent
|
||||
import com.vitorpamplona.quartz.events.ChannelCreateEvent
|
||||
import com.vitorpamplona.quartz.events.ChannelMessageEvent
|
||||
import com.vitorpamplona.quartz.events.ChannelMetadataEvent
|
||||
import com.vitorpamplona.quartz.events.ChatroomKey
|
||||
import com.vitorpamplona.quartz.events.ChatroomKeyable
|
||||
import com.vitorpamplona.quartz.events.CommunityDefinitionEvent
|
||||
import com.vitorpamplona.quartz.events.DraftEvent
|
||||
import com.vitorpamplona.quartz.events.EventInterface
|
||||
import com.vitorpamplona.quartz.events.IsInPublicChatChannel
|
||||
import com.vitorpamplona.quartz.events.LiveActivitiesChatMessageEvent
|
||||
import com.vitorpamplona.quartz.events.LiveActivitiesEvent
|
||||
import kotlinx.collections.immutable.persistentSetOf
|
||||
|
@ -40,18 +42,51 @@ fun routeFor(
|
|||
note: Note,
|
||||
loggedIn: User,
|
||||
): String? {
|
||||
val noteEvent = note.event
|
||||
val noteEvent = note.event ?: return "Note/${URLEncoder.encode(note.idHex, "utf-8")}"
|
||||
|
||||
if (
|
||||
noteEvent is ChannelMessageEvent ||
|
||||
noteEvent is ChannelCreateEvent ||
|
||||
noteEvent is ChannelMetadataEvent
|
||||
) {
|
||||
note.channelHex()?.let {
|
||||
return routeFor(noteEvent, loggedIn)
|
||||
}
|
||||
|
||||
fun routeFor(
|
||||
noteEvent: EventInterface,
|
||||
loggedIn: User,
|
||||
): String? {
|
||||
if (noteEvent is DraftEvent) {
|
||||
val innerEvent = noteEvent.preCachedDraft(loggedIn.pubkeyHex)
|
||||
|
||||
if (innerEvent is IsInPublicChatChannel) {
|
||||
innerEvent.channel()?.let {
|
||||
return "Channel/$it"
|
||||
}
|
||||
} else if (innerEvent is LiveActivitiesEvent) {
|
||||
innerEvent.address().toTag().let {
|
||||
return "Channel/${URLEncoder.encode(it, "utf-8")}"
|
||||
}
|
||||
} else if (innerEvent is LiveActivitiesChatMessageEvent) {
|
||||
innerEvent.activity()?.toTag()?.let {
|
||||
return "Channel/${URLEncoder.encode(it, "utf-8")}"
|
||||
}
|
||||
} else if (innerEvent is ChatroomKeyable) {
|
||||
val room = innerEvent.chatroomKey(loggedIn.pubkeyHex)
|
||||
loggedIn.createChatroom(room)
|
||||
return "Room/${room.hashCode()}"
|
||||
} else if (innerEvent is AddressableEvent) {
|
||||
return "Note/${URLEncoder.encode(noteEvent.address().toTag(), "utf-8")}"
|
||||
} else {
|
||||
return "Note/${URLEncoder.encode(noteEvent.id(), "utf-8")}"
|
||||
}
|
||||
} else if (noteEvent is IsInPublicChatChannel) {
|
||||
noteEvent.channel()?.let {
|
||||
return "Channel/$it"
|
||||
}
|
||||
} else if (noteEvent is LiveActivitiesEvent || noteEvent is LiveActivitiesChatMessageEvent) {
|
||||
note.channelHex()?.let {
|
||||
} else if (noteEvent is ChannelCreateEvent) {
|
||||
return "Channel/${noteEvent.id}"
|
||||
} else if (noteEvent is LiveActivitiesEvent) {
|
||||
noteEvent.address().toTag().let {
|
||||
return "Channel/${URLEncoder.encode(it, "utf-8")}"
|
||||
}
|
||||
} else if (noteEvent is LiveActivitiesChatMessageEvent) {
|
||||
noteEvent.activity()?.toTag()?.let {
|
||||
return "Channel/${URLEncoder.encode(it, "utf-8")}"
|
||||
}
|
||||
} else if (noteEvent is ChatroomKeyable) {
|
||||
|
@ -59,9 +94,11 @@ fun routeFor(
|
|||
loggedIn.createChatroom(room)
|
||||
return "Room/${room.hashCode()}"
|
||||
} else if (noteEvent is CommunityDefinitionEvent) {
|
||||
return "Community/${URLEncoder.encode(note.idHex, "utf-8")}"
|
||||
return "Community/${URLEncoder.encode(noteEvent.address().toTag(), "utf-8")}"
|
||||
} else if (noteEvent is AddressableEvent) {
|
||||
return "Note/${URLEncoder.encode(noteEvent.address().toTag(), "utf-8")}"
|
||||
} else {
|
||||
return "Note/${URLEncoder.encode(note.idHex, "utf-8")}"
|
||||
return "Note/${URLEncoder.encode(noteEvent.id(), "utf-8")}"
|
||||
}
|
||||
|
||||
return null
|
||||
|
|
|
@ -148,6 +148,13 @@ sealed class Route(
|
|||
contentDescriptor = R.string.route_home,
|
||||
)
|
||||
|
||||
object Drafts :
|
||||
Route(
|
||||
route = "Drafts",
|
||||
icon = R.drawable.ic_topics,
|
||||
contentDescriptor = R.string.drafts,
|
||||
)
|
||||
|
||||
object Profile :
|
||||
Route(
|
||||
route = "User/{id}",
|
||||
|
|
|
@ -69,7 +69,6 @@ import com.vitorpamplona.amethyst.ui.layouts.ChatHeaderLayout
|
|||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
||||
import com.vitorpamplona.amethyst.ui.theme.AccountPictureModifier
|
||||
import com.vitorpamplona.amethyst.ui.theme.Size55dp
|
||||
import com.vitorpamplona.amethyst.ui.theme.emptyLineItemModifier
|
||||
import com.vitorpamplona.amethyst.ui.theme.grayText
|
||||
import com.vitorpamplona.amethyst.ui.theme.placeholderText
|
||||
import com.vitorpamplona.quartz.encoders.HexKey
|
||||
|
@ -77,6 +76,7 @@ import com.vitorpamplona.quartz.events.ChannelCreateEvent
|
|||
import com.vitorpamplona.quartz.events.ChannelMetadataEvent
|
||||
import com.vitorpamplona.quartz.events.ChatroomKey
|
||||
import com.vitorpamplona.quartz.events.ChatroomKeyable
|
||||
import com.vitorpamplona.quartz.events.DraftEvent
|
||||
|
||||
@Composable
|
||||
fun ChatroomHeaderCompose(
|
||||
|
@ -102,12 +102,24 @@ fun ChatroomComposeChannelOrUser(
|
|||
accountViewModel: AccountViewModel,
|
||||
nav: (String) -> Unit,
|
||||
) {
|
||||
val channelHex by remember(baseNote) { derivedStateOf { baseNote.channelHex() } }
|
||||
if (baseNote.event is DraftEvent) {
|
||||
ObserveDraftEvent(baseNote, accountViewModel) {
|
||||
val channelHex by remember(it) { derivedStateOf { it.channelHex() } }
|
||||
|
||||
if (channelHex != null) {
|
||||
ChatroomChannel(channelHex!!, baseNote, accountViewModel, nav)
|
||||
if (channelHex != null) {
|
||||
ChatroomChannel(channelHex!!, it, accountViewModel, nav)
|
||||
} else {
|
||||
ChatroomPrivateMessages(it, accountViewModel, nav)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
ChatroomPrivateMessages(baseNote, accountViewModel, nav)
|
||||
val channelHex by remember(baseNote) { derivedStateOf { baseNote.channelHex() } }
|
||||
|
||||
if (channelHex != null) {
|
||||
ChatroomChannel(channelHex!!, baseNote, accountViewModel, nav)
|
||||
} else {
|
||||
ChatroomPrivateMessages(baseNote, accountViewModel, nav)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -128,9 +140,7 @@ private fun ChatroomPrivateMessages(
|
|||
if (room != null) {
|
||||
UserRoomCompose(baseNote, room, accountViewModel, nav)
|
||||
} else {
|
||||
Box(emptyLineItemModifier) {
|
||||
// Makes sure just a max amount of objects are loaded.
|
||||
}
|
||||
BlankNote()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -90,6 +90,7 @@ import com.vitorpamplona.amethyst.ui.theme.subtleBorder
|
|||
import com.vitorpamplona.quartz.events.ChannelCreateEvent
|
||||
import com.vitorpamplona.quartz.events.ChannelMetadataEvent
|
||||
import com.vitorpamplona.quartz.events.ChatMessageEvent
|
||||
import com.vitorpamplona.quartz.events.DraftEvent
|
||||
import com.vitorpamplona.quartz.events.EmptyTagList
|
||||
import com.vitorpamplona.quartz.events.ImmutableListOfLists
|
||||
import com.vitorpamplona.quartz.events.PrivateDmEvent
|
||||
|
@ -104,6 +105,7 @@ fun ChatroomMessageCompose(
|
|||
accountViewModel: AccountViewModel,
|
||||
nav: (String) -> Unit,
|
||||
onWantsToReply: (Note) -> Unit,
|
||||
onWantsToEditDraft: (Note) -> Unit,
|
||||
) {
|
||||
WatchNoteEvent(baseNote = baseNote, accountViewModel = accountViewModel) {
|
||||
WatchBlockAndReport(
|
||||
|
@ -122,6 +124,7 @@ fun ChatroomMessageCompose(
|
|||
accountViewModel,
|
||||
nav,
|
||||
onWantsToReply,
|
||||
onWantsToEditDraft,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
@ -138,6 +141,7 @@ fun NormalChatNote(
|
|||
accountViewModel: AccountViewModel,
|
||||
nav: (String) -> Unit,
|
||||
onWantsToReply: (Note) -> Unit,
|
||||
onWantsToEditDraft: (Note) -> Unit,
|
||||
) {
|
||||
val drawAuthorInfo by
|
||||
remember(note) {
|
||||
|
@ -251,6 +255,7 @@ fun NormalChatNote(
|
|||
innerQuote,
|
||||
backgroundBubbleColor,
|
||||
onWantsToReply,
|
||||
onWantsToEditDraft,
|
||||
canPreview,
|
||||
availableBubbleSize,
|
||||
showDetails,
|
||||
|
@ -264,7 +269,9 @@ fun NormalChatNote(
|
|||
note = note,
|
||||
popupExpanded = popupExpanded,
|
||||
onDismiss = { popupExpanded = false },
|
||||
onWantsToEditDraft = { onWantsToEditDraft(note) },
|
||||
accountViewModel = accountViewModel,
|
||||
nav = nav,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
@ -278,6 +285,7 @@ private fun RenderBubble(
|
|||
innerQuote: Boolean,
|
||||
backgroundBubbleColor: MutableState<Color>,
|
||||
onWantsToReply: (Note) -> Unit,
|
||||
onWantsToEditDraft: (Note) -> Unit,
|
||||
canPreview: Boolean,
|
||||
availableBubbleSize: MutableState<Int>,
|
||||
showDetails: State<Boolean>,
|
||||
|
@ -308,6 +316,7 @@ private fun RenderBubble(
|
|||
backgroundBubbleColor,
|
||||
bubbleSize,
|
||||
onWantsToReply,
|
||||
onWantsToEditDraft,
|
||||
canPreview,
|
||||
showDetails,
|
||||
accountViewModel,
|
||||
|
@ -326,6 +335,7 @@ private fun MessageBubbleLines(
|
|||
backgroundBubbleColor: MutableState<Color>,
|
||||
bubbleSize: MutableState<Int>,
|
||||
onWantsToReply: (Note) -> Unit,
|
||||
onWantsToEditDraft: (Note) -> Unit,
|
||||
canPreview: Boolean,
|
||||
showDetails: State<Boolean>,
|
||||
accountViewModel: AccountViewModel,
|
||||
|
@ -340,19 +350,24 @@ private fun MessageBubbleLines(
|
|||
)
|
||||
}
|
||||
|
||||
RenderReplyRow(
|
||||
note = baseNote,
|
||||
innerQuote = innerQuote,
|
||||
backgroundBubbleColor = backgroundBubbleColor,
|
||||
accountViewModel = accountViewModel,
|
||||
nav = nav,
|
||||
onWantsToReply = onWantsToReply,
|
||||
)
|
||||
if (baseNote.event !is DraftEvent) {
|
||||
RenderReplyRow(
|
||||
note = baseNote,
|
||||
innerQuote = innerQuote,
|
||||
backgroundBubbleColor = backgroundBubbleColor,
|
||||
accountViewModel = accountViewModel,
|
||||
nav = nav,
|
||||
onWantsToReply = onWantsToReply,
|
||||
onWantsToEditDraft = onWantsToEditDraft,
|
||||
)
|
||||
}
|
||||
|
||||
NoteRow(
|
||||
note = baseNote,
|
||||
canPreview = canPreview,
|
||||
innerQuote = innerQuote,
|
||||
onWantsToReply = onWantsToReply,
|
||||
onWantsToEditDraft = onWantsToEditDraft,
|
||||
backgroundBubbleColor = backgroundBubbleColor,
|
||||
accountViewModel = accountViewModel,
|
||||
nav = nav,
|
||||
|
@ -363,6 +378,9 @@ private fun MessageBubbleLines(
|
|||
bubbleSize = bubbleSize,
|
||||
availableBubbleSize = availableBubbleSize,
|
||||
firstColumn = {
|
||||
if (baseNote.isDraft()) {
|
||||
DisplayDraftChat()
|
||||
}
|
||||
IncognitoBadge(baseNote)
|
||||
ChatTimeAgo(baseNote)
|
||||
RelayBadgesHorizontal(baseNote, accountViewModel, nav = nav)
|
||||
|
@ -396,9 +414,10 @@ private fun RenderReplyRow(
|
|||
accountViewModel: AccountViewModel,
|
||||
nav: (String) -> Unit,
|
||||
onWantsToReply: (Note) -> Unit,
|
||||
onWantsToEditDraft: (Note) -> Unit,
|
||||
) {
|
||||
if (!innerQuote && note.replyTo?.lastOrNull() != null) {
|
||||
RenderReply(note, backgroundBubbleColor, accountViewModel, nav, onWantsToReply)
|
||||
RenderReply(note, backgroundBubbleColor, accountViewModel, nav, onWantsToReply, onWantsToEditDraft)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -409,6 +428,7 @@ private fun RenderReply(
|
|||
accountViewModel: AccountViewModel,
|
||||
nav: (String) -> Unit,
|
||||
onWantsToReply: (Note) -> Unit,
|
||||
onWantsToEditDraft: (Note) -> Unit,
|
||||
) {
|
||||
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||
val replyTo =
|
||||
|
@ -427,6 +447,7 @@ private fun RenderReply(
|
|||
accountViewModel = accountViewModel,
|
||||
nav = nav,
|
||||
onWantsToReply = onWantsToReply,
|
||||
onWantsToEditDraft = onWantsToEditDraft,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
@ -437,6 +458,8 @@ private fun NoteRow(
|
|||
note: Note,
|
||||
canPreview: Boolean,
|
||||
innerQuote: Boolean,
|
||||
onWantsToReply: (Note) -> Unit,
|
||||
onWantsToEditDraft: (Note) -> Unit,
|
||||
backgroundBubbleColor: MutableState<Color>,
|
||||
accountViewModel: AccountViewModel,
|
||||
nav: (String) -> Unit,
|
||||
|
@ -449,6 +472,18 @@ private fun NoteRow(
|
|||
is ChannelMetadataEvent -> {
|
||||
RenderChangeChannelMetadataNote(note)
|
||||
}
|
||||
is DraftEvent -> {
|
||||
RenderDraftEvent(
|
||||
note,
|
||||
canPreview,
|
||||
innerQuote,
|
||||
onWantsToReply,
|
||||
onWantsToEditDraft,
|
||||
backgroundBubbleColor,
|
||||
accountViewModel,
|
||||
nav,
|
||||
)
|
||||
}
|
||||
else -> {
|
||||
RenderRegularTextNote(
|
||||
note,
|
||||
|
@ -463,6 +498,43 @@ private fun NoteRow(
|
|||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun RenderDraftEvent(
|
||||
note: Note,
|
||||
canPreview: Boolean,
|
||||
innerQuote: Boolean,
|
||||
onWantsToReply: (Note) -> Unit,
|
||||
onWantsToEditDraft: (Note) -> Unit,
|
||||
backgroundBubbleColor: MutableState<Color>,
|
||||
accountViewModel: AccountViewModel,
|
||||
nav: (String) -> Unit,
|
||||
) {
|
||||
ObserveDraftEvent(note, accountViewModel) {
|
||||
Column {
|
||||
RenderReplyRow(
|
||||
note = it,
|
||||
innerQuote = innerQuote,
|
||||
backgroundBubbleColor = backgroundBubbleColor,
|
||||
accountViewModel = accountViewModel,
|
||||
nav = nav,
|
||||
onWantsToReply = onWantsToReply,
|
||||
onWantsToEditDraft = onWantsToEditDraft,
|
||||
)
|
||||
|
||||
NoteRow(
|
||||
note = it,
|
||||
canPreview = canPreview,
|
||||
innerQuote = innerQuote,
|
||||
onWantsToReply = onWantsToReply,
|
||||
onWantsToEditDraft = onWantsToEditDraft,
|
||||
backgroundBubbleColor = backgroundBubbleColor,
|
||||
accountViewModel = accountViewModel,
|
||||
nav = nav,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun ConstrainedStatusRow(
|
||||
bubbleSize: MutableState<Int>,
|
||||
|
|
|
@ -29,8 +29,8 @@ import androidx.compose.foundation.layout.Row
|
|||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.MutableState
|
||||
|
@ -39,6 +39,7 @@ import androidx.compose.runtime.derivedStateOf
|
|||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.livedata.observeAsState
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.produceState
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.rememberCoroutineScope
|
||||
import androidx.compose.ui.Alignment
|
||||
|
@ -48,6 +49,7 @@ import androidx.compose.ui.graphics.Brush
|
|||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.compositeOver
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.lifecycle.distinctUntilChanged
|
||||
import androidx.lifecycle.map
|
||||
|
@ -82,6 +84,8 @@ import com.vitorpamplona.amethyst.ui.note.types.RenderAppDefinition
|
|||
import com.vitorpamplona.amethyst.ui.note.types.RenderAudioHeader
|
||||
import com.vitorpamplona.amethyst.ui.note.types.RenderAudioTrack
|
||||
import com.vitorpamplona.amethyst.ui.note.types.RenderBadgeAward
|
||||
import com.vitorpamplona.amethyst.ui.note.types.RenderChannelMessage
|
||||
import com.vitorpamplona.amethyst.ui.note.types.RenderChatMessage
|
||||
import com.vitorpamplona.amethyst.ui.note.types.RenderClassifieds
|
||||
import com.vitorpamplona.amethyst.ui.note.types.RenderEmojiPack
|
||||
import com.vitorpamplona.amethyst.ui.note.types.RenderFhirResource
|
||||
|
@ -89,6 +93,7 @@ import com.vitorpamplona.amethyst.ui.note.types.RenderGitIssueEvent
|
|||
import com.vitorpamplona.amethyst.ui.note.types.RenderGitPatchEvent
|
||||
import com.vitorpamplona.amethyst.ui.note.types.RenderGitRepositoryEvent
|
||||
import com.vitorpamplona.amethyst.ui.note.types.RenderHighlight
|
||||
import com.vitorpamplona.amethyst.ui.note.types.RenderLiveActivityChatMessage
|
||||
import com.vitorpamplona.amethyst.ui.note.types.RenderLiveActivityEvent
|
||||
import com.vitorpamplona.amethyst.ui.note.types.RenderLongFormContent
|
||||
import com.vitorpamplona.amethyst.ui.note.types.RenderPinListEvent
|
||||
|
@ -105,15 +110,17 @@ import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
|||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.ChannelHeader
|
||||
import com.vitorpamplona.amethyst.ui.theme.DoubleHorzSpacer
|
||||
import com.vitorpamplona.amethyst.ui.theme.DoubleVertSpacer
|
||||
import com.vitorpamplona.amethyst.ui.theme.Font12SP
|
||||
import com.vitorpamplona.amethyst.ui.theme.HalfDoubleVertSpacer
|
||||
import com.vitorpamplona.amethyst.ui.theme.HalfEndPadding
|
||||
import com.vitorpamplona.amethyst.ui.theme.HalfPadding
|
||||
import com.vitorpamplona.amethyst.ui.theme.HalfStartPadding
|
||||
import com.vitorpamplona.amethyst.ui.theme.Size25dp
|
||||
import com.vitorpamplona.amethyst.ui.theme.Size30Modifier
|
||||
import com.vitorpamplona.amethyst.ui.theme.Size34dp
|
||||
import com.vitorpamplona.amethyst.ui.theme.Size55Modifier
|
||||
import com.vitorpamplona.amethyst.ui.theme.Size55dp
|
||||
import com.vitorpamplona.amethyst.ui.theme.StdHorzSpacer
|
||||
import com.vitorpamplona.amethyst.ui.theme.StdVertSpacer
|
||||
import com.vitorpamplona.amethyst.ui.theme.UserNameMaxRowHeight
|
||||
import com.vitorpamplona.amethyst.ui.theme.UserNameRowHeight
|
||||
import com.vitorpamplona.amethyst.ui.theme.WidthAuthorPictureModifier
|
||||
|
@ -122,6 +129,7 @@ import com.vitorpamplona.amethyst.ui.theme.channelNotePictureModifier
|
|||
import com.vitorpamplona.amethyst.ui.theme.grayText
|
||||
import com.vitorpamplona.amethyst.ui.theme.newItemBackgroundColor
|
||||
import com.vitorpamplona.amethyst.ui.theme.normalWithTopMarginNoteModifier
|
||||
import com.vitorpamplona.amethyst.ui.theme.placeholderText
|
||||
import com.vitorpamplona.amethyst.ui.theme.replyBackground
|
||||
import com.vitorpamplona.amethyst.ui.theme.replyModifier
|
||||
import com.vitorpamplona.quartz.events.AppDefinitionEvent
|
||||
|
@ -132,9 +140,11 @@ import com.vitorpamplona.quartz.events.BaseTextNoteEvent
|
|||
import com.vitorpamplona.quartz.events.ChannelCreateEvent
|
||||
import com.vitorpamplona.quartz.events.ChannelMessageEvent
|
||||
import com.vitorpamplona.quartz.events.ChannelMetadataEvent
|
||||
import com.vitorpamplona.quartz.events.ChatMessageEvent
|
||||
import com.vitorpamplona.quartz.events.ClassifiedsEvent
|
||||
import com.vitorpamplona.quartz.events.CommunityDefinitionEvent
|
||||
import com.vitorpamplona.quartz.events.CommunityPostApprovalEvent
|
||||
import com.vitorpamplona.quartz.events.DraftEvent
|
||||
import com.vitorpamplona.quartz.events.EmojiPackEvent
|
||||
import com.vitorpamplona.quartz.events.FhirResourceEvent
|
||||
import com.vitorpamplona.quartz.events.FileHeaderEvent
|
||||
|
@ -160,8 +170,6 @@ import com.vitorpamplona.quartz.events.TextNoteModificationEvent
|
|||
import com.vitorpamplona.quartz.events.VideoHorizontalEvent
|
||||
import com.vitorpamplona.quartz.events.VideoVerticalEvent
|
||||
import com.vitorpamplona.quartz.events.WikiNoteEvent
|
||||
import kotlinx.collections.immutable.persistentListOf
|
||||
import kotlinx.collections.immutable.toImmutableList
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
@Composable
|
||||
|
@ -273,9 +281,7 @@ fun AcceptableNote(
|
|||
is FileHeaderEvent -> FileHeaderDisplay(baseNote, false, accountViewModel)
|
||||
is FileStorageHeaderEvent -> FileStorageHeaderDisplay(baseNote, false, accountViewModel)
|
||||
else ->
|
||||
LongPressToQuickAction(baseNote = baseNote, accountViewModel = accountViewModel) {
|
||||
showPopup,
|
||||
->
|
||||
LongPressToQuickAction(baseNote = baseNote, accountViewModel = accountViewModel) { showPopup ->
|
||||
CheckNewAndRenderNote(
|
||||
baseNote = baseNote,
|
||||
routeForLastRead = routeForLastRead,
|
||||
|
@ -460,7 +466,7 @@ fun InnerNoteWithReactions(
|
|||
}
|
||||
}
|
||||
|
||||
val isNotRepost = baseNote.event !is RepostEvent && baseNote.event !is GenericRepostEvent
|
||||
val isNotRepost = baseNote.event !is RepostEvent && baseNote.event !is GenericRepostEvent && baseNote.event !is DraftEvent
|
||||
|
||||
if (isNotRepost) {
|
||||
if (makeItShort) {
|
||||
|
@ -477,6 +483,10 @@ fun InnerNoteWithReactions(
|
|||
nav = nav,
|
||||
)
|
||||
}
|
||||
} else {
|
||||
if (baseNote.event is DraftEvent) {
|
||||
Spacer(modifier = DoubleVertSpacer)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -515,16 +525,6 @@ fun NoteBody(
|
|||
Spacer(modifier = Modifier.height(3.dp))
|
||||
}
|
||||
|
||||
if (!makeItShort) {
|
||||
ReplyRow(
|
||||
baseNote,
|
||||
unPackReply,
|
||||
backgroundColor,
|
||||
accountViewModel,
|
||||
nav,
|
||||
)
|
||||
}
|
||||
|
||||
RenderNoteRow(
|
||||
baseNote = baseNote,
|
||||
backgroundColor = backgroundColor,
|
||||
|
@ -532,6 +532,7 @@ fun NoteBody(
|
|||
canPreview = canPreview,
|
||||
editState = editState,
|
||||
quotesLeft = quotesLeft,
|
||||
unPackReply = unPackReply,
|
||||
accountViewModel = accountViewModel,
|
||||
nav = nav,
|
||||
)
|
||||
|
@ -551,6 +552,7 @@ private fun RenderNoteRow(
|
|||
makeItShort: Boolean,
|
||||
canPreview: Boolean,
|
||||
quotesLeft: Int,
|
||||
unPackReply: Boolean,
|
||||
editState: State<GenericLoadable<EditState>>,
|
||||
accountViewModel: AccountViewModel,
|
||||
nav: (String) -> Unit,
|
||||
|
@ -560,6 +562,7 @@ private fun RenderNoteRow(
|
|||
is AppDefinitionEvent -> RenderAppDefinition(baseNote, accountViewModel, nav)
|
||||
is AudioTrackEvent -> RenderAudioTrack(baseNote, accountViewModel, nav)
|
||||
is AudioHeaderEvent -> RenderAudioHeader(baseNote, accountViewModel, nav)
|
||||
is DraftEvent -> RenderDraft(baseNote, backgroundColor, accountViewModel, nav)
|
||||
is ReactionEvent -> RenderReaction(baseNote, quotesLeft, backgroundColor, accountViewModel, nav)
|
||||
is RepostEvent -> RenderRepost(baseNote, quotesLeft, backgroundColor, accountViewModel, nav)
|
||||
is GenericRepostEvent -> RenderRepost(baseNote, quotesLeft, backgroundColor, accountViewModel, nav)
|
||||
|
@ -607,6 +610,18 @@ private fun RenderNoteRow(
|
|||
nav,
|
||||
)
|
||||
}
|
||||
is ChatMessageEvent -> {
|
||||
RenderChatMessage(
|
||||
baseNote,
|
||||
makeItShort,
|
||||
canPreview,
|
||||
quotesLeft,
|
||||
backgroundColor,
|
||||
editState,
|
||||
accountViewModel,
|
||||
nav,
|
||||
)
|
||||
}
|
||||
is ClassifiedsEvent -> {
|
||||
RenderClassifieds(
|
||||
noteEvent,
|
||||
|
@ -632,6 +647,7 @@ private fun RenderNoteRow(
|
|||
makeItShort,
|
||||
canPreview,
|
||||
quotesLeft,
|
||||
unPackReply,
|
||||
backgroundColor,
|
||||
accountViewModel,
|
||||
nav,
|
||||
|
@ -661,8 +677,8 @@ private fun RenderNoteRow(
|
|||
nav,
|
||||
)
|
||||
}
|
||||
else -> {
|
||||
RenderTextEvent(
|
||||
is ChannelMessageEvent ->
|
||||
RenderChannelMessage(
|
||||
baseNote,
|
||||
makeItShort,
|
||||
canPreview,
|
||||
|
@ -672,6 +688,84 @@ private fun RenderNoteRow(
|
|||
accountViewModel,
|
||||
nav,
|
||||
)
|
||||
is LiveActivitiesChatMessageEvent ->
|
||||
RenderLiveActivityChatMessage(
|
||||
baseNote,
|
||||
makeItShort,
|
||||
canPreview,
|
||||
quotesLeft,
|
||||
backgroundColor,
|
||||
editState,
|
||||
accountViewModel,
|
||||
nav,
|
||||
)
|
||||
else -> {
|
||||
RenderTextEvent(
|
||||
baseNote,
|
||||
makeItShort,
|
||||
canPreview,
|
||||
quotesLeft,
|
||||
unPackReply,
|
||||
backgroundColor,
|
||||
editState,
|
||||
accountViewModel,
|
||||
nav,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun ObserveDraftEvent(
|
||||
note: Note,
|
||||
accountViewModel: AccountViewModel,
|
||||
render: @Composable (Note) -> Unit,
|
||||
) {
|
||||
val noteState by note.live().metadata.observeAsState()
|
||||
|
||||
val noteEvent = noteState?.note?.event as? DraftEvent ?: return
|
||||
val noteAuthor = noteState?.note?.author ?: return
|
||||
|
||||
val innerNote =
|
||||
produceState(initialValue = accountViewModel.createTempCachedDraftNote(noteEvent, noteAuthor), noteEvent.id) {
|
||||
if (value == null || value?.event?.id() != noteEvent.id) {
|
||||
accountViewModel.createTempDraftNote(noteEvent, noteAuthor) {
|
||||
value = it
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
innerNote.value?.let {
|
||||
render(it)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun RenderDraft(
|
||||
note: Note,
|
||||
backgroundColor: MutableState<Color>,
|
||||
accountViewModel: AccountViewModel,
|
||||
nav: (String) -> Unit,
|
||||
) {
|
||||
ObserveDraftEvent(note, accountViewModel) {
|
||||
val edits = remember { mutableStateOf(GenericLoadable.Empty<EditState>()) }
|
||||
|
||||
RenderNoteRow(
|
||||
baseNote = it,
|
||||
backgroundColor = backgroundColor,
|
||||
makeItShort = false,
|
||||
canPreview = true,
|
||||
editState = edits,
|
||||
quotesLeft = 3,
|
||||
unPackReply = true,
|
||||
accountViewModel = accountViewModel,
|
||||
nav = nav,
|
||||
)
|
||||
|
||||
val zapSplits = remember(it.event) { it.event?.hasZapSplitSetup() }
|
||||
if (zapSplits == true) {
|
||||
Spacer(modifier = HalfDoubleVertSpacer)
|
||||
DisplayZapSplits(it.event!!, false, accountViewModel, nav)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -709,81 +803,7 @@ fun getGradient(backgroundColor: MutableState<Color>): Brush {
|
|||
}
|
||||
|
||||
@Composable
|
||||
private fun ReplyRow(
|
||||
note: Note,
|
||||
unPackReply: Boolean,
|
||||
backgroundColor: MutableState<Color>,
|
||||
accountViewModel: AccountViewModel,
|
||||
nav: (String) -> Unit,
|
||||
) {
|
||||
val noteEvent = note.event
|
||||
|
||||
val showReply by
|
||||
remember(note) {
|
||||
derivedStateOf {
|
||||
noteEvent is BaseTextNoteEvent && (note.replyTo != null || noteEvent.hasAnyTaggedUser())
|
||||
}
|
||||
}
|
||||
|
||||
val showChannelInfo by
|
||||
remember(note) {
|
||||
derivedStateOf {
|
||||
if (noteEvent is ChannelMessageEvent || noteEvent is LiveActivitiesChatMessageEvent) {
|
||||
note.channelHex()
|
||||
} else {
|
||||
null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
showChannelInfo?.let {
|
||||
ChannelHeader(
|
||||
channelHex = it,
|
||||
showVideo = false,
|
||||
sendToChannel = true,
|
||||
modifier = MaterialTheme.colorScheme.replyModifier.padding(10.dp),
|
||||
accountViewModel = accountViewModel,
|
||||
nav = nav,
|
||||
)
|
||||
Spacer(modifier = StdVertSpacer)
|
||||
}
|
||||
|
||||
if (showReply) {
|
||||
val replyingDirectlyTo =
|
||||
remember(note) {
|
||||
if (noteEvent is BaseTextNoteEvent) {
|
||||
val replyingTo = noteEvent.replyingToAddressOrEvent()
|
||||
if (replyingTo != null) {
|
||||
val newNote = accountViewModel.getNoteIfExists(replyingTo)
|
||||
if (newNote != null && newNote.channelHex() == null && newNote.event?.kind() != CommunityDefinitionEvent.KIND) {
|
||||
newNote
|
||||
} else {
|
||||
note.replyTo?.lastOrNull { it.event?.kind() != CommunityDefinitionEvent.KIND }
|
||||
}
|
||||
} else {
|
||||
note.replyTo?.lastOrNull { it.event?.kind() != CommunityDefinitionEvent.KIND }
|
||||
}
|
||||
} else {
|
||||
note.replyTo?.lastOrNull { it.event?.kind() != CommunityDefinitionEvent.KIND }
|
||||
}
|
||||
}
|
||||
if (replyingDirectlyTo != null && unPackReply) {
|
||||
ReplyNoteComposition(replyingDirectlyTo, backgroundColor, accountViewModel, nav)
|
||||
Spacer(modifier = StdVertSpacer)
|
||||
} else if (showChannelInfo != null) {
|
||||
val replies = remember { note.replyTo?.toImmutableList() }
|
||||
val mentions =
|
||||
remember {
|
||||
(note.event as? BaseTextNoteEvent)?.mentions()?.toImmutableList() ?: persistentListOf()
|
||||
}
|
||||
|
||||
ReplyInformationChannel(replies, mentions, accountViewModel, nav)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun ReplyNoteComposition(
|
||||
fun ReplyNoteComposition(
|
||||
replyingDirectlyTo: Note,
|
||||
backgroundColor: MutableState<Color>,
|
||||
accountViewModel: AccountViewModel,
|
||||
|
@ -868,6 +888,29 @@ fun DisplayOtsIfInOriginal(
|
|||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun DisplayDraft() {
|
||||
Text(
|
||||
"Draft",
|
||||
fontWeight = FontWeight.Bold,
|
||||
color = MaterialTheme.colorScheme.placeholderText,
|
||||
maxLines = 1,
|
||||
modifier = HalfStartPadding,
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun DisplayDraftChat() {
|
||||
Text(
|
||||
"Draft",
|
||||
color = MaterialTheme.colorScheme.placeholderText,
|
||||
modifier = HalfEndPadding,
|
||||
fontWeight = FontWeight.Bold,
|
||||
fontSize = Font12SP,
|
||||
maxLines = 1,
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun FirstUserInfoRow(
|
||||
baseNote: Note,
|
||||
|
@ -910,6 +953,10 @@ fun FirstUserInfoRow(
|
|||
}
|
||||
}
|
||||
|
||||
if (baseNote.isDraft()) {
|
||||
DisplayDraft()
|
||||
}
|
||||
|
||||
TimeAgo(baseNote)
|
||||
|
||||
MoreOptionsButton(baseNote, editState, accountViewModel, nav)
|
||||
|
|
|
@ -40,6 +40,7 @@ import androidx.compose.material.icons.filled.AlternateEmail
|
|||
import androidx.compose.material.icons.filled.Block
|
||||
import androidx.compose.material.icons.filled.ContentCopy
|
||||
import androidx.compose.material.icons.filled.Delete
|
||||
import androidx.compose.material.icons.filled.Edit
|
||||
import androidx.compose.material.icons.filled.FormatQuote
|
||||
import androidx.compose.material.icons.filled.PersonAdd
|
||||
import androidx.compose.material.icons.filled.PersonRemove
|
||||
|
@ -84,6 +85,7 @@ import androidx.core.graphics.ColorUtils
|
|||
import com.vitorpamplona.amethyst.R
|
||||
import com.vitorpamplona.amethyst.model.AddressableNote
|
||||
import com.vitorpamplona.amethyst.model.Note
|
||||
import com.vitorpamplona.amethyst.ui.actions.NewPostView
|
||||
import com.vitorpamplona.amethyst.ui.components.SelectTextDialog
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.ReportNoteDialog
|
||||
|
@ -136,11 +138,16 @@ fun LongPressToQuickAction(
|
|||
) {
|
||||
val popupExpanded = remember { mutableStateOf(false) }
|
||||
val showPopup = remember { { popupExpanded.value = true } }
|
||||
val hidePopup = remember { { popupExpanded.value = false } }
|
||||
|
||||
content(showPopup)
|
||||
|
||||
NoteQuickActionMenu(baseNote, popupExpanded.value, hidePopup, accountViewModel)
|
||||
NoteQuickActionMenu(
|
||||
note = baseNote,
|
||||
popupExpanded = popupExpanded.value,
|
||||
onDismiss = { popupExpanded.value = false },
|
||||
accountViewModel = accountViewModel,
|
||||
nav = {},
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
|
@ -149,6 +156,39 @@ fun NoteQuickActionMenu(
|
|||
popupExpanded: Boolean,
|
||||
onDismiss: () -> Unit,
|
||||
accountViewModel: AccountViewModel,
|
||||
nav: (String) -> Unit,
|
||||
) {
|
||||
val editDraftDialog = remember { mutableStateOf(false) }
|
||||
|
||||
if (editDraftDialog.value) {
|
||||
NewPostView(
|
||||
onClose = {
|
||||
editDraftDialog.value = false
|
||||
},
|
||||
accountViewModel = accountViewModel,
|
||||
draft = note,
|
||||
nav = { },
|
||||
)
|
||||
}
|
||||
|
||||
NoteQuickActionMenu(
|
||||
note = note,
|
||||
popupExpanded = popupExpanded,
|
||||
onDismiss = onDismiss,
|
||||
onWantsToEditDraft = { editDraftDialog.value = true },
|
||||
accountViewModel = accountViewModel,
|
||||
nav = nav,
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun NoteQuickActionMenu(
|
||||
note: Note,
|
||||
popupExpanded: Boolean,
|
||||
onDismiss: () -> Unit,
|
||||
onWantsToEditDraft: () -> Unit,
|
||||
accountViewModel: AccountViewModel,
|
||||
nav: (String) -> Unit,
|
||||
) {
|
||||
val showSelectTextDialog = remember { mutableStateOf(false) }
|
||||
val showDeleteAlertDialog = remember { mutableStateOf(false) }
|
||||
|
@ -163,6 +203,7 @@ fun NoteQuickActionMenu(
|
|||
showBlockAlertDialog,
|
||||
showDeleteAlertDialog,
|
||||
showReportDialog,
|
||||
onWantsToEditDraft,
|
||||
)
|
||||
}
|
||||
|
||||
|
@ -209,6 +250,7 @@ private fun RenderMainPopup(
|
|||
showBlockAlertDialog: MutableState<Boolean>,
|
||||
showDeleteAlertDialog: MutableState<Boolean>,
|
||||
showReportDialog: MutableState<Boolean>,
|
||||
onWantsToEditDraft: () -> Unit,
|
||||
) {
|
||||
val context = LocalContext.current
|
||||
val primaryLight = lightenColor(MaterialTheme.colorScheme.primary, 0.1f)
|
||||
|
@ -339,31 +381,41 @@ private fun RenderMainPopup(
|
|||
onDismiss()
|
||||
}
|
||||
VerticalDivider(color = primaryLight)
|
||||
NoteQuickActionItem(
|
||||
icon = Icons.Default.Share,
|
||||
label = stringResource(R.string.quick_action_share),
|
||||
) {
|
||||
val sendIntent =
|
||||
Intent().apply {
|
||||
action = Intent.ACTION_SEND
|
||||
type = "text/plain"
|
||||
putExtra(
|
||||
Intent.EXTRA_TEXT,
|
||||
externalLinkForNote(note),
|
||||
)
|
||||
putExtra(
|
||||
Intent.EXTRA_TITLE,
|
||||
context.getString(R.string.quick_action_share_browser_link),
|
||||
)
|
||||
}
|
||||
if (isOwnNote && note.isDraft()) {
|
||||
NoteQuickActionItem(
|
||||
Icons.Default.Edit,
|
||||
stringResource(R.string.edit_draft),
|
||||
) {
|
||||
onDismiss()
|
||||
onWantsToEditDraft()
|
||||
}
|
||||
} else {
|
||||
NoteQuickActionItem(
|
||||
icon = Icons.Default.Share,
|
||||
label = stringResource(R.string.quick_action_share),
|
||||
) {
|
||||
val sendIntent =
|
||||
Intent().apply {
|
||||
action = Intent.ACTION_SEND
|
||||
type = "text/plain"
|
||||
putExtra(
|
||||
Intent.EXTRA_TEXT,
|
||||
externalLinkForNote(note),
|
||||
)
|
||||
putExtra(
|
||||
Intent.EXTRA_TITLE,
|
||||
context.getString(R.string.quick_action_share_browser_link),
|
||||
)
|
||||
}
|
||||
|
||||
val shareIntent =
|
||||
Intent.createChooser(
|
||||
sendIntent,
|
||||
context.getString(R.string.quick_action_share),
|
||||
)
|
||||
ContextCompat.startActivity(context, shareIntent, null)
|
||||
onDismiss()
|
||||
val shareIntent =
|
||||
Intent.createChooser(
|
||||
sendIntent,
|
||||
context.getString(R.string.quick_action_share),
|
||||
)
|
||||
ContextCompat.startActivity(context, shareIntent, null)
|
||||
onDismiss()
|
||||
}
|
||||
}
|
||||
|
||||
if (!isOwnNote) {
|
||||
|
@ -389,14 +441,20 @@ fun NoteQuickActionItem(
|
|||
onClick: () -> Unit,
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier.size(70.dp).clickable { onClick() },
|
||||
modifier =
|
||||
Modifier
|
||||
.size(70.dp)
|
||||
.clickable { onClick() },
|
||||
verticalArrangement = Arrangement.Center,
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
) {
|
||||
Icon(
|
||||
imageVector = icon,
|
||||
contentDescription = null,
|
||||
modifier = Modifier.size(24.dp).padding(bottom = 5.dp),
|
||||
modifier =
|
||||
Modifier
|
||||
.size(24.dp)
|
||||
.padding(bottom = 5.dp),
|
||||
tint = Color.White,
|
||||
)
|
||||
Text(text = label, fontSize = 12.sp, color = Color.White, textAlign = TextAlign.Center)
|
||||
|
@ -527,7 +585,10 @@ fun QuickActionAlertDialog(
|
|||
text = { Text(textContent) },
|
||||
confirmButton = {
|
||||
Row(
|
||||
modifier = Modifier.padding(all = 8.dp).fillMaxWidth(),
|
||||
modifier =
|
||||
Modifier
|
||||
.padding(all = 8.dp)
|
||||
.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.SpaceBetween,
|
||||
) {
|
||||
TextButton(onClick = onClickDontShowAgain) {
|
||||
|
|
|
@ -310,7 +310,12 @@ fun RenderZapRaiser(
|
|||
}
|
||||
|
||||
LinearProgressIndicator(
|
||||
modifier = remember(details) { Modifier.fillMaxWidth().height(if (details) 24.dp else 4.dp) },
|
||||
modifier =
|
||||
remember(details) {
|
||||
Modifier
|
||||
.fillMaxWidth()
|
||||
.height(if (details) 24.dp else 4.dp)
|
||||
},
|
||||
color = color,
|
||||
progress = { zapraiserStatus.progress },
|
||||
)
|
||||
|
@ -590,6 +595,13 @@ fun ReplyReaction(
|
|||
IconButton(
|
||||
modifier = iconSizeModifier,
|
||||
onClick = {
|
||||
if (baseNote.isDraft()) {
|
||||
accountViewModel.toast(
|
||||
R.string.draft_note,
|
||||
R.string.it_s_not_possible_to_reply_to_a_draft_note,
|
||||
)
|
||||
return@IconButton
|
||||
}
|
||||
if (accountViewModel.isWriteable()) {
|
||||
onPress()
|
||||
} else {
|
||||
|
@ -776,7 +788,8 @@ fun LikeReaction(
|
|||
Box(
|
||||
contentAlignment = Center,
|
||||
modifier =
|
||||
Modifier.size(iconSize)
|
||||
Modifier
|
||||
.size(iconSize)
|
||||
.combinedClickable(
|
||||
role = Role.Button,
|
||||
interactionSource = remember { MutableInteractionSource() },
|
||||
|
@ -784,6 +797,7 @@ fun LikeReaction(
|
|||
onClick = {
|
||||
likeClick(
|
||||
accountViewModel,
|
||||
baseNote,
|
||||
onMultipleChoices = { wantsToReact = true },
|
||||
onWantsToSignReaction = { accountViewModel.reactToOrDelete(baseNote) },
|
||||
)
|
||||
|
@ -886,9 +900,17 @@ fun ObserveLikeText(
|
|||
|
||||
private fun likeClick(
|
||||
accountViewModel: AccountViewModel,
|
||||
baseNote: Note,
|
||||
onMultipleChoices: () -> Unit,
|
||||
onWantsToSignReaction: () -> Unit,
|
||||
) {
|
||||
if (baseNote.isDraft()) {
|
||||
accountViewModel.toast(
|
||||
R.string.draft_note,
|
||||
R.string.it_s_not_possible_to_react_to_a_draft_note,
|
||||
)
|
||||
return
|
||||
}
|
||||
if (accountViewModel.account.reactionChoices.isEmpty()) {
|
||||
accountViewModel.toast(
|
||||
R.string.no_reactions_setup,
|
||||
|
@ -1082,6 +1104,14 @@ fun zapClick(
|
|||
onError: (String, String) -> Unit,
|
||||
onPayViaIntent: (ImmutableList<ZapPaymentHandler.Payable>) -> Unit,
|
||||
) {
|
||||
if (baseNote.isDraft()) {
|
||||
accountViewModel.toast(
|
||||
R.string.draft_note,
|
||||
R.string.it_s_not_possible_to_zap_to_a_draft_note,
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
if (accountViewModel.account.zapAmountChoices.isEmpty()) {
|
||||
accountViewModel.toast(
|
||||
context.getString(R.string.error_dialog_zap_error),
|
||||
|
|
|
@ -179,7 +179,7 @@ fun ZapCustomDialog(
|
|||
)
|
||||
|
||||
ZapButton(
|
||||
isActive = postViewModel.canSend(),
|
||||
isActive = postViewModel.canSend() && !baseNote.isDraft(),
|
||||
) {
|
||||
accountViewModel.zap(
|
||||
baseNote,
|
||||
|
|
|
@ -57,6 +57,7 @@ import androidx.compose.ui.unit.dp
|
|||
import androidx.compose.ui.window.Dialog
|
||||
import androidx.compose.ui.window.DialogProperties
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import androidx.lifecycle.viewmodel.compose.viewModel
|
||||
import com.vitorpamplona.amethyst.R
|
||||
import com.vitorpamplona.amethyst.model.Account
|
||||
|
@ -175,19 +176,22 @@ class AddBountyAmountViewModel : ViewModel() {
|
|||
val newValue = nextAmount.text.trim().toLongOrNull()
|
||||
|
||||
if (newValue != null) {
|
||||
account?.sendPost(
|
||||
message = newValue.toString(),
|
||||
replyTo = listOfNotNull(bounty),
|
||||
mentions = listOfNotNull(bounty?.author),
|
||||
tags = listOf("bounty-added-reward"),
|
||||
wantsToMarkAsSensitive = false,
|
||||
replyingTo = null,
|
||||
root = null,
|
||||
directMentions = setOf(),
|
||||
forkedFrom = null,
|
||||
)
|
||||
viewModelScope.launch {
|
||||
account?.sendPost(
|
||||
message = newValue.toString(),
|
||||
replyTo = listOfNotNull(bounty),
|
||||
mentions = listOfNotNull(bounty?.author),
|
||||
tags = listOf("bounty-added-reward"),
|
||||
wantsToMarkAsSensitive = false,
|
||||
replyingTo = null,
|
||||
root = null,
|
||||
directMentions = setOf(),
|
||||
forkedFrom = null,
|
||||
draftTag = null,
|
||||
)
|
||||
|
||||
nextAmount = TextFieldValue("")
|
||||
nextAmount = TextFieldValue("")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -236,10 +240,8 @@ fun AddBountyAmountDialog(
|
|||
|
||||
PostButton(
|
||||
onPost = {
|
||||
scope.launch(Dispatchers.IO) {
|
||||
postViewModel.sendPost()
|
||||
onClose()
|
||||
}
|
||||
postViewModel.sendPost()
|
||||
onClose()
|
||||
},
|
||||
isActive = postViewModel.hasChanged(),
|
||||
)
|
||||
|
|
|
@ -45,6 +45,7 @@ import androidx.core.content.ContextCompat
|
|||
import com.vitorpamplona.amethyst.R
|
||||
import com.vitorpamplona.amethyst.model.Note
|
||||
import com.vitorpamplona.amethyst.ui.actions.EditPostView
|
||||
import com.vitorpamplona.amethyst.ui.actions.NewPostView
|
||||
import com.vitorpamplona.amethyst.ui.components.GenericLoadable
|
||||
import com.vitorpamplona.amethyst.ui.note.VerticalDotsIcon
|
||||
import com.vitorpamplona.amethyst.ui.note.externalLinkForNote
|
||||
|
@ -122,6 +123,11 @@ fun NoteDropDownMenu(
|
|||
mutableStateOf(false)
|
||||
}
|
||||
|
||||
val wantsToEditDraft =
|
||||
remember {
|
||||
mutableStateOf(false)
|
||||
}
|
||||
|
||||
if (wantsToEditPost.value) {
|
||||
// avoids changing while drafting a note and a new event shows up.
|
||||
val versionLookingAt =
|
||||
|
@ -141,6 +147,18 @@ fun NoteDropDownMenu(
|
|||
)
|
||||
}
|
||||
|
||||
if (wantsToEditDraft.value) {
|
||||
NewPostView(
|
||||
onClose = {
|
||||
popupExpanded.value = false
|
||||
wantsToEditDraft.value = false
|
||||
},
|
||||
accountViewModel = accountViewModel,
|
||||
draft = note,
|
||||
nav = nav,
|
||||
)
|
||||
}
|
||||
|
||||
DropdownMenu(
|
||||
expanded = popupExpanded.value,
|
||||
onDismissRequest = onDismiss,
|
||||
|
@ -219,7 +237,15 @@ fun NoteDropDownMenu(
|
|||
},
|
||||
)
|
||||
HorizontalDivider(thickness = DividerThickness)
|
||||
if (note.event is TextNoteEvent) {
|
||||
if (state.isLoggedUser && note.isDraft()) {
|
||||
DropdownMenuItem(
|
||||
text = { Text(stringResource(R.string.edit_draft)) },
|
||||
onClick = {
|
||||
wantsToEditDraft.value = true
|
||||
},
|
||||
)
|
||||
}
|
||||
if (note.event is TextNoteEvent && !note.isDraft()) {
|
||||
if (state.isLoggedUser) {
|
||||
DropdownMenuItem(
|
||||
text = { Text(stringResource(R.string.edit_post)) },
|
||||
|
|
|
@ -0,0 +1,85 @@
|
|||
/**
|
||||
* Copyright (c) 2024 Vitor Pamplona
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
* this software and associated documentation files (the "Software"), to deal in
|
||||
* the Software without restriction, including without limitation the rights to use,
|
||||
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
|
||||
* Software, and to permit persons to whom the Software is furnished to do so,
|
||||
* subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
|
||||
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
package com.vitorpamplona.amethyst.ui.note.types
|
||||
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.MutableState
|
||||
import androidx.compose.runtime.State
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.vitorpamplona.amethyst.model.Note
|
||||
import com.vitorpamplona.amethyst.ui.components.GenericLoadable
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.ChannelHeader
|
||||
import com.vitorpamplona.amethyst.ui.theme.StdVertSpacer
|
||||
import com.vitorpamplona.amethyst.ui.theme.replyModifier
|
||||
import com.vitorpamplona.quartz.events.ChannelMessageEvent
|
||||
|
||||
@Composable
|
||||
fun RenderChannelMessage(
|
||||
note: Note,
|
||||
makeItShort: Boolean,
|
||||
canPreview: Boolean,
|
||||
quotesLeft: Int,
|
||||
backgroundColor: MutableState<Color>,
|
||||
editState: State<GenericLoadable<EditState>>,
|
||||
accountViewModel: AccountViewModel,
|
||||
nav: (String) -> Unit,
|
||||
) {
|
||||
val noteEvent = note.event
|
||||
val showChannelInfo =
|
||||
remember(noteEvent) {
|
||||
if (noteEvent is ChannelMessageEvent) {
|
||||
noteEvent.channel()
|
||||
} else {
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
showChannelInfo?.let {
|
||||
ChannelHeader(
|
||||
channelHex = it,
|
||||
showVideo = false,
|
||||
sendToChannel = true,
|
||||
modifier = MaterialTheme.colorScheme.replyModifier.padding(10.dp),
|
||||
accountViewModel = accountViewModel,
|
||||
nav = nav,
|
||||
)
|
||||
Spacer(modifier = StdVertSpacer)
|
||||
}
|
||||
|
||||
RenderTextEvent(
|
||||
note,
|
||||
makeItShort,
|
||||
canPreview,
|
||||
quotesLeft,
|
||||
unPackReply = false,
|
||||
backgroundColor,
|
||||
editState,
|
||||
accountViewModel,
|
||||
nav,
|
||||
)
|
||||
}
|
|
@ -0,0 +1,83 @@
|
|||
/**
|
||||
* Copyright (c) 2024 Vitor Pamplona
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
* this software and associated documentation files (the "Software"), to deal in
|
||||
* the Software without restriction, including without limitation the rights to use,
|
||||
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
|
||||
* Software, and to permit persons to whom the Software is furnished to do so,
|
||||
* subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
|
||||
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
package com.vitorpamplona.amethyst.ui.note.types
|
||||
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.MutableState
|
||||
import androidx.compose.runtime.State
|
||||
import androidx.compose.runtime.derivedStateOf
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.vitorpamplona.amethyst.model.Note
|
||||
import com.vitorpamplona.amethyst.ui.components.GenericLoadable
|
||||
import com.vitorpamplona.amethyst.ui.navigation.routeFor
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.ChatroomHeader
|
||||
import com.vitorpamplona.amethyst.ui.theme.StdVertSpacer
|
||||
import com.vitorpamplona.amethyst.ui.theme.replyModifier
|
||||
import com.vitorpamplona.quartz.events.ChatroomKeyable
|
||||
|
||||
@Composable
|
||||
fun RenderChatMessage(
|
||||
note: Note,
|
||||
makeItShort: Boolean,
|
||||
canPreview: Boolean,
|
||||
quotesLeft: Int,
|
||||
backgroundColor: MutableState<Color>,
|
||||
editState: State<GenericLoadable<EditState>>,
|
||||
accountViewModel: AccountViewModel,
|
||||
nav: (String) -> Unit,
|
||||
) {
|
||||
val userRoom by
|
||||
remember(note) {
|
||||
derivedStateOf {
|
||||
(note.event as? ChatroomKeyable)?.chatroomKey(accountViewModel.userProfile().pubkeyHex)
|
||||
}
|
||||
}
|
||||
|
||||
userRoom?.let {
|
||||
if (it.users.size > 1 || (it.users.size == 1 && note.author == accountViewModel.account.userProfile())) {
|
||||
ChatroomHeader(it, MaterialTheme.colorScheme.replyModifier.padding(10.dp), accountViewModel) {
|
||||
routeFor(note, accountViewModel.userProfile())?.let {
|
||||
nav(it)
|
||||
}
|
||||
}
|
||||
Spacer(modifier = StdVertSpacer)
|
||||
}
|
||||
}
|
||||
|
||||
RenderTextEvent(
|
||||
note,
|
||||
makeItShort,
|
||||
canPreview,
|
||||
quotesLeft,
|
||||
unPackReply = false,
|
||||
backgroundColor,
|
||||
editState,
|
||||
accountViewModel,
|
||||
nav,
|
||||
)
|
||||
}
|
|
@ -67,6 +67,7 @@ fun FileHeaderDisplay(
|
|||
url = fullUrl,
|
||||
description = description,
|
||||
hash = hash,
|
||||
blurhash = blurHash,
|
||||
dim = dimensions,
|
||||
uri = uri,
|
||||
authorName = note.author?.toBestDisplayName(),
|
||||
|
|
|
@ -0,0 +1,85 @@
|
|||
/**
|
||||
* Copyright (c) 2024 Vitor Pamplona
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
* this software and associated documentation files (the "Software"), to deal in
|
||||
* the Software without restriction, including without limitation the rights to use,
|
||||
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
|
||||
* Software, and to permit persons to whom the Software is furnished to do so,
|
||||
* subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
|
||||
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
package com.vitorpamplona.amethyst.ui.note.types
|
||||
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.MutableState
|
||||
import androidx.compose.runtime.State
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.vitorpamplona.amethyst.model.Note
|
||||
import com.vitorpamplona.amethyst.ui.components.GenericLoadable
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.ChannelHeader
|
||||
import com.vitorpamplona.amethyst.ui.theme.StdVertSpacer
|
||||
import com.vitorpamplona.amethyst.ui.theme.replyModifier
|
||||
import com.vitorpamplona.quartz.events.LiveActivitiesChatMessageEvent
|
||||
|
||||
@Composable
|
||||
fun RenderLiveActivityChatMessage(
|
||||
note: Note,
|
||||
makeItShort: Boolean,
|
||||
canPreview: Boolean,
|
||||
quotesLeft: Int,
|
||||
backgroundColor: MutableState<Color>,
|
||||
editState: State<GenericLoadable<EditState>>,
|
||||
accountViewModel: AccountViewModel,
|
||||
nav: (String) -> Unit,
|
||||
) {
|
||||
val noteEvent = note.event
|
||||
val showChannelInfo =
|
||||
remember(noteEvent) {
|
||||
if (noteEvent is LiveActivitiesChatMessageEvent) {
|
||||
noteEvent.activity()?.toTag()
|
||||
} else {
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
showChannelInfo?.let {
|
||||
ChannelHeader(
|
||||
channelHex = it,
|
||||
showVideo = false,
|
||||
sendToChannel = true,
|
||||
modifier = MaterialTheme.colorScheme.replyModifier.padding(10.dp),
|
||||
accountViewModel = accountViewModel,
|
||||
nav = nav,
|
||||
)
|
||||
Spacer(modifier = StdVertSpacer)
|
||||
}
|
||||
|
||||
RenderTextEvent(
|
||||
note,
|
||||
makeItShort,
|
||||
canPreview,
|
||||
quotesLeft,
|
||||
unPackReply = false,
|
||||
backgroundColor,
|
||||
editState,
|
||||
accountViewModel,
|
||||
nav,
|
||||
)
|
||||
}
|
|
@ -20,11 +20,14 @@
|
|||
*/
|
||||
package com.vitorpamplona.amethyst.ui.note.types
|
||||
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.MutableState
|
||||
import androidx.compose.runtime.derivedStateOf
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
|
@ -33,9 +36,13 @@ import com.vitorpamplona.amethyst.model.Note
|
|||
import com.vitorpamplona.amethyst.ui.components.SensitivityWarning
|
||||
import com.vitorpamplona.amethyst.ui.components.TranslatableRichTextViewer
|
||||
import com.vitorpamplona.amethyst.ui.note.PollNote
|
||||
import com.vitorpamplona.amethyst.ui.note.ReplyNoteComposition
|
||||
import com.vitorpamplona.amethyst.ui.note.elements.DisplayUncitedHashtags
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
||||
import com.vitorpamplona.amethyst.ui.theme.StdVertSpacer
|
||||
import com.vitorpamplona.amethyst.ui.theme.placeholderText
|
||||
import com.vitorpamplona.quartz.events.BaseTextNoteEvent
|
||||
import com.vitorpamplona.quartz.events.CommunityDefinitionEvent
|
||||
import com.vitorpamplona.quartz.events.EmptyTagList
|
||||
import com.vitorpamplona.quartz.events.PollNoteEvent
|
||||
import com.vitorpamplona.quartz.events.toImmutableListOfLists
|
||||
|
@ -47,6 +54,7 @@ fun RenderPoll(
|
|||
makeItShort: Boolean,
|
||||
canPreview: Boolean,
|
||||
quotesLeft: Int,
|
||||
unPackReply: Boolean,
|
||||
backgroundColor: MutableState<Color>,
|
||||
accountViewModel: AccountViewModel,
|
||||
nav: (String) -> Unit,
|
||||
|
@ -54,6 +62,38 @@ fun RenderPoll(
|
|||
val noteEvent = note.event as? PollNoteEvent ?: return
|
||||
val eventContent = noteEvent.content()
|
||||
|
||||
val showReply by
|
||||
remember(note) {
|
||||
derivedStateOf {
|
||||
noteEvent is BaseTextNoteEvent && !makeItShort && unPackReply && (note.replyTo != null || noteEvent.hasAnyTaggedUser())
|
||||
}
|
||||
}
|
||||
|
||||
if (showReply) {
|
||||
val replyingDirectlyTo =
|
||||
remember(note) {
|
||||
if (noteEvent is BaseTextNoteEvent) {
|
||||
val replyingTo = noteEvent.replyingToAddressOrEvent()
|
||||
if (replyingTo != null) {
|
||||
val newNote = accountViewModel.getNoteIfExists(replyingTo)
|
||||
if (newNote != null && newNote.channelHex() == null && newNote.event?.kind() != CommunityDefinitionEvent.KIND) {
|
||||
newNote
|
||||
} else {
|
||||
note.replyTo?.lastOrNull { it.event?.kind() != CommunityDefinitionEvent.KIND }
|
||||
}
|
||||
} else {
|
||||
note.replyTo?.lastOrNull { it.event?.kind() != CommunityDefinitionEvent.KIND }
|
||||
}
|
||||
} else {
|
||||
note.replyTo?.lastOrNull { it.event?.kind() != CommunityDefinitionEvent.KIND }
|
||||
}
|
||||
}
|
||||
if (replyingDirectlyTo != null) {
|
||||
ReplyNoteComposition(replyingDirectlyTo, backgroundColor, accountViewModel, nav)
|
||||
Spacer(modifier = StdVertSpacer)
|
||||
}
|
||||
}
|
||||
|
||||
if (makeItShort && accountViewModel.isLoggedUser(note.author)) {
|
||||
Text(
|
||||
text = eventContent,
|
||||
|
|
|
@ -20,25 +20,35 @@
|
|||
*/
|
||||
package com.vitorpamplona.amethyst.ui.note.types
|
||||
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.MutableState
|
||||
import androidx.compose.runtime.derivedStateOf
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.vitorpamplona.amethyst.R
|
||||
import com.vitorpamplona.amethyst.model.Note
|
||||
import com.vitorpamplona.amethyst.ui.components.SensitivityWarning
|
||||
import com.vitorpamplona.amethyst.ui.components.TranslatableRichTextViewer
|
||||
import com.vitorpamplona.amethyst.ui.navigation.routeFor
|
||||
import com.vitorpamplona.amethyst.ui.note.LoadDecryptedContent
|
||||
import com.vitorpamplona.amethyst.ui.note.elements.DisplayUncitedHashtags
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.ChatroomHeader
|
||||
import com.vitorpamplona.amethyst.ui.theme.StdVertSpacer
|
||||
import com.vitorpamplona.amethyst.ui.theme.placeholderText
|
||||
import com.vitorpamplona.amethyst.ui.theme.replyModifier
|
||||
import com.vitorpamplona.quartz.encoders.toNpub
|
||||
import com.vitorpamplona.quartz.events.ChatroomKeyable
|
||||
import com.vitorpamplona.quartz.events.EmptyTagList
|
||||
import com.vitorpamplona.quartz.events.PrivateDmEvent
|
||||
import com.vitorpamplona.quartz.events.toImmutableListOfLists
|
||||
|
@ -57,6 +67,24 @@ fun RenderPrivateMessage(
|
|||
) {
|
||||
val noteEvent = note.event as? PrivateDmEvent ?: return
|
||||
|
||||
val userRoom by
|
||||
remember(note) {
|
||||
derivedStateOf {
|
||||
(note.event as? ChatroomKeyable)?.chatroomKey(accountViewModel.userProfile().pubkeyHex)
|
||||
}
|
||||
}
|
||||
|
||||
userRoom?.let {
|
||||
if (it.users.size > 1 || (it.users.size == 1 && note.author == accountViewModel.account.userProfile())) {
|
||||
ChatroomHeader(it, MaterialTheme.colorScheme.replyModifier.padding(10.dp), accountViewModel) {
|
||||
routeFor(note, accountViewModel.userProfile())?.let {
|
||||
nav(it)
|
||||
}
|
||||
}
|
||||
Spacer(modifier = StdVertSpacer)
|
||||
}
|
||||
}
|
||||
|
||||
val withMe = remember { noteEvent.with(accountViewModel.userProfile().pubkeyHex) }
|
||||
if (withMe) {
|
||||
LoadDecryptedContent(note, accountViewModel) { eventContent ->
|
||||
|
|
|
@ -20,6 +20,7 @@
|
|||
*/
|
||||
package com.vitorpamplona.amethyst.ui.note.types
|
||||
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
|
@ -37,9 +38,13 @@ import com.vitorpamplona.amethyst.ui.components.GenericLoadable
|
|||
import com.vitorpamplona.amethyst.ui.components.SensitivityWarning
|
||||
import com.vitorpamplona.amethyst.ui.components.TranslatableRichTextViewer
|
||||
import com.vitorpamplona.amethyst.ui.note.LoadDecryptedContent
|
||||
import com.vitorpamplona.amethyst.ui.note.ReplyNoteComposition
|
||||
import com.vitorpamplona.amethyst.ui.note.elements.DisplayUncitedHashtags
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
||||
import com.vitorpamplona.amethyst.ui.theme.StdVertSpacer
|
||||
import com.vitorpamplona.amethyst.ui.theme.placeholderText
|
||||
import com.vitorpamplona.quartz.events.BaseTextNoteEvent
|
||||
import com.vitorpamplona.quartz.events.CommunityDefinitionEvent
|
||||
import com.vitorpamplona.quartz.events.EmptyTagList
|
||||
import com.vitorpamplona.quartz.events.TextNoteEvent
|
||||
import com.vitorpamplona.quartz.events.toImmutableListOfLists
|
||||
|
@ -52,11 +57,46 @@ fun RenderTextEvent(
|
|||
makeItShort: Boolean,
|
||||
canPreview: Boolean,
|
||||
quotesLeft: Int,
|
||||
unPackReply: Boolean,
|
||||
backgroundColor: MutableState<Color>,
|
||||
editState: State<GenericLoadable<EditState>>,
|
||||
accountViewModel: AccountViewModel,
|
||||
nav: (String) -> Unit,
|
||||
) {
|
||||
val noteEvent = note.event
|
||||
|
||||
val showReply by
|
||||
remember(note) {
|
||||
derivedStateOf {
|
||||
noteEvent is BaseTextNoteEvent && !makeItShort && unPackReply && (note.replyTo != null || noteEvent.hasAnyTaggedUser())
|
||||
}
|
||||
}
|
||||
|
||||
if (showReply) {
|
||||
val replyingDirectlyTo =
|
||||
remember(note) {
|
||||
if (noteEvent is BaseTextNoteEvent) {
|
||||
val replyingTo = noteEvent.replyingToAddressOrEvent()
|
||||
if (replyingTo != null) {
|
||||
val newNote = accountViewModel.getNoteIfExists(replyingTo)
|
||||
if (newNote != null && newNote.channelHex() == null && newNote.event?.kind() != CommunityDefinitionEvent.KIND) {
|
||||
newNote
|
||||
} else {
|
||||
note.replyTo?.lastOrNull { it.event?.kind() != CommunityDefinitionEvent.KIND }
|
||||
}
|
||||
} else {
|
||||
note.replyTo?.lastOrNull { it.event?.kind() != CommunityDefinitionEvent.KIND }
|
||||
}
|
||||
} else {
|
||||
note.replyTo?.lastOrNull { it.event?.kind() != CommunityDefinitionEvent.KIND }
|
||||
}
|
||||
}
|
||||
if (replyingDirectlyTo != null && unPackReply) {
|
||||
ReplyNoteComposition(replyingDirectlyTo, backgroundColor, accountViewModel, nav)
|
||||
Spacer(modifier = StdVertSpacer)
|
||||
}
|
||||
}
|
||||
|
||||
LoadDecryptedContent(
|
||||
note,
|
||||
accountViewModel,
|
||||
|
|
|
@ -43,6 +43,7 @@ import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
|||
import com.vitorpamplona.amethyst.ui.theme.FeedPadding
|
||||
import com.vitorpamplona.amethyst.ui.theme.Font14SP
|
||||
import com.vitorpamplona.amethyst.ui.theme.HalfPadding
|
||||
import com.vitorpamplona.quartz.events.DraftEvent
|
||||
|
||||
@Composable
|
||||
fun RefreshingChatroomFeedView(
|
||||
|
@ -51,6 +52,8 @@ fun RefreshingChatroomFeedView(
|
|||
nav: (String) -> Unit,
|
||||
routeForLastRead: String,
|
||||
onWantsToReply: (Note) -> Unit,
|
||||
onWantsToEditDraft: (Note) -> Unit,
|
||||
avoidDraft: String? = null,
|
||||
scrollStateKey: String? = null,
|
||||
enablePullRefresh: Boolean = true,
|
||||
) {
|
||||
|
@ -63,6 +66,8 @@ fun RefreshingChatroomFeedView(
|
|||
nav,
|
||||
routeForLastRead,
|
||||
onWantsToReply,
|
||||
onWantsToEditDraft,
|
||||
avoidDraft,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
@ -76,6 +81,8 @@ fun RenderChatroomFeedView(
|
|||
nav: (String) -> Unit,
|
||||
routeForLastRead: String,
|
||||
onWantsToReply: (Note) -> Unit,
|
||||
onWantsToEditDraft: (Note) -> Unit,
|
||||
avoidDraft: String? = null,
|
||||
) {
|
||||
val feedState by viewModel.feedContent.collectAsStateWithLifecycle()
|
||||
|
||||
|
@ -95,6 +102,8 @@ fun RenderChatroomFeedView(
|
|||
nav,
|
||||
routeForLastRead,
|
||||
onWantsToReply,
|
||||
onWantsToEditDraft,
|
||||
avoidDraft,
|
||||
)
|
||||
}
|
||||
is FeedState.Loading -> {
|
||||
|
@ -112,6 +121,8 @@ fun ChatroomFeedLoaded(
|
|||
nav: (String) -> Unit,
|
||||
routeForLastRead: String,
|
||||
onWantsToReply: (Note) -> Unit,
|
||||
onWantsToEditDraft: (Note) -> Unit,
|
||||
avoidDraft: String? = null,
|
||||
) {
|
||||
LaunchedEffect(state.feed.value.firstOrNull()) {
|
||||
if (listState.firstVisibleItemIndex <= 1) {
|
||||
|
@ -126,13 +137,17 @@ fun ChatroomFeedLoaded(
|
|||
state = listState,
|
||||
) {
|
||||
itemsIndexed(state.feed.value, key = { _, item -> item.idHex }) { _, item ->
|
||||
ChatroomMessageCompose(
|
||||
baseNote = item,
|
||||
routeForLastRead = routeForLastRead,
|
||||
accountViewModel = accountViewModel,
|
||||
nav = nav,
|
||||
onWantsToReply = onWantsToReply,
|
||||
)
|
||||
val noteEvent = item.event
|
||||
if (avoidDraft == null || noteEvent !is DraftEvent || noteEvent.dTag() != avoidDraft) {
|
||||
ChatroomMessageCompose(
|
||||
baseNote = item,
|
||||
routeForLastRead = routeForLastRead,
|
||||
accountViewModel = accountViewModel,
|
||||
nav = nav,
|
||||
onWantsToReply = onWantsToReply,
|
||||
onWantsToEditDraft = onWantsToEditDraft,
|
||||
)
|
||||
}
|
||||
NewSubject(item)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -39,7 +39,6 @@ import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
|||
import com.vitorpamplona.amethyst.ui.theme.DividerThickness
|
||||
import com.vitorpamplona.amethyst.ui.theme.FeedPadding
|
||||
import com.vitorpamplona.amethyst.ui.theme.StdTopPadding
|
||||
import kotlin.time.ExperimentalTime
|
||||
|
||||
@Composable
|
||||
fun ChatroomListFeedView(
|
||||
|
@ -81,7 +80,6 @@ private fun CrossFadeState(
|
|||
}
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalTime::class)
|
||||
@Composable
|
||||
private fun FeedLoaded(
|
||||
state: FeedState.Loaded,
|
||||
|
|
|
@ -47,6 +47,7 @@ import com.vitorpamplona.amethyst.ui.dal.DiscoverChatFeedFilter
|
|||
import com.vitorpamplona.amethyst.ui.dal.DiscoverCommunityFeedFilter
|
||||
import com.vitorpamplona.amethyst.ui.dal.DiscoverLiveFeedFilter
|
||||
import com.vitorpamplona.amethyst.ui.dal.DiscoverMarketplaceFeedFilter
|
||||
import com.vitorpamplona.amethyst.ui.dal.DraftEventsFeedFilter
|
||||
import com.vitorpamplona.amethyst.ui.dal.FeedFilter
|
||||
import com.vitorpamplona.amethyst.ui.dal.GeoHashFeedFilter
|
||||
import com.vitorpamplona.amethyst.ui.dal.HashtagFeedFilter
|
||||
|
@ -60,6 +61,7 @@ import com.vitorpamplona.amethyst.ui.dal.UserProfileNewThreadFeedFilter
|
|||
import com.vitorpamplona.amethyst.ui.dal.UserProfileReportsFeedFilter
|
||||
import com.vitorpamplona.amethyst.ui.dal.VideoFeedFilter
|
||||
import com.vitorpamplona.quartz.events.ChatroomKey
|
||||
import com.vitorpamplona.quartz.events.DeletionEvent
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
import kotlinx.collections.immutable.toImmutableList
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
|
@ -267,6 +269,16 @@ class NostrBookmarkPrivateFeedViewModel(val account: Account) :
|
|||
}
|
||||
}
|
||||
|
||||
@Stable
|
||||
class NostrDraftEventsFeedViewModel(val account: Account) :
|
||||
FeedViewModel(DraftEventsFeedFilter(account)) {
|
||||
class Factory(val account: Account) : ViewModelProvider.Factory {
|
||||
override fun <NostrDraftEventsFeedViewModel : ViewModel> create(modelClass: Class<NostrDraftEventsFeedViewModel>): NostrDraftEventsFeedViewModel {
|
||||
return NostrDraftEventsFeedViewModel(account) as NostrDraftEventsFeedViewModel
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class NostrUserAppRecommendationsFeedViewModel(val user: User) :
|
||||
FeedViewModel(UserProfileAppRecommendationsFeedFilter(user)) {
|
||||
class Factory(val user: User) : ViewModelProvider.Factory {
|
||||
|
@ -344,9 +356,24 @@ abstract class FeedViewModel(val localFilter: FeedFilter<Note>) :
|
|||
val oldNotesState = _feedContent.value
|
||||
if (localFilter is AdditiveFeedFilter && lastFeedKey == localFilter.feedKey()) {
|
||||
if (oldNotesState is FeedState.Loaded) {
|
||||
val deletionEvents: List<DeletionEvent> =
|
||||
newItems.mapNotNull {
|
||||
val noteEvent = it.event
|
||||
if (noteEvent is DeletionEvent) noteEvent else null
|
||||
}
|
||||
|
||||
val oldList =
|
||||
if (deletionEvents.isEmpty()) {
|
||||
oldNotesState.feed.value
|
||||
} else {
|
||||
val deletedEventIds = deletionEvents.flatMapTo(HashSet()) { it.deleteEvents() }
|
||||
val deletedEventAddresses = deletionEvents.flatMapTo(HashSet()) { it.deleteAddresses() }
|
||||
oldNotesState.feed.value.filter { !it.wasOrShouldBeDeletedBy(deletedEventIds, deletedEventAddresses) }.toImmutableList()
|
||||
}
|
||||
|
||||
val newList =
|
||||
localFilter
|
||||
.updateListWith(oldNotesState.feed.value, newItems)
|
||||
.updateListWith(oldList, newItems)
|
||||
.distinctBy { it.idHex }
|
||||
.toImmutableList()
|
||||
if (!equalImmutableLists(newList, oldNotesState.feed.value)) {
|
||||
|
|
|
@ -86,6 +86,7 @@ import com.vitorpamplona.amethyst.ui.components.ObserveDisplayNip05Status
|
|||
import com.vitorpamplona.amethyst.ui.components.mockAccountViewModel
|
||||
import com.vitorpamplona.amethyst.ui.navigation.routeToMessage
|
||||
import com.vitorpamplona.amethyst.ui.note.BlankNote
|
||||
import com.vitorpamplona.amethyst.ui.note.DisplayDraft
|
||||
import com.vitorpamplona.amethyst.ui.note.DisplayOtsIfInOriginal
|
||||
import com.vitorpamplona.amethyst.ui.note.HiddenNote
|
||||
import com.vitorpamplona.amethyst.ui.note.LoadAddressableNote
|
||||
|
@ -94,6 +95,7 @@ import com.vitorpamplona.amethyst.ui.note.NoteCompose
|
|||
import com.vitorpamplona.amethyst.ui.note.NoteQuickActionMenu
|
||||
import com.vitorpamplona.amethyst.ui.note.NoteUsernameDisplay
|
||||
import com.vitorpamplona.amethyst.ui.note.ReactionsRow
|
||||
import com.vitorpamplona.amethyst.ui.note.RenderDraft
|
||||
import com.vitorpamplona.amethyst.ui.note.RenderRepost
|
||||
import com.vitorpamplona.amethyst.ui.note.elements.DefaultImageHeader
|
||||
import com.vitorpamplona.amethyst.ui.note.elements.DisplayEditStatus
|
||||
|
@ -119,14 +121,17 @@ import com.vitorpamplona.amethyst.ui.note.types.EditState
|
|||
import com.vitorpamplona.amethyst.ui.note.types.FileHeaderDisplay
|
||||
import com.vitorpamplona.amethyst.ui.note.types.FileStorageHeaderDisplay
|
||||
import com.vitorpamplona.amethyst.ui.note.types.RenderAppDefinition
|
||||
import com.vitorpamplona.amethyst.ui.note.types.RenderChannelMessage
|
||||
import com.vitorpamplona.amethyst.ui.note.types.RenderEmojiPack
|
||||
import com.vitorpamplona.amethyst.ui.note.types.RenderFhirResource
|
||||
import com.vitorpamplona.amethyst.ui.note.types.RenderGitIssueEvent
|
||||
import com.vitorpamplona.amethyst.ui.note.types.RenderGitPatchEvent
|
||||
import com.vitorpamplona.amethyst.ui.note.types.RenderGitRepositoryEvent
|
||||
import com.vitorpamplona.amethyst.ui.note.types.RenderLiveActivityChatMessage
|
||||
import com.vitorpamplona.amethyst.ui.note.types.RenderPinListEvent
|
||||
import com.vitorpamplona.amethyst.ui.note.types.RenderPoll
|
||||
import com.vitorpamplona.amethyst.ui.note.types.RenderPostApproval
|
||||
import com.vitorpamplona.amethyst.ui.note.types.RenderPrivateMessage
|
||||
import com.vitorpamplona.amethyst.ui.note.types.RenderTextEvent
|
||||
import com.vitorpamplona.amethyst.ui.note.types.RenderTextModificationEvent
|
||||
import com.vitorpamplona.amethyst.ui.note.types.VideoDisplay
|
||||
|
@ -150,10 +155,12 @@ import com.vitorpamplona.quartz.events.AudioHeaderEvent
|
|||
import com.vitorpamplona.quartz.events.AudioTrackEvent
|
||||
import com.vitorpamplona.quartz.events.BadgeDefinitionEvent
|
||||
import com.vitorpamplona.quartz.events.ChannelCreateEvent
|
||||
import com.vitorpamplona.quartz.events.ChannelMessageEvent
|
||||
import com.vitorpamplona.quartz.events.ChannelMetadataEvent
|
||||
import com.vitorpamplona.quartz.events.ClassifiedsEvent
|
||||
import com.vitorpamplona.quartz.events.CommunityDefinitionEvent
|
||||
import com.vitorpamplona.quartz.events.CommunityPostApprovalEvent
|
||||
import com.vitorpamplona.quartz.events.DraftEvent
|
||||
import com.vitorpamplona.quartz.events.EmojiPackEvent
|
||||
import com.vitorpamplona.quartz.events.Event
|
||||
import com.vitorpamplona.quartz.events.FhirResourceEvent
|
||||
|
@ -164,10 +171,12 @@ import com.vitorpamplona.quartz.events.GitIssueEvent
|
|||
import com.vitorpamplona.quartz.events.GitPatchEvent
|
||||
import com.vitorpamplona.quartz.events.GitRepositoryEvent
|
||||
import com.vitorpamplona.quartz.events.HighlightEvent
|
||||
import com.vitorpamplona.quartz.events.LiveActivitiesChatMessageEvent
|
||||
import com.vitorpamplona.quartz.events.LongTextNoteEvent
|
||||
import com.vitorpamplona.quartz.events.PeopleListEvent
|
||||
import com.vitorpamplona.quartz.events.PinListEvent
|
||||
import com.vitorpamplona.quartz.events.PollNoteEvent
|
||||
import com.vitorpamplona.quartz.events.PrivateDmEvent
|
||||
import com.vitorpamplona.quartz.events.RelaySetEvent
|
||||
import com.vitorpamplona.quartz.events.RepostEvent
|
||||
import com.vitorpamplona.quartz.events.TextNoteModificationEvent
|
||||
|
@ -446,6 +455,10 @@ fun NoteMaster(
|
|||
DisplayPoW(pow)
|
||||
}
|
||||
|
||||
if (note.isDraft()) {
|
||||
DisplayDraft()
|
||||
}
|
||||
|
||||
DisplayOtsIfInOriginal(note, editState, accountViewModel)
|
||||
}
|
||||
}
|
||||
|
@ -473,6 +486,11 @@ fun NoteMaster(
|
|||
),
|
||||
) {
|
||||
Column {
|
||||
val canPreview =
|
||||
note.author == account.userProfile() ||
|
||||
(note.author?.let { account.userProfile().isFollowingCached(it) } ?: true) ||
|
||||
!noteForReports.hasAnyReports()
|
||||
|
||||
if (
|
||||
(noteEvent is ChannelCreateEvent || noteEvent is ChannelMetadataEvent) &&
|
||||
note.channelHex() != null
|
||||
|
@ -535,6 +553,8 @@ fun NoteMaster(
|
|||
RenderGitIssueEvent(baseNote, false, true, quotesLeft = 3, backgroundColor, accountViewModel, nav)
|
||||
} else if (noteEvent is AppDefinitionEvent) {
|
||||
RenderAppDefinition(baseNote, accountViewModel, nav)
|
||||
} else if (noteEvent is DraftEvent) {
|
||||
RenderDraft(baseNote, backgroundColor, accountViewModel, nav)
|
||||
} else if (noteEvent is HighlightEvent) {
|
||||
DisplayHighlight(
|
||||
noteEvent.quote(),
|
||||
|
@ -561,31 +581,55 @@ fun NoteMaster(
|
|||
nav,
|
||||
)
|
||||
} else if (noteEvent is PollNoteEvent) {
|
||||
val canPreview =
|
||||
note.author == account.userProfile() ||
|
||||
(note.author?.let { account.userProfile().isFollowingCached(it) } ?: true) ||
|
||||
!noteForReports.hasAnyReports()
|
||||
|
||||
RenderPoll(
|
||||
baseNote,
|
||||
false,
|
||||
canPreview,
|
||||
quotesLeft = 3,
|
||||
unPackReply = false,
|
||||
backgroundColor,
|
||||
accountViewModel,
|
||||
nav,
|
||||
)
|
||||
} else if (noteEvent is PrivateDmEvent) {
|
||||
RenderPrivateMessage(
|
||||
baseNote,
|
||||
false,
|
||||
canPreview,
|
||||
3,
|
||||
backgroundColor,
|
||||
accountViewModel,
|
||||
nav,
|
||||
)
|
||||
} else if (noteEvent is ChannelMessageEvent) {
|
||||
RenderChannelMessage(
|
||||
baseNote,
|
||||
false,
|
||||
canPreview,
|
||||
3,
|
||||
backgroundColor,
|
||||
editState,
|
||||
accountViewModel,
|
||||
nav,
|
||||
)
|
||||
} else if (noteEvent is LiveActivitiesChatMessageEvent) {
|
||||
RenderLiveActivityChatMessage(
|
||||
baseNote,
|
||||
false,
|
||||
canPreview,
|
||||
3,
|
||||
backgroundColor,
|
||||
editState,
|
||||
accountViewModel,
|
||||
nav,
|
||||
)
|
||||
} else {
|
||||
val canPreview =
|
||||
note.author == account.userProfile() ||
|
||||
(note.author?.let { account.userProfile().isFollowingCached(it) } ?: true) ||
|
||||
!noteForReports.hasAnyReports()
|
||||
|
||||
RenderTextEvent(
|
||||
baseNote,
|
||||
false,
|
||||
canPreview,
|
||||
quotesLeft = 3,
|
||||
unPackReply = false,
|
||||
backgroundColor,
|
||||
editState,
|
||||
accountViewModel,
|
||||
|
@ -605,7 +649,14 @@ fun NoteMaster(
|
|||
ReactionsRow(note, true, editState, accountViewModel, nav)
|
||||
}
|
||||
|
||||
NoteQuickActionMenu(note, popupExpanded, { popupExpanded = false }, accountViewModel)
|
||||
NoteQuickActionMenu(
|
||||
note = note,
|
||||
popupExpanded = popupExpanded,
|
||||
onDismiss = { popupExpanded = false },
|
||||
onWantsToEditDraft = { },
|
||||
accountViewModel = accountViewModel,
|
||||
nav = nav,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -860,6 +911,7 @@ private fun RenderWikiHeaderForThreadPreview() {
|
|||
false,
|
||||
true,
|
||||
quotesLeft = 3,
|
||||
unPackReply = false,
|
||||
backgroundColor,
|
||||
editState,
|
||||
accountViewModel,
|
||||
|
|
|
@ -71,8 +71,10 @@ import com.vitorpamplona.quartz.encoders.ATag
|
|||
import com.vitorpamplona.quartz.encoders.HexKey
|
||||
import com.vitorpamplona.quartz.encoders.Nip11RelayInformation
|
||||
import com.vitorpamplona.quartz.encoders.Nip19Bech32
|
||||
import com.vitorpamplona.quartz.events.AddressableEvent
|
||||
import com.vitorpamplona.quartz.events.ChatroomKey
|
||||
import com.vitorpamplona.quartz.events.ChatroomKeyable
|
||||
import com.vitorpamplona.quartz.events.DraftEvent
|
||||
import com.vitorpamplona.quartz.events.Event
|
||||
import com.vitorpamplona.quartz.events.EventInterface
|
||||
import com.vitorpamplona.quartz.events.GiftWrapEvent
|
||||
|
@ -573,6 +575,10 @@ class AccountViewModel(val account: Account, val settings: SettingsState) : View
|
|||
return account.cachedDecryptContent(note)
|
||||
}
|
||||
|
||||
fun cachedDecrypt(event: EventInterface?): String? {
|
||||
return account.cachedDecryptContent(event)
|
||||
}
|
||||
|
||||
fun decrypt(
|
||||
note: Note,
|
||||
onReady: (String) -> Unit,
|
||||
|
@ -1209,6 +1215,14 @@ class AccountViewModel(val account: Account, val settings: SettingsState) : View
|
|||
baseNote: Note,
|
||||
onMore: () -> Unit,
|
||||
) {
|
||||
if (baseNote.isDraft()) {
|
||||
toast(
|
||||
R.string.draft_note,
|
||||
R.string.it_s_not_possible_to_quote_to_a_draft_note,
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
if (isWriteable()) {
|
||||
if (hasBoosted(baseNote)) {
|
||||
deleteBoostsTo(baseNote)
|
||||
|
@ -1304,6 +1318,43 @@ class AccountViewModel(val account: Account, val settings: SettingsState) : View
|
|||
}
|
||||
}
|
||||
|
||||
suspend fun deleteDraft(draftTag: String) {
|
||||
account.deleteDraft(draftTag)
|
||||
}
|
||||
|
||||
fun createTempCachedDraftNote(
|
||||
noteEvent: DraftEvent,
|
||||
author: User,
|
||||
): Note? {
|
||||
return noteEvent.preCachedDraft(account.signer)?.let { createTempDraftNote(it, author) }
|
||||
}
|
||||
|
||||
fun createTempDraftNote(
|
||||
noteEvent: DraftEvent,
|
||||
author: User,
|
||||
onReady: (Note) -> Unit,
|
||||
) {
|
||||
viewModelScope.launch(Dispatchers.IO) {
|
||||
noteEvent.cachedDraft(account.signer) {
|
||||
onReady(createTempDraftNote(it, author))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun createTempDraftNote(
|
||||
innerEvent: Event,
|
||||
author: User,
|
||||
): Note {
|
||||
val note =
|
||||
if (innerEvent is AddressableEvent) {
|
||||
AddressableNote(innerEvent.address())
|
||||
} else {
|
||||
Note(innerEvent.id)
|
||||
}
|
||||
note.loadEvent(innerEvent, author, LocalCache.computeReplyTo(innerEvent))
|
||||
return note
|
||||
}
|
||||
|
||||
val bechLinkCache = CachedLoadedBechLink(this)
|
||||
|
||||
class CachedLoadedBechLink(val accountViewModel: AccountViewModel) : GenericBaseCache<String, LoadedBechLink>(20) {
|
||||
|
|
|
@ -64,6 +64,7 @@ import androidx.compose.runtime.Composable
|
|||
import androidx.compose.runtime.CompositionLocalProvider
|
||||
import androidx.compose.runtime.DisposableEffect
|
||||
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
|
||||
|
@ -163,12 +164,17 @@ import kotlinx.collections.immutable.ImmutableList
|
|||
import kotlinx.collections.immutable.persistentListOf
|
||||
import kotlinx.collections.immutable.toImmutableList
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.FlowPreview
|
||||
import kotlinx.coroutines.flow.collectLatest
|
||||
import kotlinx.coroutines.flow.debounce
|
||||
import kotlinx.coroutines.flow.receiveAsFlow
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import java.text.DateFormat
|
||||
import java.text.SimpleDateFormat
|
||||
import java.util.Date
|
||||
import java.util.Locale
|
||||
import java.util.UUID
|
||||
|
||||
@Composable
|
||||
fun ChannelScreen(
|
||||
|
@ -187,6 +193,7 @@ fun ChannelScreen(
|
|||
}
|
||||
}
|
||||
|
||||
@OptIn(FlowPreview::class)
|
||||
@Composable
|
||||
fun PrepareChannelViewModels(
|
||||
baseChannel: Channel,
|
||||
|
@ -289,7 +296,11 @@ fun ChannelScreen(
|
|||
accountViewModel = accountViewModel,
|
||||
nav = nav,
|
||||
routeForLastRead = "Channel/${channel.idHex}",
|
||||
avoidDraft = newPostModel.draftTag,
|
||||
onWantsToReply = { replyTo.value = it },
|
||||
onWantsToEditDraft = {
|
||||
newPostModel.load(accountViewModel, null, null, null, null, it)
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
|
@ -299,49 +310,74 @@ fun ChannelScreen(
|
|||
|
||||
val scope = rememberCoroutineScope()
|
||||
|
||||
LaunchedEffect(Unit) {
|
||||
launch(Dispatchers.IO) {
|
||||
newPostModel.draftTextChanges
|
||||
.receiveAsFlow()
|
||||
.debounce(1000)
|
||||
.collectLatest {
|
||||
innerSendPost(replyTo, channel, newPostModel, accountViewModel, newPostModel.draftTag)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// LAST ROW
|
||||
EditFieldRow(newPostModel, isPrivate = false, accountViewModel = accountViewModel) {
|
||||
scope.launch(Dispatchers.IO) {
|
||||
val tagger =
|
||||
NewMessageTagger(
|
||||
message = newPostModel.message.text,
|
||||
pTags = listOfNotNull(replyTo.value?.author),
|
||||
eTags = listOfNotNull(replyTo.value),
|
||||
channelHex = channel.idHex,
|
||||
dao = accountViewModel,
|
||||
)
|
||||
tagger.run()
|
||||
|
||||
val urls = findURLs(tagger.message)
|
||||
val usedAttachments = newPostModel.nip94attachments.filter { it.urls().intersect(urls.toSet()).isNotEmpty() }
|
||||
|
||||
if (channel is PublicChatChannel) {
|
||||
accountViewModel.account.sendChannelMessage(
|
||||
message = tagger.message,
|
||||
toChannel = channel.idHex,
|
||||
replyTo = tagger.eTags,
|
||||
mentions = tagger.pTags,
|
||||
wantsToMarkAsSensitive = false,
|
||||
nip94attachments = usedAttachments,
|
||||
)
|
||||
} else if (channel is LiveActivitiesChannel) {
|
||||
accountViewModel.account.sendLiveMessage(
|
||||
message = tagger.message,
|
||||
toChannel = channel.address,
|
||||
replyTo = tagger.eTags,
|
||||
mentions = tagger.pTags,
|
||||
wantsToMarkAsSensitive = false,
|
||||
nip94attachments = usedAttachments,
|
||||
)
|
||||
}
|
||||
innerSendPost(replyTo, channel, newPostModel, accountViewModel, null)
|
||||
newPostModel.message = TextFieldValue("")
|
||||
replyTo.value = null
|
||||
accountViewModel.deleteDraft(newPostModel.draftTag)
|
||||
newPostModel.draftTag = UUID.randomUUID().toString()
|
||||
feedViewModel.sendToTop()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun innerSendPost(
|
||||
replyTo: MutableState<Note?>,
|
||||
channel: Channel,
|
||||
newPostModel: NewPostViewModel,
|
||||
accountViewModel: AccountViewModel,
|
||||
draftTag: String?,
|
||||
) {
|
||||
val tagger =
|
||||
NewMessageTagger(
|
||||
message = newPostModel.message.text,
|
||||
pTags = listOfNotNull(replyTo.value?.author),
|
||||
eTags = listOfNotNull(replyTo.value),
|
||||
channelHex = channel.idHex,
|
||||
dao = accountViewModel,
|
||||
)
|
||||
tagger.run()
|
||||
|
||||
val urls = findURLs(tagger.message)
|
||||
val usedAttachments = newPostModel.nip94attachments.filter { it.urls().intersect(urls.toSet()).isNotEmpty() }
|
||||
|
||||
if (channel is PublicChatChannel) {
|
||||
accountViewModel.account.sendChannelMessage(
|
||||
message = tagger.message,
|
||||
toChannel = channel.idHex,
|
||||
replyTo = tagger.eTags,
|
||||
mentions = tagger.pTags,
|
||||
wantsToMarkAsSensitive = false,
|
||||
nip94attachments = usedAttachments,
|
||||
draftTag = draftTag,
|
||||
)
|
||||
} else if (channel is LiveActivitiesChannel) {
|
||||
accountViewModel.account.sendLiveMessage(
|
||||
message = tagger.message,
|
||||
toChannel = channel.address,
|
||||
replyTo = tagger.eTags,
|
||||
mentions = tagger.pTags,
|
||||
wantsToMarkAsSensitive = false,
|
||||
nip94attachments = usedAttachments,
|
||||
draftTag = draftTag,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun DisplayReplyingToNote(
|
||||
replyingNote: Note?,
|
||||
|
@ -366,6 +402,7 @@ fun DisplayReplyingToNote(
|
|||
accountViewModel = accountViewModel,
|
||||
nav = nav,
|
||||
onWantsToReply = {},
|
||||
onWantsToEditDraft = {},
|
||||
)
|
||||
}
|
||||
|
||||
|
@ -665,7 +702,12 @@ fun ShowVideoStreaming(
|
|||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.Center,
|
||||
modifier = remember { Modifier.fillMaxWidth().heightIn(min = 50.dp, max = 300.dp) },
|
||||
modifier =
|
||||
remember {
|
||||
Modifier
|
||||
.fillMaxWidth()
|
||||
.heightIn(min = 50.dp, max = 300.dp)
|
||||
},
|
||||
) {
|
||||
val zoomableUrlVideo =
|
||||
remember(streamingInfo) {
|
||||
|
|
|
@ -57,6 +57,7 @@ import androidx.compose.material3.TextFieldDefaults
|
|||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.DisposableEffect
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.MutableState
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.livedata.observeAsState
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
|
@ -93,7 +94,6 @@ import com.vitorpamplona.amethyst.ui.actions.PostButton
|
|||
import com.vitorpamplona.amethyst.ui.actions.ServerOption
|
||||
import com.vitorpamplona.amethyst.ui.actions.UploadFromGallery
|
||||
import com.vitorpamplona.amethyst.ui.actions.UrlUserTagTransformation
|
||||
import com.vitorpamplona.amethyst.ui.components.ObserveDisplayNip05Status
|
||||
import com.vitorpamplona.amethyst.ui.note.ClickableUserPicture
|
||||
import com.vitorpamplona.amethyst.ui.note.DisplayRoomSubject
|
||||
import com.vitorpamplona.amethyst.ui.note.DisplayUserSetAsSubject
|
||||
|
@ -122,8 +122,13 @@ import com.vitorpamplona.quartz.events.findURLs
|
|||
import kotlinx.collections.immutable.persistentSetOf
|
||||
import kotlinx.collections.immutable.toPersistentList
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.FlowPreview
|
||||
import kotlinx.coroutines.flow.collectLatest
|
||||
import kotlinx.coroutines.flow.debounce
|
||||
import kotlinx.coroutines.flow.receiveAsFlow
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import java.util.UUID
|
||||
|
||||
@Composable
|
||||
fun ChatroomScreen(
|
||||
|
@ -206,6 +211,7 @@ fun LoadRoomByAuthor(
|
|||
content(room)
|
||||
}
|
||||
|
||||
@OptIn(FlowPreview::class)
|
||||
@Composable
|
||||
fun PrepareChatroomViewModels(
|
||||
room: ChatroomKey,
|
||||
|
@ -315,7 +321,13 @@ fun ChatroomScreen(
|
|||
accountViewModel = accountViewModel,
|
||||
nav = nav,
|
||||
routeForLastRead = "Room/${room.hashCode()}",
|
||||
onWantsToReply = { replyTo.value = it },
|
||||
avoidDraft = newPostModel.draftTag,
|
||||
onWantsToReply = {
|
||||
replyTo.value = it
|
||||
},
|
||||
onWantsToEditDraft = {
|
||||
newPostModel.load(accountViewModel, null, null, null, null, it)
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
|
@ -325,33 +337,27 @@ fun ChatroomScreen(
|
|||
|
||||
val scope = rememberCoroutineScope()
|
||||
|
||||
LaunchedEffect(key1 = newPostModel.draftTag) {
|
||||
launch(Dispatchers.IO) {
|
||||
newPostModel.draftTextChanges
|
||||
.receiveAsFlow()
|
||||
.debounce(1000)
|
||||
.collectLatest {
|
||||
innerSendPost(newPostModel, room, replyTo, accountViewModel, newPostModel.draftTag)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// LAST ROW
|
||||
PrivateMessageEditFieldRow(newPostModel, isPrivate = true, accountViewModel) {
|
||||
scope.launch(Dispatchers.IO) {
|
||||
val urls = findURLs(newPostModel.message.text)
|
||||
val usedAttachments = newPostModel.nip94attachments.filter { it.urls().intersect(urls.toSet()).isNotEmpty() }
|
||||
innerSendPost(newPostModel, room, replyTo, accountViewModel, null)
|
||||
|
||||
if (newPostModel.nip24 || room.users.size > 1 || replyTo.value?.event is ChatMessageEvent) {
|
||||
accountViewModel.account.sendNIP24PrivateMessage(
|
||||
message = newPostModel.message.text,
|
||||
toUsers = room.users.toList(),
|
||||
replyingTo = replyTo.value,
|
||||
mentions = null,
|
||||
wantsToMarkAsSensitive = false,
|
||||
nip94attachments = usedAttachments,
|
||||
)
|
||||
} else {
|
||||
accountViewModel.account.sendPrivateMessage(
|
||||
message = newPostModel.message.text,
|
||||
toUser = room.users.first(),
|
||||
replyingTo = replyTo.value,
|
||||
mentions = null,
|
||||
wantsToMarkAsSensitive = false,
|
||||
nip94attachments = usedAttachments,
|
||||
)
|
||||
}
|
||||
accountViewModel.deleteDraft(newPostModel.draftTag)
|
||||
|
||||
newPostModel.message = TextFieldValue("")
|
||||
newPostModel.draftTag = UUID.randomUUID().toString()
|
||||
|
||||
replyTo.value = null
|
||||
feedViewModel.sendToTop()
|
||||
}
|
||||
|
@ -359,6 +365,39 @@ fun ChatroomScreen(
|
|||
}
|
||||
}
|
||||
|
||||
private fun innerSendPost(
|
||||
newPostModel: NewPostViewModel,
|
||||
room: ChatroomKey,
|
||||
replyTo: MutableState<Note?>,
|
||||
accountViewModel: AccountViewModel,
|
||||
dTag: String?,
|
||||
) {
|
||||
val urls = findURLs(newPostModel.message.text)
|
||||
val usedAttachments = newPostModel.nip94attachments.filter { it.urls().intersect(urls.toSet()).isNotEmpty() }
|
||||
|
||||
if (newPostModel.nip24 || room.users.size > 1 || replyTo.value?.event is ChatMessageEvent) {
|
||||
accountViewModel.account.sendNIP24PrivateMessage(
|
||||
message = newPostModel.message.text,
|
||||
toUsers = room.users.toList(),
|
||||
replyingTo = replyTo.value,
|
||||
mentions = null,
|
||||
wantsToMarkAsSensitive = false,
|
||||
nip94attachments = usedAttachments,
|
||||
draftTag = dTag,
|
||||
)
|
||||
} else {
|
||||
accountViewModel.account.sendPrivateMessage(
|
||||
message = newPostModel.message.text,
|
||||
toUser = room.users.first(),
|
||||
replyingTo = replyTo.value,
|
||||
mentions = null,
|
||||
wantsToMarkAsSensitive = false,
|
||||
nip94attachments = usedAttachments,
|
||||
draftTag = dTag,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun PrivateMessageEditFieldRow(
|
||||
channelScreenModel: NewPostViewModel,
|
||||
|
@ -551,7 +590,7 @@ fun ChatroomHeader(
|
|||
room: ChatroomKey,
|
||||
modifier: Modifier = StdPadding,
|
||||
accountViewModel: AccountViewModel,
|
||||
nav: (String) -> Unit,
|
||||
onClick: () -> Unit,
|
||||
) {
|
||||
if (room.users.size == 1) {
|
||||
LoadUser(baseUserHex = room.users.first(), accountViewModel) { baseUser ->
|
||||
|
@ -560,7 +599,7 @@ fun ChatroomHeader(
|
|||
baseUser = baseUser,
|
||||
modifier = modifier,
|
||||
accountViewModel = accountViewModel,
|
||||
nav = nav,
|
||||
onClick = onClick,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
@ -569,7 +608,7 @@ fun ChatroomHeader(
|
|||
room = room,
|
||||
modifier = modifier,
|
||||
accountViewModel = accountViewModel,
|
||||
nav = nav,
|
||||
onClick = onClick,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
@ -579,13 +618,13 @@ fun ChatroomHeader(
|
|||
baseUser: User,
|
||||
modifier: Modifier = StdPadding,
|
||||
accountViewModel: AccountViewModel,
|
||||
nav: (String) -> Unit,
|
||||
onClick: () -> Unit,
|
||||
) {
|
||||
Column(
|
||||
modifier =
|
||||
Modifier.fillMaxWidth()
|
||||
.clickable(
|
||||
onClick = { nav("User/${baseUser.pubkeyHex}") },
|
||||
onClick = onClick,
|
||||
),
|
||||
) {
|
||||
Column(
|
||||
|
@ -601,14 +640,9 @@ fun ChatroomHeader(
|
|||
|
||||
Column(modifier = Modifier.padding(start = 10.dp)) {
|
||||
UsernameDisplay(baseUser)
|
||||
ObserveDisplayNip05Status(baseUser, accountViewModel = accountViewModel, nav = nav)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
HorizontalDivider(
|
||||
thickness = DividerThickness,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -617,12 +651,10 @@ fun GroupChatroomHeader(
|
|||
room: ChatroomKey,
|
||||
modifier: Modifier = StdPadding,
|
||||
accountViewModel: AccountViewModel,
|
||||
nav: (String) -> Unit,
|
||||
onClick: () -> Unit,
|
||||
) {
|
||||
val expanded = remember { mutableStateOf(false) }
|
||||
|
||||
Column(
|
||||
modifier = Modifier.fillMaxWidth().clickable { expanded.value = !expanded.value },
|
||||
modifier = Modifier.fillMaxWidth().clickable(onClick = onClick),
|
||||
) {
|
||||
Column(
|
||||
verticalArrangement = Arrangement.Center,
|
||||
|
@ -640,15 +672,7 @@ fun GroupChatroomHeader(
|
|||
DisplayUserSetAsSubject(room, accountViewModel, FontWeight.Normal)
|
||||
}
|
||||
}
|
||||
|
||||
if (expanded.value) {
|
||||
LongRoomHeader(room = room, accountViewModel = accountViewModel, nav = nav)
|
||||
}
|
||||
}
|
||||
|
||||
HorizontalDivider(
|
||||
thickness = DividerThickness,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -0,0 +1,82 @@
|
|||
/**
|
||||
* Copyright (c) 2024 Vitor Pamplona
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
* this software and associated documentation files (the "Software"), to deal in
|
||||
* the Software without restriction, including without limitation the rights to use,
|
||||
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
|
||||
* Software, and to permit persons to whom the Software is furnished to do so,
|
||||
* subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
|
||||
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
package com.vitorpamplona.amethyst.ui.screen.loggedIn
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.DisposableEffect
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.ui.platform.LocalLifecycleOwner
|
||||
import androidx.lifecycle.Lifecycle
|
||||
import androidx.lifecycle.LifecycleEventObserver
|
||||
import androidx.lifecycle.viewmodel.compose.viewModel
|
||||
import com.vitorpamplona.amethyst.ui.screen.NostrDraftEventsFeedViewModel
|
||||
import com.vitorpamplona.amethyst.ui.screen.RefresheableFeedView
|
||||
|
||||
@Composable
|
||||
fun DraftListScreen(
|
||||
accountViewModel: AccountViewModel,
|
||||
nav: (String) -> Unit,
|
||||
) {
|
||||
val draftFeedViewModel: NostrDraftEventsFeedViewModel =
|
||||
viewModel(
|
||||
key = "NostrDraftEventsFeedViewModel",
|
||||
factory = NostrDraftEventsFeedViewModel.Factory(accountViewModel.account),
|
||||
)
|
||||
|
||||
RenderDraftListScreen(draftFeedViewModel, accountViewModel, nav)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun RenderDraftListScreen(
|
||||
feedViewModel: NostrDraftEventsFeedViewModel,
|
||||
accountViewModel: AccountViewModel,
|
||||
nav: (String) -> Unit,
|
||||
) {
|
||||
val lifeCycleOwner = LocalLifecycleOwner.current
|
||||
|
||||
LaunchedEffect(feedViewModel) {
|
||||
feedViewModel.invalidateData()
|
||||
}
|
||||
|
||||
DisposableEffect(lifeCycleOwner) {
|
||||
val observer =
|
||||
LifecycleEventObserver { _, event ->
|
||||
if (event == Lifecycle.Event.ON_RESUME) {
|
||||
println("DraftList Start")
|
||||
feedViewModel.invalidateData()
|
||||
}
|
||||
if (event == Lifecycle.Event.ON_PAUSE) {
|
||||
println("DraftList Stop")
|
||||
}
|
||||
}
|
||||
|
||||
lifeCycleOwner.lifecycle.addObserver(observer)
|
||||
onDispose { lifeCycleOwner.lifecycle.removeObserver(observer) }
|
||||
}
|
||||
|
||||
RefresheableFeedView(
|
||||
feedViewModel,
|
||||
null,
|
||||
accountViewModel = accountViewModel,
|
||||
nav = nav,
|
||||
)
|
||||
}
|
|
@ -93,6 +93,7 @@ val Size40dp = 40.dp
|
|||
val Size55dp = 55.dp
|
||||
val Size75dp = 75.dp
|
||||
|
||||
val HalfEndPadding = Modifier.padding(end = 5.dp)
|
||||
val HalfStartPadding = Modifier.padding(start = 5.dp)
|
||||
val StdStartPadding = Modifier.padding(start = 10.dp)
|
||||
val StdTopPadding = Modifier.padding(top = 10.dp)
|
||||
|
|
|
@ -622,6 +622,7 @@
|
|||
<string name="server_did_not_provide_a_url_after_uploading">El servidor no proporcionó una URL después de la carga</string>
|
||||
<string name="could_not_download_from_the_server">No se pudo descargar el contenido cargado desde el servidor</string>
|
||||
<string name="could_not_prepare_local_file_to_upload">No se pudo preparar el archivo local para cargar: %1$s</string>
|
||||
<string name="edit_draft">Editar borrador</string>
|
||||
<string name="login_with_qr_code">Iniciar sesión con código QR</string>
|
||||
<string name="route">Ruta</string>
|
||||
<string name="route_home">Inicio</string>
|
||||
|
@ -693,4 +694,9 @@
|
|||
<string name="accessibility_play_username">Reproducir nombre de usuario como audio</string>
|
||||
<string name="accessibility_scan_qr_code">Escanear código QR</string>
|
||||
<string name="accessibility_navigate_to_alby">Ir al proveedor externo de monederos Alby</string>
|
||||
<string name="it_s_not_possible_to_reply_to_a_draft_note">No es posible responder a un borrador de nota</string>
|
||||
<string name="it_s_not_possible_to_quote_to_a_draft_note">No es posible citar un borrador de nota</string>
|
||||
<string name="it_s_not_possible_to_react_to_a_draft_note">No es posible reaccionar a un borrador de nota</string>
|
||||
<string name="it_s_not_possible_to_zap_to_a_draft_note">No es posible zapear un borrador de nota</string>
|
||||
<string name="draft_note">Borrador de nota</string>
|
||||
</resources>
|
||||
|
|
|
@ -622,6 +622,7 @@
|
|||
<string name="server_did_not_provide_a_url_after_uploading">El servidor no proporcionó una URL después de la subida</string>
|
||||
<string name="could_not_download_from_the_server">No se pudo descargar el contenido subido desde el servidor</string>
|
||||
<string name="could_not_prepare_local_file_to_upload">No se pudo preparar el archivo local para subir: %1$s</string>
|
||||
<string name="edit_draft">Editar borrador</string>
|
||||
<string name="login_with_qr_code">Iniciar sesión con código QR</string>
|
||||
<string name="route">Ruta</string>
|
||||
<string name="route_home">Inicio</string>
|
||||
|
@ -693,4 +694,9 @@
|
|||
<string name="accessibility_play_username">Reproducir nombre de usuario como audio</string>
|
||||
<string name="accessibility_scan_qr_code">Escanear código QR</string>
|
||||
<string name="accessibility_navigate_to_alby">Ir al proveedor externo de billeteras Alby</string>
|
||||
<string name="it_s_not_possible_to_reply_to_a_draft_note">No es posible responder a un borrador de nota</string>
|
||||
<string name="it_s_not_possible_to_quote_to_a_draft_note">No es posible citar un borrador de nota</string>
|
||||
<string name="it_s_not_possible_to_react_to_a_draft_note">No es posible reaccionar a un borrador de nota</string>
|
||||
<string name="it_s_not_possible_to_zap_to_a_draft_note">No es posible zapear un borrador de nota</string>
|
||||
<string name="draft_note">Borrador de nota</string>
|
||||
</resources>
|
||||
|
|
|
@ -622,6 +622,7 @@
|
|||
<string name="server_did_not_provide_a_url_after_uploading">El servidor no proporcionó una URL después de la subida</string>
|
||||
<string name="could_not_download_from_the_server">No se pudo descargar el contenido subido desde el servidor</string>
|
||||
<string name="could_not_prepare_local_file_to_upload">No se pudo preparar el archivo local para subir: %1$s</string>
|
||||
<string name="edit_draft">Editar borrador</string>
|
||||
<string name="login_with_qr_code">Iniciar sesión con código QR</string>
|
||||
<string name="route">Ruta</string>
|
||||
<string name="route_home">Inicio</string>
|
||||
|
@ -693,4 +694,9 @@
|
|||
<string name="accessibility_play_username">Reproducir nombre de usuario como audio</string>
|
||||
<string name="accessibility_scan_qr_code">Escanear código QR</string>
|
||||
<string name="accessibility_navigate_to_alby">Ir al proveedor externo de billeteras Alby</string>
|
||||
<string name="it_s_not_possible_to_reply_to_a_draft_note">No es posible responder a un borrador de nota</string>
|
||||
<string name="it_s_not_possible_to_quote_to_a_draft_note">No es posible citar un borrador de nota</string>
|
||||
<string name="it_s_not_possible_to_react_to_a_draft_note">No es posible reaccionar a un borrador de nota</string>
|
||||
<string name="it_s_not_possible_to_zap_to_a_draft_note">No es posible zapear un borrador de nota</string>
|
||||
<string name="draft_note">Borrador de nota</string>
|
||||
</resources>
|
||||
|
|
|
@ -622,6 +622,7 @@
|
|||
<string name="server_did_not_provide_a_url_after_uploading">Le serveur n\'a pas fourni d\'URL après le téléversement</string>
|
||||
<string name="could_not_download_from_the_server">Impossible de télécharger le média depuis le serveur</string>
|
||||
<string name="could_not_prepare_local_file_to_upload">Impossible de préparer le fichier local à téléverser: %1$s</string>
|
||||
<string name="edit_draft">Modifier le brouillon</string>
|
||||
<string name="login_with_qr_code">Se connecter avec un QR Code</string>
|
||||
<string name="route">Route</string>
|
||||
<string name="route_home">Accueil</string>
|
||||
|
@ -693,4 +694,9 @@
|
|||
<string name="accessibility_play_username">Écouter le nom d\'utilisateur</string>
|
||||
<string name="accessibility_scan_qr_code">Scanner le QR code</string>
|
||||
<string name="accessibility_navigate_to_alby">Accéder au fournisseur de portefeuille tiers Alby</string>
|
||||
<string name="it_s_not_possible_to_reply_to_a_draft_note">Il n\'est pas possible de répondre à un brouillon</string>
|
||||
<string name="it_s_not_possible_to_quote_to_a_draft_note">Il n\'est pas possible de citer un brouillon</string>
|
||||
<string name="it_s_not_possible_to_react_to_a_draft_note">Il n\'est pas possible de réagir à un brouillon</string>
|
||||
<string name="it_s_not_possible_to_zap_to_a_draft_note">Il n\'est pas possible de zapper un brouillon</string>
|
||||
<string name="draft_note">Brouillon</string>
|
||||
</resources>
|
||||
|
|
|
@ -293,6 +293,7 @@
|
|||
<string name="block_only">Block</string>
|
||||
|
||||
<string name="bookmarks">Bookmarks</string>
|
||||
<string name="drafts">Drafts</string>
|
||||
<string name="private_bookmarks">Private Bookmarks</string>
|
||||
<string name="public_bookmarks">Public Bookmarks</string>
|
||||
<string name="add_to_private_bookmarks">Add to Private Bookmarks</string>
|
||||
|
@ -733,6 +734,8 @@
|
|||
<string name="could_not_download_from_the_server">Could not download uploaded media from the server</string>
|
||||
<string name="could_not_prepare_local_file_to_upload">Could not prepare local file to upload: %1$s</string>
|
||||
|
||||
<string name="edit_draft">Edit draft</string>
|
||||
|
||||
<string name="login_with_qr_code">Login with QR Code</string>
|
||||
<string name="route">Route</string>
|
||||
<string name="route_home">Home</string>
|
||||
|
@ -819,4 +822,9 @@
|
|||
<string name="accessibility_play_username">Play username as audio</string>
|
||||
<string name="accessibility_scan_qr_code">Scan QR code</string>
|
||||
<string name="accessibility_navigate_to_alby">Navigate to the third-party wallet provider Alby</string>
|
||||
<string name="it_s_not_possible_to_reply_to_a_draft_note">It\'s not possible to reply a draft note</string>
|
||||
<string name="it_s_not_possible_to_quote_to_a_draft_note">It\'s not possible to quote a draft note</string>
|
||||
<string name="it_s_not_possible_to_react_to_a_draft_note">It\'s not possible to react a draft note</string>
|
||||
<string name="it_s_not_possible_to_zap_to_a_draft_note">It\'s not possible to zap a draft note</string>
|
||||
<string name="draft_note">Draft Note</string>
|
||||
</resources>
|
||||
|
|
|
@ -0,0 +1,55 @@
|
|||
/**
|
||||
* Copyright (c) 2024 Vitor Pamplona
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
* this software and associated documentation files (the "Software"), to deal in
|
||||
* the Software without restriction, including without limitation the rights to use,
|
||||
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
|
||||
* Software, and to permit persons to whom the Software is furnished to do so,
|
||||
* subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
|
||||
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
package com.vitorpamplona.amethyst.benchmark
|
||||
|
||||
import androidx.benchmark.junit4.BenchmarkRule
|
||||
import androidx.benchmark.junit4.measureRepeated
|
||||
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||
import androidx.test.platform.app.InstrumentationRegistry.getInstrumentation
|
||||
import com.vitorpamplona.amethyst.commons.preview.MetaTagsParser
|
||||
import org.junit.Assert.assertEquals
|
||||
import org.junit.Assert.assertNotNull
|
||||
import org.junit.Rule
|
||||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
import java.nio.charset.Charset
|
||||
|
||||
@RunWith(AndroidJUnit4::class)
|
||||
class MetaTagsParserBenchmark {
|
||||
private val html =
|
||||
getInstrumentation().context.assets.open("github_amethyst.html")
|
||||
.readBytes().toString(Charset.forName("utf-8"))
|
||||
|
||||
@get:Rule
|
||||
val benchmarkRule = BenchmarkRule()
|
||||
|
||||
@Test
|
||||
fun parseMetaTags() {
|
||||
benchmarkRule.measureRepeated {
|
||||
val metaOgTitle = MetaTagsParser.parse(html).find { it.attr("property") == "og:title" }
|
||||
assertNotNull(metaOgTitle)
|
||||
assertEquals(
|
||||
"GitHub - vitorpamplona/amethyst: Nostr client for Android",
|
||||
metaOgTitle!!.attr("content"),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -68,6 +68,7 @@ class GiftWrapReceivingBenchmark {
|
|||
markAsSensitive = true,
|
||||
zapRaiserAmount = 10000,
|
||||
geohash = null,
|
||||
isDraft = true,
|
||||
signer = sender,
|
||||
) {
|
||||
SealedGossipEvent.create(
|
||||
|
@ -107,6 +108,7 @@ class GiftWrapReceivingBenchmark {
|
|||
markAsSensitive = true,
|
||||
zapRaiserAmount = 10000,
|
||||
geohash = null,
|
||||
isDraft = true,
|
||||
signer = sender,
|
||||
) {
|
||||
SealedGossipEvent.create(
|
||||
|
|
|
@ -0,0 +1,81 @@
|
|||
/**
|
||||
* Copyright (c) 2024 Vitor Pamplona
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
* this software and associated documentation files (the "Software"), to deal in
|
||||
* the Software without restriction, including without limitation the rights to use,
|
||||
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
|
||||
* Software, and to permit persons to whom the Software is furnished to do so,
|
||||
* subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
|
||||
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
package com.vitorpamplona.amethyst.commons.preview
|
||||
|
||||
import org.junit.Assert.assertEquals
|
||||
import org.junit.Test
|
||||
|
||||
class MetaTagsParserTest {
|
||||
@Test
|
||||
fun testParse() {
|
||||
val input =
|
||||
"""<html>
|
||||
| <head>
|
||||
| <meta charset="utf-8">
|
||||
| <meta http-equiv="content-type" content="text/html; charset=utf-8">
|
||||
| <meta property="og:title" content=title>
|
||||
| <meta property="og:description" content='description'>
|
||||
| <meta property="og:image" content="https://example.com/img/foo.png">
|
||||
| <!-- edge cases -->
|
||||
| <meta
|
||||
| name="newline"
|
||||
| content="newline"
|
||||
| >
|
||||
| <meta name="space before gt" >
|
||||
| <meta name ="space before =">
|
||||
| <meta name= "space after =">
|
||||
| <META NAME="CAPITAL">
|
||||
| <meta name="character reference" content="<meta>">
|
||||
| <meta name="attr value with end of head doesn't harm" content="<head>bang!</head>">
|
||||
| <meta name="ignore tags with duplicated attr" name="dup">
|
||||
| </head>
|
||||
| <body>
|
||||
| <meta name="ignore meta tags in body">
|
||||
| </body>
|
||||
|</html>
|
||||
""".trimMargin()
|
||||
|
||||
val exp =
|
||||
listOf(
|
||||
listOf("charset" to "utf-8"),
|
||||
listOf("http-equiv" to "content-type", "content" to "text/html; charset=utf-8"),
|
||||
listOf("property" to "og:title", "content" to "title"),
|
||||
listOf("property" to "og:description", "content" to "description"),
|
||||
listOf("property" to "og:image", "content" to "https://example.com/img/foo.png"),
|
||||
listOf("name" to "newline", "content" to "newline"),
|
||||
listOf("name" to "space before gt"),
|
||||
listOf("name" to "space before ="),
|
||||
listOf("name" to "space after ="),
|
||||
listOf("name" to "CAPITAL"),
|
||||
listOf("name" to "character reference", "content" to "<meta>"),
|
||||
listOf("name" to "attr value with end of head doesn't harm", "content" to "<head>bang!</head>"),
|
||||
)
|
||||
|
||||
val metaTags = MetaTagsParser.parse(input).toList()
|
||||
println(metaTags)
|
||||
assertEquals(exp.size, metaTags.size)
|
||||
metaTags.zip(exp).forEach { (meta, expAttrs) ->
|
||||
expAttrs.forEach { (name, expValue) ->
|
||||
assertEquals(expValue, meta.attr(name))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,301 @@
|
|||
/**
|
||||
* Copyright (c) 2024 Vitor Pamplona
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
* this software and associated documentation files (the "Software"), to deal in
|
||||
* the Software without restriction, including without limitation the rights to use,
|
||||
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
|
||||
* Software, and to permit persons to whom the Software is furnished to do so,
|
||||
* subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
|
||||
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
package com.vitorpamplona.amethyst.commons.preview
|
||||
|
||||
import kotlinx.collections.immutable.toImmutableMap
|
||||
|
||||
data class MetaTag(private val attrs: Map<String, String>) {
|
||||
/**
|
||||
* Returns a value of an attribute specified by its name (case insensitive), or empty string if it doesn't exist.
|
||||
*/
|
||||
fun attr(name: String): String = attrs[name.lowercase()] ?: ""
|
||||
}
|
||||
|
||||
object MetaTagsParser {
|
||||
private val NON_ATTR_NAME_CHARS = setOf(Char(0x0), '"', '\'', '>', '/')
|
||||
private val NON_UNQUOTED_ATTR_VALUE_CHARS = setOf('"', '\'', '=', '>', '<', '`')
|
||||
|
||||
/**
|
||||
* Lazily parse a partial HTML document and extract meta tags.
|
||||
*/
|
||||
fun parse(input: String): Sequence<MetaTag> =
|
||||
sequence {
|
||||
val s = TagScanner(input)
|
||||
while (!s.exhausted()) {
|
||||
val t = s.nextTag() ?: continue
|
||||
if (t.name == "head" && t.isEnd) {
|
||||
break
|
||||
}
|
||||
if (t.name == "meta") {
|
||||
val attrs = parseAttrs(t.attrPart) ?: continue
|
||||
yield(MetaTag(attrs))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private data class RawTag(val isEnd: Boolean, val name: String, val attrPart: String)
|
||||
|
||||
private class TagScanner(private val input: String) {
|
||||
private var p = 0
|
||||
|
||||
fun exhausted(): Boolean = p >= input.length
|
||||
|
||||
private fun peek(): Char = input[p]
|
||||
|
||||
private fun consume(): Char = input[p++]
|
||||
|
||||
private fun skipWhile(pred: (Char) -> Boolean) {
|
||||
while (!this.exhausted() && pred(this.peek())) {
|
||||
this.consume()
|
||||
}
|
||||
}
|
||||
|
||||
private fun skipSpaces() {
|
||||
this.skipWhile { it.isWhitespace() }
|
||||
}
|
||||
|
||||
fun nextTag(): RawTag? {
|
||||
skipWhile { it != '<' }
|
||||
consume()
|
||||
|
||||
// read tag name
|
||||
val isEnd = peek() == '/'
|
||||
if (isEnd) {
|
||||
consume()
|
||||
}
|
||||
val nameStart = p
|
||||
skipWhile { !it.isWhitespace() && it != '>' }
|
||||
val nameEnd = p
|
||||
|
||||
// seek to start of attrs part
|
||||
skipSpaces()
|
||||
val attrsStart = p
|
||||
|
||||
// skip until end of tag
|
||||
var quote: Char? = null
|
||||
while (!exhausted()) {
|
||||
val c = consume()
|
||||
when {
|
||||
// `/>` out of quote -> end of tag
|
||||
quote == null && c == '/' && peek() == '>' -> {
|
||||
consume()
|
||||
break
|
||||
}
|
||||
// `>` out of quote -> end of tag
|
||||
quote == null && c == '>' -> {
|
||||
break
|
||||
}
|
||||
// entering quote
|
||||
quote == null && (c == '\'' || c == '"') -> {
|
||||
quote = c
|
||||
}
|
||||
// leaving quote
|
||||
quote != null && c == quote -> {
|
||||
quote = null
|
||||
}
|
||||
}
|
||||
}
|
||||
val attrsEnd = p - 1
|
||||
|
||||
val name = input.slice(nameStart..<nameEnd)
|
||||
if (!name.matches(Regex("""[0-9a-zA-Z]+"""))) {
|
||||
return null
|
||||
}
|
||||
val attrsPart = input.slice(attrsStart..<attrsEnd)
|
||||
return RawTag(isEnd, name.lowercase(), attrsPart)
|
||||
}
|
||||
}
|
||||
|
||||
// map of HTML element attribute name to its value, with additional logics:
|
||||
// - attribute names are matched in a case-insensitive manner
|
||||
// - attribute names never duplicate
|
||||
// - commonly used character references in attribute values are resolved
|
||||
private class Attrs {
|
||||
companion object {
|
||||
val RE_CHAR_REF = Regex("""&(\w+)(;?)""")
|
||||
val BASE_CHAR_REFS =
|
||||
mapOf(
|
||||
"amp" to "&",
|
||||
"AMP" to "&",
|
||||
"quot" to "\"",
|
||||
"QUOT" to "\"",
|
||||
"lt" to "<",
|
||||
"LT" to "<",
|
||||
"gt" to ">",
|
||||
"GT" to ">",
|
||||
)
|
||||
val CHAR_REFS =
|
||||
mapOf(
|
||||
"apos" to "'",
|
||||
"equals" to "=",
|
||||
"grave" to "`",
|
||||
"DiacriticalGrave" to "`",
|
||||
)
|
||||
|
||||
fun replaceCharRefs(match: MatchResult): String {
|
||||
val bcr = BASE_CHAR_REFS[match.groupValues[1]]
|
||||
if (bcr != null) {
|
||||
return bcr
|
||||
}
|
||||
// non-base char refs must be terminated by ';'
|
||||
if (match.groupValues[2].isNotEmpty()) {
|
||||
val cr = CHAR_REFS[match.groupValues[1]]
|
||||
if (cr != null) {
|
||||
return cr
|
||||
}
|
||||
}
|
||||
return match.value
|
||||
}
|
||||
}
|
||||
|
||||
private val attrs = mutableMapOf<String, String>()
|
||||
|
||||
fun add(attr: Pair<String, String>) {
|
||||
val name = attr.first.lowercase()
|
||||
if (attrs.containsKey(name)) {
|
||||
throw IllegalArgumentException("duplicated attribute name: $name")
|
||||
}
|
||||
val value = attr.second.replace(RE_CHAR_REF, Companion::replaceCharRefs)
|
||||
attrs += Pair(name, value)
|
||||
}
|
||||
|
||||
fun freeze(): Map<String, String> = attrs.toImmutableMap()
|
||||
}
|
||||
|
||||
private enum class State {
|
||||
NAME,
|
||||
BEFORE_EQ,
|
||||
AFTER_EQ,
|
||||
VALUE,
|
||||
SPACE,
|
||||
}
|
||||
|
||||
private fun parseAttrs(input: String): Map<String, String>? {
|
||||
val attrs = Attrs()
|
||||
|
||||
var state = State.NAME
|
||||
var nameBegin = 0
|
||||
var nameEnd = 0
|
||||
var valueBegin = 0
|
||||
var valueQuote: Char? = null
|
||||
|
||||
input.forEachIndexed { i, c ->
|
||||
when (state) {
|
||||
State.NAME -> {
|
||||
when {
|
||||
c == '=' -> {
|
||||
nameEnd = i
|
||||
state = State.AFTER_EQ
|
||||
}
|
||||
|
||||
c.isWhitespace() -> {
|
||||
nameEnd = i
|
||||
state = State.BEFORE_EQ
|
||||
}
|
||||
|
||||
NON_ATTR_NAME_CHARS.contains(c) || c.isISOControl() || !c.isDefined() -> {
|
||||
return null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
State.BEFORE_EQ -> {
|
||||
when {
|
||||
c == '=' -> {
|
||||
state = State.AFTER_EQ
|
||||
}
|
||||
|
||||
c.isWhitespace() -> {}
|
||||
else -> return null
|
||||
}
|
||||
}
|
||||
|
||||
State.AFTER_EQ -> {
|
||||
when {
|
||||
c.isWhitespace() -> {}
|
||||
c == '\'' || c == '"' -> {
|
||||
valueBegin = i + 1
|
||||
valueQuote = c
|
||||
state = State.VALUE
|
||||
}
|
||||
|
||||
else -> {
|
||||
valueBegin = i
|
||||
valueQuote = null
|
||||
state = State.VALUE
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
State.VALUE -> {
|
||||
var attr: Pair<String, String>? = null
|
||||
when {
|
||||
valueQuote != null -> {
|
||||
if (c == valueQuote) {
|
||||
attr =
|
||||
Pair(
|
||||
input.slice(nameBegin..<nameEnd),
|
||||
input.slice(valueBegin..<i),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
valueQuote == null -> {
|
||||
when {
|
||||
c.isWhitespace() -> {
|
||||
attr =
|
||||
Pair(
|
||||
input.slice(nameBegin..<nameEnd),
|
||||
input.slice(valueBegin..<i),
|
||||
)
|
||||
}
|
||||
|
||||
i == input.length - 1 -> {
|
||||
attr =
|
||||
Pair(
|
||||
input.slice(nameBegin..<nameEnd),
|
||||
input.slice(valueBegin..i),
|
||||
)
|
||||
}
|
||||
|
||||
NON_UNQUOTED_ATTR_VALUE_CHARS.contains(c) -> {
|
||||
return null
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if (attr != null) {
|
||||
runCatching { attrs.add(attr) }.getOrNull() ?: return null
|
||||
state = State.SPACE
|
||||
}
|
||||
}
|
||||
|
||||
State.SPACE -> {
|
||||
if (!c.isWhitespace()) {
|
||||
nameBegin = i
|
||||
state = State.NAME
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return attrs.freeze()
|
||||
}
|
||||
}
|
|
@ -15,12 +15,11 @@ coil = "2.6.0"
|
|||
composeBom = "2024.03.00"
|
||||
coreKtx = "1.12.0"
|
||||
espressoCore = "3.5.1"
|
||||
firebaseBom = "32.7.4"
|
||||
firebaseBom = "32.8.0"
|
||||
fragmentKtx = "1.6.2"
|
||||
gms = "4.4.1"
|
||||
jacksonModuleKotlin = "2.17.0"
|
||||
jna = "5.14.0"
|
||||
jsoup = "1.17.2"
|
||||
junit = "4.13.2"
|
||||
kotlin = "1.9.22"
|
||||
kotlinxCollectionsImmutable = "0.3.7"
|
||||
|
@ -93,7 +92,6 @@ google-mlkit-language-id = { group = "com.google.mlkit", name = "language-id", v
|
|||
google-mlkit-translate = { group = "com.google.mlkit", name = "translate", version.ref = "translate" }
|
||||
jackson-module-kotlin = { group = "com.fasterxml.jackson.module", name = "jackson-module-kotlin", version.ref = "jacksonModuleKotlin" }
|
||||
jna = { group = "net.java.dev.jna", name = "jna", version.ref = "jna" }
|
||||
jsoup = { group = "org.jsoup", name = "jsoup", version.ref = "jsoup" }
|
||||
junit = { group = "junit", name = "junit", version.ref = "junit" }
|
||||
kotlinx-collections-immutable = { group = "org.jetbrains.kotlinx", name = "kotlinx-collections-immutable", version.ref = "kotlinxCollectionsImmutable" }
|
||||
lazysodium-android = { group = "com.goterl", name = "lazysodium-android", version.ref = "lazysodiumAndroid" }
|
||||
|
|
File diff suppressed because one or more lines are too long
|
@ -60,6 +60,7 @@ class ChannelMessageEvent(
|
|||
zapRaiserAmount: Long?,
|
||||
geohash: String? = null,
|
||||
nip94attachments: List<FileHeaderEvent>? = null,
|
||||
isDraft: Boolean,
|
||||
onReady: (ChannelMessageEvent) -> Unit,
|
||||
) {
|
||||
val tags =
|
||||
|
@ -87,7 +88,11 @@ class ChannelMessageEvent(
|
|||
arrayOf("alt", ALT),
|
||||
)
|
||||
|
||||
signer.sign(createdAt, KIND, tags.toTypedArray(), message, onReady)
|
||||
if (isDraft) {
|
||||
signer.assembleRumor(createdAt, KIND, tags.toTypedArray(), message, onReady)
|
||||
} else {
|
||||
signer.sign(createdAt, KIND, tags.toTypedArray(), message, onReady)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -82,11 +82,12 @@ class ChatMessageEvent(
|
|||
signer: NostrSigner,
|
||||
createdAt: Long = TimeUtils.now(),
|
||||
nip94attachments: List<FileHeaderEvent>? = null,
|
||||
isDraft: Boolean,
|
||||
onReady: (ChatMessageEvent) -> Unit,
|
||||
) {
|
||||
val tags = mutableListOf<Array<String>>()
|
||||
to?.forEach { tags.add(arrayOf("p", it)) }
|
||||
replyTos?.forEach { tags.add(arrayOf("e", it)) }
|
||||
replyTos?.forEach { tags.add(arrayOf("e", it, "", "reply")) }
|
||||
mentions?.forEach { tags.add(arrayOf("p", it, "", "mention")) }
|
||||
zapReceiver?.forEach {
|
||||
tags.add(arrayOf("zap", it.lnAddressOrPubKeyHex, it.relay ?: "", it.weight.toString()))
|
||||
|
@ -106,7 +107,11 @@ class ChatMessageEvent(
|
|||
}
|
||||
// tags.add(arrayOf("alt", alt))
|
||||
|
||||
signer.sign(createdAt, KIND, tags.toTypedArray(), msg, onReady)
|
||||
if (isDraft) {
|
||||
signer.assembleRumor(createdAt, KIND, tags.toTypedArray(), msg, onReady)
|
||||
} else {
|
||||
signer.sign(createdAt, KIND, tags.toTypedArray(), msg, onReady)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -113,6 +113,7 @@ class ClassifiedsEvent(
|
|||
nip94attachments: List<Event>? = null,
|
||||
signer: NostrSigner,
|
||||
createdAt: Long = TimeUtils.now(),
|
||||
isDraft: Boolean,
|
||||
onReady: (ClassifiedsEvent) -> Unit,
|
||||
) {
|
||||
val tags = mutableListOf<Array<String>>()
|
||||
|
@ -192,7 +193,11 @@ class ClassifiedsEvent(
|
|||
}
|
||||
tags.add(arrayOf("alt", ALT))
|
||||
|
||||
signer.sign(createdAt, KIND, tags.toTypedArray(), message, onReady)
|
||||
if (isDraft) {
|
||||
signer.assembleRumor(createdAt, KIND, tags.toTypedArray(), message, onReady)
|
||||
} else {
|
||||
signer.sign(createdAt, KIND, tags.toTypedArray(), message, onReady)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -34,22 +34,59 @@ class DeletionEvent(
|
|||
content: String,
|
||||
sig: HexKey,
|
||||
) : Event(id, pubKey, createdAt, KIND, tags, content, sig) {
|
||||
fun deleteEvents() = tags.map { it[1] }
|
||||
fun deleteEvents() = taggedEvents()
|
||||
|
||||
fun deleteAddresses() = taggedAddresses()
|
||||
|
||||
companion object {
|
||||
const val KIND = 5
|
||||
const val ALT = "Deletion event"
|
||||
|
||||
fun create(
|
||||
deleteEvents: List<String>,
|
||||
deleteEvents: List<Event>,
|
||||
signer: NostrSigner,
|
||||
createdAt: Long = TimeUtils.now(),
|
||||
onReady: (DeletionEvent) -> Unit,
|
||||
) {
|
||||
val content = ""
|
||||
val tags =
|
||||
deleteEvents.map { arrayOf("e", it) }.plusElement(arrayOf("alt", ALT)).toTypedArray()
|
||||
signer.sign(createdAt, KIND, tags, content, onReady)
|
||||
val tags = mutableListOf<Array<String>>()
|
||||
|
||||
val kinds =
|
||||
deleteEvents.mapTo(HashSet()) {
|
||||
"${it.kind}"
|
||||
}.map {
|
||||
arrayOf("k", it)
|
||||
}
|
||||
|
||||
tags.addAll(deleteEvents.map { arrayOf("e", it.id) })
|
||||
tags.addAll(deleteEvents.mapNotNull { if (it is AddressableEvent) arrayOf("a", it.address().toTag()) else null })
|
||||
tags.addAll(kinds)
|
||||
tags.add(arrayOf("alt", ALT))
|
||||
|
||||
signer.sign(createdAt, KIND, tags.toTypedArray(), content, onReady)
|
||||
}
|
||||
|
||||
fun createForVersionOnly(
|
||||
deleteEvents: List<Event>,
|
||||
signer: NostrSigner,
|
||||
createdAt: Long = TimeUtils.now(),
|
||||
onReady: (DeletionEvent) -> Unit,
|
||||
) {
|
||||
val content = ""
|
||||
val tags = mutableListOf<Array<String>>()
|
||||
|
||||
val kinds =
|
||||
deleteEvents.mapTo(HashSet()) {
|
||||
"${it.kind}"
|
||||
}.map {
|
||||
arrayOf("k", it)
|
||||
}
|
||||
|
||||
tags.addAll(deleteEvents.map { arrayOf("e", it.id) })
|
||||
tags.addAll(kinds)
|
||||
tags.add(arrayOf("alt", ALT))
|
||||
|
||||
signer.sign(createdAt, KIND, tags.toTypedArray(), content, onReady)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,210 @@
|
|||
/**
|
||||
* Copyright (c) 2024 Vitor Pamplona
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
* this software and associated documentation files (the "Software"), to deal in
|
||||
* the Software without restriction, including without limitation the rights to use,
|
||||
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
|
||||
* Software, and to permit persons to whom the Software is furnished to do so,
|
||||
* subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
|
||||
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
package com.vitorpamplona.quartz.events
|
||||
|
||||
import androidx.compose.runtime.Immutable
|
||||
import com.vitorpamplona.quartz.encoders.ATag
|
||||
import com.vitorpamplona.quartz.encoders.HexKey
|
||||
import com.vitorpamplona.quartz.signers.NostrSigner
|
||||
import com.vitorpamplona.quartz.utils.TimeUtils
|
||||
|
||||
@Immutable
|
||||
class DraftEvent(
|
||||
id: HexKey,
|
||||
pubKey: HexKey,
|
||||
createdAt: Long,
|
||||
tags: Array<Array<String>>,
|
||||
content: String,
|
||||
sig: HexKey,
|
||||
) : BaseAddressableEvent(id, pubKey, createdAt, KIND, tags, content, sig) {
|
||||
@Transient private var cachedInnerEvent: Map<HexKey, Event?> = mapOf()
|
||||
|
||||
override fun isContentEncoded() = true
|
||||
|
||||
fun isDeleted() = content == ""
|
||||
|
||||
fun preCachedDraft(signer: NostrSigner): Event? {
|
||||
return cachedInnerEvent[signer.pubKey]
|
||||
}
|
||||
|
||||
fun preCachedDraft(pubKey: HexKey): Event? {
|
||||
return cachedInnerEvent[pubKey]
|
||||
}
|
||||
|
||||
fun allCache() = cachedInnerEvent.values
|
||||
|
||||
fun addToCache(
|
||||
pubKey: HexKey,
|
||||
innerEvent: Event,
|
||||
) {
|
||||
cachedInnerEvent = cachedInnerEvent + Pair(pubKey, innerEvent)
|
||||
}
|
||||
|
||||
fun cachedDraft(
|
||||
signer: NostrSigner,
|
||||
onReady: (Event) -> Unit,
|
||||
) {
|
||||
cachedInnerEvent[signer.pubKey]?.let {
|
||||
onReady(it)
|
||||
return
|
||||
}
|
||||
decrypt(signer) { draft ->
|
||||
addToCache(signer.pubKey, draft)
|
||||
|
||||
onReady(draft)
|
||||
}
|
||||
}
|
||||
|
||||
private fun decrypt(
|
||||
signer: NostrSigner,
|
||||
onReady: (Event) -> Unit,
|
||||
) {
|
||||
try {
|
||||
plainContent(signer) { onReady(fromJson(it)) }
|
||||
} catch (e: Exception) {
|
||||
// Log.e("UnwrapError", "Couldn't Decrypt the content", e)
|
||||
}
|
||||
}
|
||||
|
||||
private fun plainContent(
|
||||
signer: NostrSigner,
|
||||
onReady: (String) -> Unit,
|
||||
) {
|
||||
if (content.isEmpty()) return
|
||||
|
||||
signer.nip44Decrypt(content, pubKey, onReady)
|
||||
}
|
||||
|
||||
fun createDeletedEvent(
|
||||
signer: NostrSigner,
|
||||
onReady: (DraftEvent) -> Unit,
|
||||
) {
|
||||
signer.sign<DraftEvent>(createdAt, KIND, tags, "") {
|
||||
onReady(it)
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
const val KIND = 31234
|
||||
|
||||
fun createAddressTag(
|
||||
pubKey: HexKey,
|
||||
dTag: String,
|
||||
): String {
|
||||
return ATag(KIND, pubKey, dTag, null).toTag()
|
||||
}
|
||||
|
||||
fun create(
|
||||
dTag: String,
|
||||
originalNote: LiveActivitiesChatMessageEvent,
|
||||
signer: NostrSigner,
|
||||
createdAt: Long = TimeUtils.now(),
|
||||
onReady: (DraftEvent) -> Unit,
|
||||
) {
|
||||
val tags = mutableListOf<Array<String>>()
|
||||
originalNote.activity()?.let { tags.add(arrayOf("a", it.toTag())) }
|
||||
originalNote.replyingTo()?.let { tags.add(arrayOf("e", it)) }
|
||||
|
||||
create(dTag, originalNote, emptyList(), signer, createdAt, onReady)
|
||||
}
|
||||
|
||||
fun create(
|
||||
dTag: String,
|
||||
originalNote: ChannelMessageEvent,
|
||||
signer: NostrSigner,
|
||||
createdAt: Long = TimeUtils.now(),
|
||||
onReady: (DraftEvent) -> Unit,
|
||||
) {
|
||||
val tags = mutableListOf<Array<String>>()
|
||||
originalNote.channel()?.let { tags.add(arrayOf("e", it)) }
|
||||
|
||||
create(dTag, originalNote, tags, signer, createdAt, onReady)
|
||||
}
|
||||
|
||||
fun create(
|
||||
dTag: String,
|
||||
originalNote: GitReplyEvent,
|
||||
signer: NostrSigner,
|
||||
createdAt: Long = TimeUtils.now(),
|
||||
onReady: (DraftEvent) -> Unit,
|
||||
) {
|
||||
val tags = mutableListOf<Array<String>>()
|
||||
originalNote.repository()?.let { tags.add(arrayOf("a", it.toTag())) }
|
||||
originalNote.replyingTo()?.let { tags.add(arrayOf("e", it)) }
|
||||
|
||||
create(dTag, originalNote, tags, signer, createdAt, onReady)
|
||||
}
|
||||
|
||||
fun create(
|
||||
dTag: String,
|
||||
originalNote: PollNoteEvent,
|
||||
signer: NostrSigner,
|
||||
createdAt: Long = TimeUtils.now(),
|
||||
onReady: (DraftEvent) -> Unit,
|
||||
) {
|
||||
val tagsWithMarkers =
|
||||
originalNote.tags().filter {
|
||||
it.size > 3 && (it[0] == "e" || it[0] == "a") && (it[3] == "root" || it[3] == "reply")
|
||||
}
|
||||
|
||||
create(dTag, originalNote, tagsWithMarkers, signer, createdAt, onReady)
|
||||
}
|
||||
|
||||
fun create(
|
||||
dTag: String,
|
||||
originalNote: TextNoteEvent,
|
||||
signer: NostrSigner,
|
||||
createdAt: Long = TimeUtils.now(),
|
||||
onReady: (DraftEvent) -> Unit,
|
||||
) {
|
||||
val tagsWithMarkers =
|
||||
originalNote.tags().filter {
|
||||
it.size > 3 && (it[0] == "e" || it[0] == "a") && (it[3] == "root" || it[3] == "reply")
|
||||
}
|
||||
|
||||
create(dTag, originalNote, tagsWithMarkers, signer, createdAt, onReady)
|
||||
}
|
||||
|
||||
fun create(
|
||||
dTag: String,
|
||||
innerEvent: Event,
|
||||
anchorTagArray: List<Array<String>> = emptyList(),
|
||||
signer: NostrSigner,
|
||||
createdAt: Long = TimeUtils.now(),
|
||||
onReady: (DraftEvent) -> Unit,
|
||||
) {
|
||||
val tags = mutableListOf<Array<String>>()
|
||||
tags.add(arrayOf("d", dTag))
|
||||
tags.add(arrayOf("k", "${innerEvent.kind}"))
|
||||
|
||||
if (anchorTagArray.isNotEmpty()) {
|
||||
tags.addAll(anchorTagArray)
|
||||
}
|
||||
|
||||
signer.nip44Encrypt(innerEvent.toJson(), signer.pubKey) { encryptedContent ->
|
||||
signer.sign<DraftEvent>(createdAt, KIND, tags.toTypedArray(), encryptedContent) {
|
||||
it.addToCache(signer.pubKey, innerEvent)
|
||||
onReady(it)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -97,6 +97,8 @@ open class Event(
|
|||
|
||||
override fun firstTaggedUrl() = tags.firstOrNull { it.size > 1 && it[0] == "r" }?.let { it[1] }
|
||||
|
||||
override fun firstTaggedK() = tags.firstOrNull { it.size > 1 && it[0] == "k" }?.let { it[1].toIntOrNull() }
|
||||
|
||||
override fun firstTaggedAddress() =
|
||||
tags
|
||||
.firstOrNull { it.size > 1 && it[0] == "a" }
|
||||
|
|
|
@ -79,6 +79,7 @@ class EventFactory {
|
|||
CommunityPostApprovalEvent(id, pubKey, createdAt, tags, content, sig)
|
||||
ContactListEvent.KIND -> ContactListEvent(id, pubKey, createdAt, tags, content, sig)
|
||||
DeletionEvent.KIND -> DeletionEvent(id, pubKey, createdAt, tags, content, sig)
|
||||
DraftEvent.KIND -> DraftEvent(id, pubKey, createdAt, tags, content, sig)
|
||||
EmojiPackEvent.KIND -> EmojiPackEvent(id, pubKey, createdAt, tags, content, sig)
|
||||
EmojiPackSelectionEvent.KIND ->
|
||||
EmojiPackSelectionEvent(id, pubKey, createdAt, tags, content, sig)
|
||||
|
|
|
@ -133,6 +133,8 @@ interface EventInterface {
|
|||
|
||||
fun firstTaggedUrl(): String?
|
||||
|
||||
fun firstTaggedK(): Int?
|
||||
|
||||
fun taggedEmojis(): List<EmojiUrl>
|
||||
|
||||
fun matchTag1With(text: String): Boolean
|
||||
|
|
|
@ -94,6 +94,7 @@ class GitReplyEvent(
|
|||
forkedFrom: Event? = null,
|
||||
signer: NostrSigner,
|
||||
createdAt: Long = TimeUtils.now(),
|
||||
isDraft: Boolean,
|
||||
onReady: (GitReplyEvent) -> Unit,
|
||||
) {
|
||||
val tags = mutableListOf<Array<String>>()
|
||||
|
@ -156,7 +157,11 @@ class GitReplyEvent(
|
|||
}
|
||||
tags.add(arrayOf("alt", "a git issue reply"))
|
||||
|
||||
signer.sign(createdAt, KIND, tags.toTypedArray(), msg, onReady)
|
||||
if (isDraft) {
|
||||
signer.assembleRumor(createdAt, KIND, tags.toTypedArray(), msg, onReady)
|
||||
} else {
|
||||
signer.sign(createdAt, KIND, tags.toTypedArray(), msg, onReady)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -72,6 +72,7 @@ class LiveActivitiesChatMessageEvent(
|
|||
zapRaiserAmount: Long?,
|
||||
geohash: String? = null,
|
||||
nip94attachments: List<FileHeaderEvent>? = null,
|
||||
isDraft: Boolean,
|
||||
onReady: (LiveActivitiesChatMessageEvent) -> Unit,
|
||||
) {
|
||||
val content = message
|
||||
|
@ -98,7 +99,11 @@ class LiveActivitiesChatMessageEvent(
|
|||
}
|
||||
tags.add(arrayOf("alt", ALT))
|
||||
|
||||
signer.sign(createdAt, KIND, tags.toTypedArray(), content, onReady)
|
||||
if (isDraft) {
|
||||
signer.assembleRumor(createdAt, KIND, tags.toTypedArray(), content, onReady)
|
||||
} else {
|
||||
signer.sign(createdAt, KIND, tags.toTypedArray(), content, onReady)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -20,7 +20,6 @@
|
|||
*/
|
||||
package com.vitorpamplona.quartz.events
|
||||
|
||||
import com.vitorpamplona.quartz.encoders.ATag
|
||||
import com.vitorpamplona.quartz.encoders.HexKey
|
||||
import com.vitorpamplona.quartz.signers.NostrSigner
|
||||
|
||||
|
@ -78,6 +77,7 @@ class NIP24Factory {
|
|||
zapRaiserAmount: Long? = null,
|
||||
geohash: String? = null,
|
||||
nip94attachments: List<FileHeaderEvent>? = null,
|
||||
draftTag: String? = null,
|
||||
onReady: (Result) -> Unit,
|
||||
) {
|
||||
val senderPublicKey = signer.pubKey
|
||||
|
@ -93,15 +93,25 @@ class NIP24Factory {
|
|||
markAsSensitive = markAsSensitive,
|
||||
zapRaiserAmount = zapRaiserAmount,
|
||||
geohash = geohash,
|
||||
isDraft = draftTag != null,
|
||||
nip94attachments = nip94attachments,
|
||||
) { senderMessage ->
|
||||
createWraps(senderMessage, to.plus(senderPublicKey).toSet(), signer) { wraps ->
|
||||
if (draftTag != null) {
|
||||
onReady(
|
||||
Result(
|
||||
msg = senderMessage,
|
||||
wraps = wraps,
|
||||
wraps = listOf(),
|
||||
),
|
||||
)
|
||||
} else {
|
||||
createWraps(senderMessage, to.plus(senderPublicKey).toSet(), signer) { wraps ->
|
||||
onReady(
|
||||
Result(
|
||||
msg = senderMessage,
|
||||
wraps = wraps,
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -155,49 +165,4 @@ class NIP24Factory {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun createTextNoteNIP24(
|
||||
msg: String,
|
||||
to: List<HexKey>,
|
||||
signer: NostrSigner,
|
||||
replyTos: List<String>? = null,
|
||||
mentions: List<String>? = null,
|
||||
addresses: List<ATag>?,
|
||||
extraTags: List<String>?,
|
||||
zapReceiver: List<ZapSplitSetup>? = null,
|
||||
markAsSensitive: Boolean = false,
|
||||
replyingTo: String?,
|
||||
root: String?,
|
||||
directMentions: Set<HexKey>,
|
||||
zapRaiserAmount: Long? = null,
|
||||
geohash: String? = null,
|
||||
onReady: (Result) -> Unit,
|
||||
) {
|
||||
val senderPublicKey = signer.pubKey
|
||||
|
||||
TextNoteEvent.create(
|
||||
msg = msg,
|
||||
signer = signer,
|
||||
replyTos = replyTos,
|
||||
mentions = mentions,
|
||||
zapReceiver = zapReceiver,
|
||||
root = root,
|
||||
extraTags = extraTags,
|
||||
addresses = addresses,
|
||||
directMentions = directMentions,
|
||||
replyingTo = replyingTo,
|
||||
markAsSensitive = markAsSensitive,
|
||||
zapRaiserAmount = zapRaiserAmount,
|
||||
geohash = geohash,
|
||||
) { senderMessage ->
|
||||
createWraps(senderMessage, to.plus(senderPublicKey).toSet(), signer) { wraps ->
|
||||
onReady(
|
||||
Result(
|
||||
msg = senderMessage,
|
||||
wraps = wraps,
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -80,6 +80,7 @@ class PollNoteEvent(
|
|||
zapRaiserAmount: Long?,
|
||||
geohash: String? = null,
|
||||
nip94attachments: List<FileHeaderEvent>? = null,
|
||||
isDraft: Boolean,
|
||||
onReady: (PollNoteEvent) -> Unit,
|
||||
) {
|
||||
val tags = mutableListOf<Array<String>>()
|
||||
|
@ -112,7 +113,11 @@ class PollNoteEvent(
|
|||
}
|
||||
tags.add(arrayOf("alt", ALT))
|
||||
|
||||
signer.sign(createdAt, KIND, tags.toTypedArray(), msg, onReady)
|
||||
if (isDraft) {
|
||||
signer.assembleRumor(createdAt, KIND, tags.toTypedArray(), msg, onReady)
|
||||
} else {
|
||||
signer.sign(createdAt, KIND, tags.toTypedArray(), msg, onReady)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -126,6 +126,7 @@ class PrivateDmEvent(
|
|||
zapRaiserAmount: Long?,
|
||||
geohash: String? = null,
|
||||
nip94attachments: List<FileHeaderEvent>? = null,
|
||||
isDraft: Boolean,
|
||||
onReady: (PrivateDmEvent) -> Unit,
|
||||
) {
|
||||
var message = msg
|
||||
|
@ -145,7 +146,7 @@ class PrivateDmEvent(
|
|||
|
||||
val tags = mutableListOf<Array<String>>()
|
||||
publishedRecipientPubKey?.let { tags.add(arrayOf("p", publishedRecipientPubKey)) }
|
||||
replyTos?.forEach { tags.add(arrayOf("e", it)) }
|
||||
replyTos?.forEach { tags.add(arrayOf("e", it, "", "reply")) }
|
||||
mentions?.forEach { tags.add(arrayOf("p", it)) }
|
||||
zapReceiver?.forEach {
|
||||
tags.add(arrayOf("zap", it.lnAddressOrPubKeyHex, it.relay ?: "", it.weight.toString()))
|
||||
|
@ -167,7 +168,11 @@ class PrivateDmEvent(
|
|||
tags.add(arrayOf("alt", ALT))
|
||||
|
||||
signer.nip04Encrypt(message, recipientPubKey) { content ->
|
||||
signer.sign(createdAt, KIND, tags.toTypedArray(), content, onReady)
|
||||
if (isDraft) {
|
||||
signer.assembleRumor(createdAt, KIND, tags.toTypedArray(), content, onReady)
|
||||
} else {
|
||||
signer.sign(createdAt, KIND, tags.toTypedArray(), content, onReady)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -60,6 +60,7 @@ class TextNoteEvent(
|
|||
forkedFrom: Event? = null,
|
||||
signer: NostrSigner,
|
||||
createdAt: Long = TimeUtils.now(),
|
||||
isDraft: Boolean,
|
||||
onReady: (TextNoteEvent) -> Unit,
|
||||
) {
|
||||
val tags = mutableListOf<Array<String>>()
|
||||
|
@ -124,7 +125,11 @@ class TextNoteEvent(
|
|||
}
|
||||
}
|
||||
|
||||
signer.sign(createdAt, KIND, tags.toTypedArray(), msg, onReady)
|
||||
if (isDraft) {
|
||||
signer.assembleRumor(createdAt, KIND, tags.toTypedArray(), msg, onReady)
|
||||
} else {
|
||||
signer.sign(createdAt, KIND, tags.toTypedArray(), msg, onReady)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -171,6 +171,10 @@ class ExternalSignerLauncher(
|
|||
"sign_event",
|
||||
22242,
|
||||
),
|
||||
Permission(
|
||||
"sign_event",
|
||||
31234,
|
||||
),
|
||||
Permission(
|
||||
"nip04_encrypt",
|
||||
),
|
||||
|
|
|
@ -21,7 +21,9 @@
|
|||
package com.vitorpamplona.quartz.signers
|
||||
|
||||
import com.vitorpamplona.quartz.encoders.HexKey
|
||||
import com.vitorpamplona.quartz.encoders.toHexKey
|
||||
import com.vitorpamplona.quartz.events.Event
|
||||
import com.vitorpamplona.quartz.events.EventFactory
|
||||
import com.vitorpamplona.quartz.events.LnZapPrivateEvent
|
||||
import com.vitorpamplona.quartz.events.LnZapRequestEvent
|
||||
|
||||
|
@ -62,4 +64,26 @@ abstract class NostrSigner(val pubKey: HexKey) {
|
|||
event: LnZapRequestEvent,
|
||||
onReady: (LnZapPrivateEvent) -> Unit,
|
||||
)
|
||||
|
||||
fun <T : Event> assembleRumor(
|
||||
createdAt: Long,
|
||||
kind: Int,
|
||||
tags: Array<Array<String>>,
|
||||
content: String,
|
||||
onReady: (T) -> Unit,
|
||||
) {
|
||||
val id = Event.generateId(pubKey, createdAt, kind, tags, content).toHexKey()
|
||||
|
||||
onReady(
|
||||
EventFactory.create(
|
||||
id = id,
|
||||
pubKey = pubKey,
|
||||
createdAt = createdAt,
|
||||
kind = kind,
|
||||
tags = tags,
|
||||
content = content,
|
||||
sig = "",
|
||||
) as T,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
Ładowanie…
Reference in New Issue