mirror of
https://github.com/vitorpamplona/amethyst.git
synced 2025-03-26 17:52:29 +01:00
Adds support for NIP-35 torrents and their comments
This commit is contained in:
parent
83f1e523ea
commit
a05ad6e686
@ -102,6 +102,7 @@ import com.vitorpamplona.quartz.events.SearchRelayListEvent
|
||||
import com.vitorpamplona.quartz.events.StatusEvent
|
||||
import com.vitorpamplona.quartz.events.TextNoteEvent
|
||||
import com.vitorpamplona.quartz.events.TextNoteModificationEvent
|
||||
import com.vitorpamplona.quartz.events.TorrentCommentEvent
|
||||
import com.vitorpamplona.quartz.events.WrappedEvent
|
||||
import com.vitorpamplona.quartz.events.ZapSplitSetup
|
||||
import com.vitorpamplona.quartz.signers.NostrSigner
|
||||
@ -1796,6 +1797,78 @@ class Account(
|
||||
}
|
||||
}
|
||||
|
||||
fun sendTorrentComment(
|
||||
message: String,
|
||||
replyTo: List<Note>?,
|
||||
mentions: List<User>?,
|
||||
zapReceiver: List<ZapSplitSetup>? = null,
|
||||
wantsToMarkAsSensitive: Boolean,
|
||||
zapRaiserAmount: Long? = null,
|
||||
replyingTo: String?,
|
||||
root: String,
|
||||
directMentions: Set<HexKey>,
|
||||
forkedFrom: Event?,
|
||||
relayList: List<RelaySetupInfo>? = null,
|
||||
geohash: String? = null,
|
||||
nip94attachments: List<FileHeaderEvent>? = null,
|
||||
draftTag: String?,
|
||||
) {
|
||||
if (!isWriteable()) return
|
||||
|
||||
val repliesToHex = replyTo?.filter { it.address() == null }?.map { it.idHex }
|
||||
val mentionsHex = mentions?.map { it.pubkeyHex }
|
||||
val addresses = replyTo?.mapNotNull { it.address() } ?: emptyList()
|
||||
|
||||
TorrentCommentEvent.create(
|
||||
message = message,
|
||||
replyTos = repliesToHex,
|
||||
mentions = mentionsHex,
|
||||
zapReceiver = zapReceiver,
|
||||
markAsSensitive = wantsToMarkAsSensitive,
|
||||
zapRaiserAmount = zapRaiserAmount,
|
||||
replyingTo = replyingTo,
|
||||
torrent = root,
|
||||
directMentions = directMentions,
|
||||
geohash = geohash,
|
||||
nip94attachments = nip94attachments,
|
||||
forkedFrom = forkedFrom,
|
||||
signer = signer,
|
||||
isDraft = draftTag != null,
|
||||
) {
|
||||
if (draftTag != null) {
|
||||
if (message.isBlank()) {
|
||||
deleteDraft(draftTag)
|
||||
} else {
|
||||
DraftEvent.create(draftTag, it, signer) { draftEvent ->
|
||||
val newRelayList = getPrivateOutboxRelayList()?.relays()
|
||||
if (newRelayList != null) {
|
||||
Client.sendPrivately(draftEvent, newRelayList)
|
||||
} else {
|
||||
Client.send(draftEvent, relayList = relayList)
|
||||
}
|
||||
LocalCache.justConsume(draftEvent, null)
|
||||
}
|
||||
}
|
||||
} 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 deleteDraft(draftTag: String) {
|
||||
val key = DraftEvent.createAddressTag(userProfile().pubkeyHex, draftTag)
|
||||
LocalCache.getAddressableNoteIfExists(key)?.let { note ->
|
||||
|
@ -114,6 +114,8 @@ import com.vitorpamplona.quartz.events.SearchRelayListEvent
|
||||
import com.vitorpamplona.quartz.events.StatusEvent
|
||||
import com.vitorpamplona.quartz.events.TextNoteEvent
|
||||
import com.vitorpamplona.quartz.events.TextNoteModificationEvent
|
||||
import com.vitorpamplona.quartz.events.TorrentCommentEvent
|
||||
import com.vitorpamplona.quartz.events.TorrentEvent
|
||||
import com.vitorpamplona.quartz.events.VideoHorizontalEvent
|
||||
import com.vitorpamplona.quartz.events.VideoVerticalEvent
|
||||
import com.vitorpamplona.quartz.events.WikiNoteEvent
|
||||
@ -463,6 +465,61 @@ object LocalCache {
|
||||
refreshObservers(note)
|
||||
}
|
||||
|
||||
fun consume(
|
||||
event: TorrentEvent,
|
||||
relay: Relay?,
|
||||
) {
|
||||
val note = getOrCreateNote(event.id)
|
||||
val author = getOrCreateUser(event.pubKey)
|
||||
|
||||
if (relay != null) {
|
||||
author.addRelayBeingUsed(relay, event.createdAt)
|
||||
note.addRelay(relay)
|
||||
}
|
||||
|
||||
// Already processed this event.
|
||||
if (note.event != null) return
|
||||
|
||||
if (antiSpam.isSpam(event, relay)) {
|
||||
return
|
||||
}
|
||||
|
||||
note.loadEvent(event, author, emptyList())
|
||||
|
||||
refreshObservers(note)
|
||||
}
|
||||
|
||||
fun consume(
|
||||
event: TorrentCommentEvent,
|
||||
relay: Relay?,
|
||||
) {
|
||||
val note = getOrCreateNote(event.id)
|
||||
val author = getOrCreateUser(event.pubKey)
|
||||
|
||||
if (relay != null) {
|
||||
author.addRelayBeingUsed(relay, event.createdAt)
|
||||
note.addRelay(relay)
|
||||
}
|
||||
|
||||
// Already processed this event.
|
||||
if (note.event != null) return
|
||||
|
||||
if (antiSpam.isSpam(event, relay)) {
|
||||
return
|
||||
}
|
||||
|
||||
val replyTo = computeReplyTo(event)
|
||||
|
||||
note.loadEvent(event, author, replyTo)
|
||||
|
||||
// Counts the replies
|
||||
replyTo.forEach {
|
||||
it.addReply(note)
|
||||
}
|
||||
|
||||
refreshObservers(note)
|
||||
}
|
||||
|
||||
fun consume(
|
||||
event: NIP90ContentDiscoveryResponseEvent,
|
||||
relay: Relay? = null,
|
||||
@ -795,6 +852,8 @@ object LocalCache {
|
||||
.tagsWithoutCitations()
|
||||
.filter { it != event.activity()?.toTag() }
|
||||
.mapNotNull { checkGetOrCreateNote(it) }
|
||||
is TorrentCommentEvent ->
|
||||
event.tagsWithoutCitations().mapNotNull { checkGetOrCreateNote(it) }
|
||||
|
||||
is DraftEvent -> {
|
||||
event.mapTaggedEvent { checkGetOrCreateNote(it) } + event.mapTaggedAddress { checkGetOrCreateAddressableNote(it) }
|
||||
@ -1303,6 +1362,10 @@ object LocalCache {
|
||||
getChannelIfExists(it.toTag())?.removeNote(deleteNote)
|
||||
}
|
||||
|
||||
(deletedEvent as? TorrentCommentEvent)?.torrent()?.let {
|
||||
getNoteIfExists(it)?.removeReply(deleteNote)
|
||||
}
|
||||
|
||||
if (deletedEvent is PrivateDmEvent) {
|
||||
val author = deleteNote.author
|
||||
val recipient =
|
||||
@ -2664,6 +2727,8 @@ object LocalCache {
|
||||
is StatusEvent -> consume(event, relay)
|
||||
is TextNoteEvent -> consume(event, relay)
|
||||
is TextNoteModificationEvent -> consume(event, relay)
|
||||
is TorrentEvent -> consume(event, relay)
|
||||
is TorrentCommentEvent -> consume(event, relay)
|
||||
is VideoHorizontalEvent -> consume(event, relay)
|
||||
is VideoVerticalEvent -> consume(event, relay)
|
||||
is WikiNoteEvent -> consume(event, relay)
|
||||
|
@ -42,6 +42,7 @@ import com.vitorpamplona.quartz.events.ReportEvent
|
||||
import com.vitorpamplona.quartz.events.RepostEvent
|
||||
import com.vitorpamplona.quartz.events.TextNoteEvent
|
||||
import com.vitorpamplona.quartz.events.TextNoteModificationEvent
|
||||
import com.vitorpamplona.quartz.events.TorrentCommentEvent
|
||||
|
||||
object NostrSingleEventDataSource : AmethystNostrDataSource("SingleEventFeed") {
|
||||
private var nextEventsToWatch = setOf<Note>()
|
||||
@ -179,6 +180,7 @@ object NostrSingleEventDataSource : AmethystNostrDataSource("SingleEventFeed") {
|
||||
DeletionEvent.KIND,
|
||||
NIP90ContentDiscoveryResponseEvent.KIND,
|
||||
NIP90StatusEvent.KIND,
|
||||
TorrentCommentEvent.KIND,
|
||||
),
|
||||
tags = mapOf("e" to it.map { it.idHex }),
|
||||
since = findMinimumEOSEs(it),
|
||||
|
@ -42,6 +42,8 @@ import com.vitorpamplona.quartz.events.PollNoteEvent
|
||||
import com.vitorpamplona.quartz.events.ProfileGalleryEntryEvent
|
||||
import com.vitorpamplona.quartz.events.RepostEvent
|
||||
import com.vitorpamplona.quartz.events.TextNoteEvent
|
||||
import com.vitorpamplona.quartz.events.TorrentCommentEvent
|
||||
import com.vitorpamplona.quartz.events.TorrentEvent
|
||||
import com.vitorpamplona.quartz.events.WikiNoteEvent
|
||||
|
||||
object NostrUserProfileDataSource : AmethystNostrDataSource("UserProfileFeed") {
|
||||
@ -89,6 +91,23 @@ object NostrUserProfileDataSource : AmethystNostrDataSource("UserProfileFeed") {
|
||||
)
|
||||
}
|
||||
|
||||
fun createUserPostsFilter2() =
|
||||
user?.let {
|
||||
TypedFilter(
|
||||
types = COMMON_FEED_TYPES,
|
||||
filter =
|
||||
SincePerRelayFilter(
|
||||
kinds =
|
||||
listOf(
|
||||
TorrentEvent.KIND,
|
||||
TorrentCommentEvent.KIND,
|
||||
),
|
||||
authors = listOf(it.pubkeyHex),
|
||||
limit = 20,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
fun createUserReceivedZapsFilter() =
|
||||
user?.let {
|
||||
TypedFilter(
|
||||
@ -188,6 +207,7 @@ object NostrUserProfileDataSource : AmethystNostrDataSource("UserProfileFeed") {
|
||||
listOfNotNull(
|
||||
createUserInfoFilter(),
|
||||
createUserPostsFilter(),
|
||||
createUserPostsFilter2(),
|
||||
createProfileGalleryFilter(),
|
||||
createFollowFilter(),
|
||||
createFollowersFilter(),
|
||||
|
@ -69,6 +69,8 @@ import com.vitorpamplona.quartz.events.GitIssueEvent
|
||||
import com.vitorpamplona.quartz.events.Price
|
||||
import com.vitorpamplona.quartz.events.PrivateDmEvent
|
||||
import com.vitorpamplona.quartz.events.TextNoteEvent
|
||||
import com.vitorpamplona.quartz.events.TorrentCommentEvent
|
||||
import com.vitorpamplona.quartz.events.TorrentEvent
|
||||
import com.vitorpamplona.quartz.events.ZapSplitSetup
|
||||
import com.vitorpamplona.quartz.events.findURLs
|
||||
import kotlinx.coroutines.CancellationException
|
||||
@ -681,6 +683,74 @@ open class NewPostViewModel : ViewModel() {
|
||||
nip94attachments = usedAttachments,
|
||||
draftTag = localDraft,
|
||||
)
|
||||
} else if (originalNote?.event is TorrentCommentEvent) {
|
||||
val originalNoteEvent = originalNote?.event as TorrentCommentEvent
|
||||
// adds markers
|
||||
val rootId =
|
||||
originalNoteEvent.torrent() // if it has a marker as root
|
||||
?: originalNote
|
||||
?.replyTo
|
||||
?.firstOrNull { it.event != null && it.replyTo?.isEmpty() == true }
|
||||
?.idHex // if it has loaded events with zero replies in the reply list
|
||||
?: originalNote?.replyTo?.firstOrNull()?.idHex // old rules, first item is root.
|
||||
?: originalNote?.idHex
|
||||
|
||||
if (rootId != null) {
|
||||
// There must be a torrent ID
|
||||
val replyId = originalNote?.idHex
|
||||
|
||||
val replyToSet =
|
||||
if (forkedFromNote != null) {
|
||||
(listOfNotNull(forkedFromNote) + (tagger.eTags ?: emptyList())).ifEmpty { null }
|
||||
} else {
|
||||
tagger.eTags
|
||||
}
|
||||
|
||||
account?.sendTorrentComment(
|
||||
message = tagger.message,
|
||||
replyTo = replyToSet,
|
||||
mentions = tagger.pTags,
|
||||
zapReceiver = zapReceiver,
|
||||
wantsToMarkAsSensitive = wantsToMarkAsSensitive,
|
||||
zapRaiserAmount = localZapRaiserAmount,
|
||||
replyingTo = replyId,
|
||||
root = rootId,
|
||||
directMentions = tagger.directMentions,
|
||||
forkedFrom = forkedFromNote?.event as? Event,
|
||||
relayList = relayList,
|
||||
geohash = geoHash,
|
||||
nip94attachments = usedAttachments,
|
||||
draftTag = localDraft,
|
||||
)
|
||||
}
|
||||
} else if (originalNote?.event is TorrentEvent) {
|
||||
val originalNoteEvent = originalNote?.event as TorrentEvent
|
||||
// adds markers
|
||||
val rootId = originalNoteEvent.id
|
||||
|
||||
val replyToSet =
|
||||
if (forkedFromNote != null) {
|
||||
(listOfNotNull(forkedFromNote) + (tagger.eTags ?: emptyList())).ifEmpty { null }
|
||||
} else {
|
||||
tagger.eTags
|
||||
}
|
||||
|
||||
account?.sendTorrentComment(
|
||||
message = tagger.message,
|
||||
replyTo = replyToSet,
|
||||
mentions = tagger.pTags,
|
||||
zapReceiver = zapReceiver,
|
||||
wantsToMarkAsSensitive = wantsToMarkAsSensitive,
|
||||
zapRaiserAmount = localZapRaiserAmount,
|
||||
replyingTo = null,
|
||||
root = rootId,
|
||||
directMentions = tagger.directMentions,
|
||||
forkedFrom = forkedFromNote?.event as? Event,
|
||||
relayList = relayList,
|
||||
geohash = geoHash,
|
||||
nip94attachments = usedAttachments,
|
||||
draftTag = localDraft,
|
||||
)
|
||||
} else {
|
||||
if (wantsPoll) {
|
||||
account?.sendPoll(
|
||||
|
@ -28,6 +28,7 @@ import com.vitorpamplona.quartz.events.ChannelMessageEvent
|
||||
import com.vitorpamplona.quartz.events.LiveActivitiesChatMessageEvent
|
||||
import com.vitorpamplona.quartz.events.PollNoteEvent
|
||||
import com.vitorpamplona.quartz.events.TextNoteEvent
|
||||
import com.vitorpamplona.quartz.events.TorrentCommentEvent
|
||||
|
||||
class UserProfileConversationsFeedFilter(
|
||||
val user: User,
|
||||
@ -59,7 +60,8 @@ class UserProfileConversationsFeedFilter(
|
||||
it.event is TextNoteEvent ||
|
||||
it.event is PollNoteEvent ||
|
||||
it.event is ChannelMessageEvent ||
|
||||
it.event is LiveActivitiesChatMessageEvent
|
||||
it.event is LiveActivitiesChatMessageEvent ||
|
||||
it.event is TorrentCommentEvent
|
||||
) &&
|
||||
!it.isNewThread() &&
|
||||
account.isAcceptable(it)
|
||||
|
@ -35,6 +35,7 @@ import com.vitorpamplona.quartz.events.LongTextNoteEvent
|
||||
import com.vitorpamplona.quartz.events.PollNoteEvent
|
||||
import com.vitorpamplona.quartz.events.RepostEvent
|
||||
import com.vitorpamplona.quartz.events.TextNoteEvent
|
||||
import com.vitorpamplona.quartz.events.TorrentEvent
|
||||
import com.vitorpamplona.quartz.events.WikiNoteEvent
|
||||
|
||||
class UserProfileNewThreadFeedFilter(
|
||||
@ -73,7 +74,8 @@ class UserProfileNewThreadFeedFilter(
|
||||
it.event is PollNoteEvent ||
|
||||
it.event is HighlightEvent ||
|
||||
it.event is AudioTrackEvent ||
|
||||
it.event is AudioHeaderEvent
|
||||
it.event is AudioHeaderEvent ||
|
||||
it.event is TorrentEvent
|
||||
) &&
|
||||
it.isNewThread() &&
|
||||
account.isAcceptable(it)
|
||||
|
@ -112,6 +112,8 @@ import com.vitorpamplona.amethyst.ui.note.types.RenderReaction
|
||||
import com.vitorpamplona.amethyst.ui.note.types.RenderReport
|
||||
import com.vitorpamplona.amethyst.ui.note.types.RenderTextEvent
|
||||
import com.vitorpamplona.amethyst.ui.note.types.RenderTextModificationEvent
|
||||
import com.vitorpamplona.amethyst.ui.note.types.RenderTorrent
|
||||
import com.vitorpamplona.amethyst.ui.note.types.RenderTorrentComment
|
||||
import com.vitorpamplona.amethyst.ui.note.types.RenderWikiContent
|
||||
import com.vitorpamplona.amethyst.ui.note.types.VideoDisplay
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
||||
@ -182,6 +184,8 @@ import com.vitorpamplona.quartz.events.RepostEvent
|
||||
import com.vitorpamplona.quartz.events.SearchRelayListEvent
|
||||
import com.vitorpamplona.quartz.events.TextNoteEvent
|
||||
import com.vitorpamplona.quartz.events.TextNoteModificationEvent
|
||||
import com.vitorpamplona.quartz.events.TorrentCommentEvent
|
||||
import com.vitorpamplona.quartz.events.TorrentEvent
|
||||
import com.vitorpamplona.quartz.events.VideoEvent
|
||||
import com.vitorpamplona.quartz.events.VideoHorizontalEvent
|
||||
import com.vitorpamplona.quartz.events.VideoVerticalEvent
|
||||
@ -754,6 +758,27 @@ private fun RenderNoteRow(
|
||||
accountViewModel,
|
||||
nav,
|
||||
)
|
||||
is TorrentEvent ->
|
||||
RenderTorrent(
|
||||
baseNote,
|
||||
backgroundColor,
|
||||
accountViewModel,
|
||||
nav,
|
||||
)
|
||||
|
||||
is TorrentCommentEvent ->
|
||||
RenderTorrentComment(
|
||||
baseNote,
|
||||
makeItShort,
|
||||
canPreview,
|
||||
quotesLeft,
|
||||
unPackReply,
|
||||
backgroundColor,
|
||||
editState,
|
||||
accountViewModel,
|
||||
nav,
|
||||
)
|
||||
|
||||
else -> {
|
||||
RenderTextEvent(
|
||||
baseNote,
|
||||
|
@ -0,0 +1,286 @@
|
||||
/**
|
||||
* 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 android.content.Intent
|
||||
import android.net.Uri
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.material3.IconButton
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.MutableState
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.core.content.ContextCompat
|
||||
import com.vitorpamplona.amethyst.R
|
||||
import com.vitorpamplona.amethyst.model.LocalCache
|
||||
import com.vitorpamplona.amethyst.model.Note
|
||||
import com.vitorpamplona.amethyst.ui.actions.relays.countToHumanReadableBytes
|
||||
import com.vitorpamplona.amethyst.ui.components.ShowMoreButton
|
||||
import com.vitorpamplona.amethyst.ui.note.DownloadForOfflineIcon
|
||||
import com.vitorpamplona.amethyst.ui.note.getGradient
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.mockAccountViewModel
|
||||
import com.vitorpamplona.amethyst.ui.theme.Size20dp
|
||||
import com.vitorpamplona.amethyst.ui.theme.Size30dp
|
||||
import com.vitorpamplona.amethyst.ui.theme.Size5dp
|
||||
import com.vitorpamplona.amethyst.ui.theme.ThemeComparisonColumn
|
||||
import com.vitorpamplona.quartz.events.TorrentEvent
|
||||
import com.vitorpamplona.quartz.events.TorrentFile
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
import kotlinx.collections.immutable.toImmutableList
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import kotlinx.coroutines.withContext
|
||||
import kotlin.coroutines.cancellation.CancellationException
|
||||
|
||||
@Preview
|
||||
@Composable
|
||||
fun TorrentPreview() {
|
||||
val accountViewModel = mockAccountViewModel()
|
||||
val nav: (String) -> Unit = {}
|
||||
|
||||
val torrent =
|
||||
runBlocking {
|
||||
withContext(Dispatchers.IO) {
|
||||
val torrent =
|
||||
TorrentEvent(
|
||||
id = "e1ab66dd66e6ac4f32119deacf80d7c50787705d343d3bb896c099cf93821757",
|
||||
pubKey = "0aa39e5aef99a000a7bdb0b499158c92bc4aa20fb65931a52d055b5eb6dff738",
|
||||
createdAt = 1724177932,
|
||||
tags =
|
||||
arrayOf(
|
||||
arrayOf("title", "bitcoin-core-27.0"),
|
||||
arrayOf("btih", "2b2d123e5e831b245fb1dc5b8b71f89de4a90d00"),
|
||||
arrayOf("t", "application"),
|
||||
arrayOf("file", "SHA256SUMS", "2842"),
|
||||
arrayOf("file", "SHA256SUMS.asc", "7590"),
|
||||
arrayOf("file", "SHA256SUMS.ots", "573"),
|
||||
arrayOf("file", "bitcoin-27.0-aarch64-linux-gnu-debug.tar.gz", "456216269"),
|
||||
arrayOf("file", "bitcoin-27.0-aarch64-linux-gnu.tar.gz", "46953175"),
|
||||
arrayOf("file", "bitcoin-27.0-arm-linux-gnueabihf-debug.tar.gz", "449092874"),
|
||||
arrayOf("file", "bitcoin-27.0-arm-linux-gnueabihf.tar.gz", "42887090"),
|
||||
arrayOf("file", "bitcoin-27.0-arm64-apple-darwin-unsigned.tar.gz", "16417606"),
|
||||
arrayOf("file", "bitcoin-27.0-arm64-apple-darwin-unsigned.zip", "16451447"),
|
||||
arrayOf("file", "bitcoin-27.0-arm64-apple-darwin.tar.gz", "36433324"),
|
||||
arrayOf("file", "bitcoin-27.0-arm64-apple-darwin.zip", "16217968"),
|
||||
arrayOf("file", "bitcoin-27.0-codesignatures-27.0.tar.gz", "335808"),
|
||||
arrayOf("file", "bitcoin-27.0-powerpc64-linux-gnu-debug.tar.gz", "467821947"),
|
||||
arrayOf("file", "bitcoin-27.0-powerpc64-linux-gnu.tar.gz", "53252604"),
|
||||
arrayOf("file", "bitcoin-27.0-powerpc64le-linux-gnu-debug.tar.gz", "460024501"),
|
||||
arrayOf("file", "bitcoin-27.0-powerpc64le-linux-gnu.tar.gz", "52020438"),
|
||||
arrayOf("file", "bitcoin-27.0-riscv64-linux-gnu-debug.tar.gz", "355544520"),
|
||||
arrayOf("file", "bitcoin-27.0-riscv64-linux-gnu.tar.gz", "47063263"),
|
||||
arrayOf("file", "bitcoin-27.0-win64-debug.zip", "522357199"),
|
||||
arrayOf("file", "bitcoin-27.0-win64-setup-unsigned.exe", "32348633"),
|
||||
arrayOf("file", "bitcoin-27.0-win64-setup.exe", "32359120"),
|
||||
arrayOf("file", "bitcoin-27.0-win64-unsigned.tar.gz", "32296875"),
|
||||
arrayOf("file", "bitcoin-27.0-win64.zip", "46821837"),
|
||||
arrayOf("file", "bitcoin-27.0-x86_64-apple-darwin-unsigned.tar.gz", "17160472"),
|
||||
arrayOf("file", "bitcoin-27.0-x86_64-apple-darwin-unsigned.zip", "17213924"),
|
||||
arrayOf("file", "bitcoin-27.0-x86_64-apple-darwin.tar.gz", "38428165"),
|
||||
arrayOf("file", "bitcoin-27.0-x86_64-apple-darwin.zip", "17474503"),
|
||||
arrayOf("file", "bitcoin-27.0-x86_64-linux-gnu-debug.tar.gz", "471529844"),
|
||||
arrayOf("file", "bitcoin-27.0-x86_64-linux-gnu.tar.gz", "48849225"),
|
||||
arrayOf("file", "bitcoin-27.0.tar.gz", "13082621"),
|
||||
),
|
||||
content = "bitcoin-core-27.0",
|
||||
sig = "40e1ccfdc38a32e6c164bb66e50df0cd3769e0431137a07709534a72b462dfcbc40106560d0dd66841fef4cbb7aece7db64e83a0fbe414759d4d9a799e522c57",
|
||||
)
|
||||
|
||||
LocalCache.justConsume(torrent, null)
|
||||
|
||||
LocalCache.getOrCreateNote("e1ab66dd66e6ac4f32119deacf80d7c50787705d343d3bb896c099cf93821757")
|
||||
}
|
||||
}
|
||||
|
||||
ThemeComparisonColumn(
|
||||
toPreview = {
|
||||
RenderTorrent(
|
||||
torrent,
|
||||
remember { mutableStateOf(Color.Transparent) },
|
||||
accountViewModel = accountViewModel,
|
||||
nav = nav,
|
||||
)
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun RenderTorrent(
|
||||
note: Note,
|
||||
backgroundColor: MutableState<Color>,
|
||||
accountViewModel: AccountViewModel,
|
||||
nav: (String) -> Unit,
|
||||
) {
|
||||
val noteEvent = note.event as? TorrentEvent ?: return
|
||||
|
||||
val name = (noteEvent.title() ?: TorrentEvent.ALT_DESCRIPTION)
|
||||
val size = " (" + countToHumanReadableBytes(noteEvent.totalSizeBytes()) + ")"
|
||||
|
||||
val description =
|
||||
if (noteEvent.content() != name) {
|
||||
noteEvent.content()
|
||||
} else {
|
||||
null
|
||||
}
|
||||
|
||||
DisplayFileList(
|
||||
noteEvent.files().toImmutableList(),
|
||||
name + size,
|
||||
description,
|
||||
noteEvent::toMagnetLink,
|
||||
backgroundColor,
|
||||
accountViewModel,
|
||||
nav,
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun DisplayFileList(
|
||||
files: ImmutableList<TorrentFile>,
|
||||
name: String,
|
||||
description: String?,
|
||||
link: () -> String,
|
||||
backgroundColor: MutableState<Color>,
|
||||
accountViewModel: AccountViewModel,
|
||||
nav: (String) -> Unit,
|
||||
) {
|
||||
var expanded by remember { mutableStateOf(false) }
|
||||
|
||||
val toMembersShow =
|
||||
if (expanded) {
|
||||
files
|
||||
} else {
|
||||
files.take(6)
|
||||
}
|
||||
|
||||
Column {
|
||||
Box(
|
||||
contentAlignment = Alignment.CenterEnd,
|
||||
modifier =
|
||||
Modifier
|
||||
.padding(horizontal = Size5dp)
|
||||
.fillMaxWidth(),
|
||||
) {
|
||||
Text(
|
||||
text = name,
|
||||
fontWeight = FontWeight.Bold,
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
textAlign = TextAlign.Center,
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
)
|
||||
|
||||
val context = LocalContext.current
|
||||
|
||||
IconButton(onClick = {
|
||||
try {
|
||||
val intent = Intent(Intent.ACTION_VIEW, Uri.parse(link()))
|
||||
intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK
|
||||
|
||||
ContextCompat.startActivity(context, intent, null)
|
||||
} catch (e: Exception) {
|
||||
if (e is CancellationException) throw e
|
||||
accountViewModel.toast(R.string.torrent_failure, R.string.torrent_no_apps)
|
||||
}
|
||||
}, Modifier.size(Size30dp)) {
|
||||
DownloadForOfflineIcon(Size20dp, MaterialTheme.colorScheme.onBackground)
|
||||
}
|
||||
}
|
||||
|
||||
description?.let {
|
||||
Text(
|
||||
text = it,
|
||||
maxLines = 3,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
textAlign = TextAlign.Center,
|
||||
color = Color.Gray,
|
||||
)
|
||||
}
|
||||
|
||||
Box {
|
||||
Column(modifier = Modifier.padding(top = 5.dp)) {
|
||||
toMembersShow.forEach { fileName ->
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
Text(
|
||||
text = fileName.fileName,
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
modifier =
|
||||
Modifier
|
||||
.padding(start = 10.dp, end = 5.dp)
|
||||
.weight(1f),
|
||||
)
|
||||
|
||||
fileName.bytes?.let {
|
||||
Text(
|
||||
text = countToHumanReadableBytes(it),
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
modifier =
|
||||
Modifier
|
||||
.padding(start = 5.dp, end = 10.dp),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (files.size > 3 && !expanded) {
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.Center,
|
||||
modifier =
|
||||
Modifier
|
||||
.align(Alignment.BottomCenter)
|
||||
.fillMaxWidth()
|
||||
.background(getGradient(backgroundColor)),
|
||||
) {
|
||||
ShowMoreButton { expanded = !expanded }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,260 @@
|
||||
/**
|
||||
* 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.clickable
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.outlined.FileOpen
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.MutableState
|
||||
import androidx.compose.runtime.State
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.livedata.observeAsState
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.vitorpamplona.amethyst.R
|
||||
import com.vitorpamplona.amethyst.model.LocalCache
|
||||
import com.vitorpamplona.amethyst.model.Note
|
||||
import com.vitorpamplona.amethyst.ui.actions.relays.countToHumanReadableBytes
|
||||
import com.vitorpamplona.amethyst.ui.components.GenericLoadable
|
||||
import com.vitorpamplona.amethyst.ui.components.LoadNote
|
||||
import com.vitorpamplona.amethyst.ui.navigation.routeFor
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.mockAccountViewModel
|
||||
import com.vitorpamplona.amethyst.ui.stringRes
|
||||
import com.vitorpamplona.amethyst.ui.theme.StdVertSpacer
|
||||
import com.vitorpamplona.amethyst.ui.theme.ThemeComparisonColumn
|
||||
import com.vitorpamplona.amethyst.ui.theme.replyModifier
|
||||
import com.vitorpamplona.quartz.events.TorrentCommentEvent
|
||||
import com.vitorpamplona.quartz.events.TorrentEvent
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import kotlinx.coroutines.withContext
|
||||
|
||||
@Preview
|
||||
@Composable
|
||||
fun TorrentCommentPreview() {
|
||||
val accountViewModel = mockAccountViewModel()
|
||||
val nav: (String) -> Unit = {}
|
||||
|
||||
val comment =
|
||||
runBlocking {
|
||||
withContext(Dispatchers.IO) {
|
||||
val torrent =
|
||||
TorrentEvent(
|
||||
id = "e1ab66dd66e6ac4f32119deacf80d7c50787705d343d3bb896c099cf93821757",
|
||||
pubKey = "0aa39e5aef99a000a7bdb0b499158c92bc4aa20fb65931a52d055b5eb6dff738",
|
||||
createdAt = 1724177932,
|
||||
tags =
|
||||
arrayOf(
|
||||
arrayOf("title", "bitcoin-core-27.0"),
|
||||
arrayOf("btih", "2b2d123e5e831b245fb1dc5b8b71f89de4a90d00"),
|
||||
arrayOf("t", "application"),
|
||||
arrayOf("file", "SHA256SUMS", "2842"),
|
||||
arrayOf("file", "SHA256SUMS.asc", "7590"),
|
||||
arrayOf("file", "SHA256SUMS.ots", "573"),
|
||||
arrayOf("file", "bitcoin-27.0-aarch64-linux-gnu-debug.tar.gz", "456216269"),
|
||||
arrayOf("file", "bitcoin-27.0-aarch64-linux-gnu.tar.gz", "46953175"),
|
||||
arrayOf("file", "bitcoin-27.0-arm-linux-gnueabihf-debug.tar.gz", "449092874"),
|
||||
arrayOf("file", "bitcoin-27.0-arm-linux-gnueabihf.tar.gz", "42887090"),
|
||||
arrayOf("file", "bitcoin-27.0-arm64-apple-darwin-unsigned.tar.gz", "16417606"),
|
||||
arrayOf("file", "bitcoin-27.0-arm64-apple-darwin-unsigned.zip", "16451447"),
|
||||
arrayOf("file", "bitcoin-27.0-arm64-apple-darwin.tar.gz", "36433324"),
|
||||
arrayOf("file", "bitcoin-27.0-arm64-apple-darwin.zip", "16217968"),
|
||||
arrayOf("file", "bitcoin-27.0-codesignatures-27.0.tar.gz", "335808"),
|
||||
arrayOf("file", "bitcoin-27.0-powerpc64-linux-gnu-debug.tar.gz", "467821947"),
|
||||
arrayOf("file", "bitcoin-27.0-powerpc64-linux-gnu.tar.gz", "53252604"),
|
||||
arrayOf("file", "bitcoin-27.0-powerpc64le-linux-gnu-debug.tar.gz", "460024501"),
|
||||
arrayOf("file", "bitcoin-27.0-powerpc64le-linux-gnu.tar.gz", "52020438"),
|
||||
arrayOf("file", "bitcoin-27.0-riscv64-linux-gnu-debug.tar.gz", "355544520"),
|
||||
arrayOf("file", "bitcoin-27.0-riscv64-linux-gnu.tar.gz", "47063263"),
|
||||
arrayOf("file", "bitcoin-27.0-win64-debug.zip", "522357199"),
|
||||
arrayOf("file", "bitcoin-27.0-win64-setup-unsigned.exe", "32348633"),
|
||||
arrayOf("file", "bitcoin-27.0-win64-setup.exe", "32359120"),
|
||||
arrayOf("file", "bitcoin-27.0-win64-unsigned.tar.gz", "32296875"),
|
||||
arrayOf("file", "bitcoin-27.0-win64.zip", "46821837"),
|
||||
arrayOf("file", "bitcoin-27.0-x86_64-apple-darwin-unsigned.tar.gz", "17160472"),
|
||||
arrayOf("file", "bitcoin-27.0-x86_64-apple-darwin-unsigned.zip", "17213924"),
|
||||
arrayOf("file", "bitcoin-27.0-x86_64-apple-darwin.tar.gz", "38428165"),
|
||||
arrayOf("file", "bitcoin-27.0-x86_64-apple-darwin.zip", "17474503"),
|
||||
arrayOf("file", "bitcoin-27.0-x86_64-linux-gnu-debug.tar.gz", "471529844"),
|
||||
arrayOf("file", "bitcoin-27.0-x86_64-linux-gnu.tar.gz", "48849225"),
|
||||
arrayOf("file", "bitcoin-27.0.tar.gz", "13082621"),
|
||||
),
|
||||
content = "bitcoin-core-27.0",
|
||||
sig = "40e1ccfdc38a32e6c164bb66e50df0cd3769e0431137a07709534a72b462dfcbc40106560d0dd66841fef4cbb7aece7db64e83a0fbe414759d4d9a799e522c57",
|
||||
)
|
||||
|
||||
val comment =
|
||||
TorrentCommentEvent(
|
||||
id = "040aba32010b5adf7cb917054e894e86c8ea7a2bcee448b2266c493f3140e9a0",
|
||||
pubKey = "460c25e682fda7832b52d1f22d3d22b3176d972f60dcdc3212ed8c92ef85065c",
|
||||
createdAt = 1724276875,
|
||||
tags = arrayOf(arrayOf("e", "e1ab66dd66e6ac4f32119deacf80d7c50787705d343d3bb896c099cf93821757", "wss://relay.damus.io/", "", "0aa39e5aef99a000a7bdb0b499158c92bc4aa20fb65931a52d055b5eb6dff738", "root")),
|
||||
content = "nice!",
|
||||
sig = "014391c310b1eebb807da4c9b11563126f2b795c9372a9432cae4dd2c0695b88584bb1c68814554c9b1a47626e3d60983e3653c29d0fdbc3a474277c140b95c3",
|
||||
)
|
||||
|
||||
LocalCache.justConsume(torrent, null)
|
||||
LocalCache.justConsume(comment, null)
|
||||
LocalCache.getOrCreateNote("040aba32010b5adf7cb917054e894e86c8ea7a2bcee448b2266c493f3140e9a0")
|
||||
}
|
||||
}
|
||||
|
||||
ThemeComparisonColumn(
|
||||
toPreview = {
|
||||
RenderTorrentComment(
|
||||
comment,
|
||||
false,
|
||||
true,
|
||||
3,
|
||||
true,
|
||||
remember { mutableStateOf(Color.Transparent) },
|
||||
EmptyState,
|
||||
accountViewModel = accountViewModel,
|
||||
nav = nav,
|
||||
)
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun RenderTorrentComment(
|
||||
note: Note,
|
||||
makeItShort: Boolean,
|
||||
canPreview: Boolean,
|
||||
quotesLeft: Int,
|
||||
unPackReply: Boolean,
|
||||
backgroundColor: MutableState<Color>,
|
||||
editState: State<GenericLoadable<EditState>>,
|
||||
accountViewModel: AccountViewModel,
|
||||
nav: (String) -> Unit,
|
||||
) {
|
||||
Column {
|
||||
val noteEvent = note.event
|
||||
|
||||
if (unPackReply) {
|
||||
val torrentInfo =
|
||||
remember(noteEvent) {
|
||||
if (noteEvent is TorrentCommentEvent) {
|
||||
noteEvent.torrent()
|
||||
} else {
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
torrentInfo?.let {
|
||||
TorrentHeader(
|
||||
torrentHex = it,
|
||||
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,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun TorrentHeader(
|
||||
torrentHex: String,
|
||||
modifier: Modifier,
|
||||
accountViewModel: AccountViewModel,
|
||||
nav: (String) -> Unit,
|
||||
) {
|
||||
LoadNote(baseNoteHex = torrentHex, accountViewModel = accountViewModel) {
|
||||
if (it != null) {
|
||||
ShortTorrentHeader(it, modifier, accountViewModel, nav)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun ShortTorrentHeader(
|
||||
baseNote: Note,
|
||||
modifier: Modifier,
|
||||
accountViewModel: AccountViewModel,
|
||||
nav: (String) -> Unit,
|
||||
) {
|
||||
val channelState by baseNote.live().metadata.observeAsState()
|
||||
val note = channelState?.note ?: return
|
||||
val noteEvent = note.event as? TorrentEvent ?: return
|
||||
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
modifier =
|
||||
modifier.clickable {
|
||||
routeFor(baseNote, accountViewModel.userProfile())?.let { nav(it) }
|
||||
},
|
||||
) {
|
||||
Icons.Outlined.FileOpen
|
||||
|
||||
Icon(
|
||||
imageVector = Icons.Outlined.FileOpen,
|
||||
contentDescription = stringRes(id = R.string.torrent_file),
|
||||
modifier = Modifier.size(20.dp),
|
||||
)
|
||||
|
||||
Text(
|
||||
text = remember(channelState) { noteEvent.title() ?: TorrentEvent.ALT_DESCRIPTION },
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
modifier =
|
||||
Modifier
|
||||
.padding(start = 10.dp)
|
||||
.weight(1f),
|
||||
)
|
||||
|
||||
Text(
|
||||
text = remember(channelState) { countToHumanReadableBytes(noteEvent.totalSizeBytes()) },
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
modifier = Modifier.padding(end = 5.dp),
|
||||
)
|
||||
}
|
||||
}
|
@ -135,6 +135,8 @@ 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.RenderTorrent
|
||||
import com.vitorpamplona.amethyst.ui.note.types.RenderTorrentComment
|
||||
import com.vitorpamplona.amethyst.ui.note.types.VideoDisplay
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.chatrooms.ChannelHeader
|
||||
@ -185,6 +187,8 @@ import com.vitorpamplona.quartz.events.RelaySetEvent
|
||||
import com.vitorpamplona.quartz.events.RepostEvent
|
||||
import com.vitorpamplona.quartz.events.SearchRelayListEvent
|
||||
import com.vitorpamplona.quartz.events.TextNoteModificationEvent
|
||||
import com.vitorpamplona.quartz.events.TorrentCommentEvent
|
||||
import com.vitorpamplona.quartz.events.TorrentEvent
|
||||
import com.vitorpamplona.quartz.events.VideoEvent
|
||||
import com.vitorpamplona.quartz.events.WikiNoteEvent
|
||||
import kotlinx.collections.immutable.toImmutableList
|
||||
@ -607,6 +611,25 @@ private fun FullBleedNoteCompose(
|
||||
accountViewModel,
|
||||
nav,
|
||||
)
|
||||
} else if (noteEvent is TorrentEvent) {
|
||||
RenderTorrent(
|
||||
baseNote,
|
||||
backgroundColor,
|
||||
accountViewModel,
|
||||
nav,
|
||||
)
|
||||
} else if (noteEvent is TorrentCommentEvent) {
|
||||
RenderTorrentComment(
|
||||
baseNote,
|
||||
false,
|
||||
canPreview,
|
||||
quotesLeft = 3,
|
||||
unPackReply = false,
|
||||
backgroundColor,
|
||||
editState,
|
||||
accountViewModel,
|
||||
nav,
|
||||
)
|
||||
} else {
|
||||
RenderTextEvent(
|
||||
baseNote,
|
||||
|
@ -964,4 +964,9 @@
|
||||
<string name="delete_all_drafts_confirmation">Are you sure you want to delete all drafts?</string>
|
||||
<string name="and_more">" … and %1$s more"</string>
|
||||
<string name="stack">Stack:</string>
|
||||
|
||||
<string name="torrent_file">Torrent File</string>
|
||||
<string name="torrent_download">Download</string>
|
||||
<string name="torrent_failure">Failed to open file</string>
|
||||
<string name="torrent_no_apps">No torrent apps installed to open and download the file.</string>
|
||||
</resources>
|
||||
|
@ -117,6 +117,21 @@ class DraftEvent(
|
||||
dTag: String,
|
||||
): String = ATag.assembleATag(KIND, pubKey, dTag)
|
||||
|
||||
fun create(
|
||||
dTag: String,
|
||||
originalNote: TorrentCommentEvent,
|
||||
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: LiveActivitiesChatMessageEvent,
|
||||
@ -125,10 +140,10 @@ class DraftEvent(
|
||||
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)) }
|
||||
originalNote.activity()?.let { tags.add(arrayOf("a", it.toTag(), "", "root")) }
|
||||
originalNote.replyingTo()?.let { tags.add(arrayOf("e", it, "", "reply")) }
|
||||
|
||||
create(dTag, originalNote, emptyList(), signer, createdAt, onReady)
|
||||
create(dTag, originalNote, tags, signer, createdAt, onReady)
|
||||
}
|
||||
|
||||
fun create(
|
||||
|
@ -124,6 +124,8 @@ open class Event(
|
||||
|
||||
override fun taggedUrls() = tags.filter { it.size > 1 && it[0] == "r" }.map { it[1] }
|
||||
|
||||
override fun firstTag(key: String) = tags.firstOrNull { it.size > 1 && it[0] == key }?.let { it[1] }
|
||||
|
||||
override fun firstTagFor(vararg key: String) = tags.firstOrNull { it.size > 1 && it[0] in key }?.let { it[1] }
|
||||
|
||||
override fun firstTaggedUser() = tags.firstOrNull { it.size > 1 && it[0] == "p" }?.let { it[1] }
|
||||
|
@ -142,6 +142,8 @@ class EventFactory {
|
||||
StatusEvent.KIND -> StatusEvent(id, pubKey, createdAt, tags, content, sig)
|
||||
TextNoteEvent.KIND -> TextNoteEvent(id, pubKey, createdAt, tags, content, sig)
|
||||
TextNoteModificationEvent.KIND -> TextNoteModificationEvent(id, pubKey, createdAt, tags, content, sig)
|
||||
TorrentEvent.KIND -> TorrentEvent(id, pubKey, createdAt, tags, content, sig)
|
||||
TorrentCommentEvent.KIND -> TorrentCommentEvent(id, pubKey, createdAt, tags, content, sig)
|
||||
VideoHorizontalEvent.KIND -> VideoHorizontalEvent(id, pubKey, createdAt, tags, content, sig)
|
||||
VideoVerticalEvent.KIND -> VideoVerticalEvent(id, pubKey, createdAt, tags, content, sig)
|
||||
VideoViewEvent.KIND -> VideoViewEvent(id, pubKey, createdAt, tags, content, sig)
|
||||
|
@ -133,6 +133,8 @@ interface EventInterface {
|
||||
|
||||
fun taggedUrls(): List<String>
|
||||
|
||||
fun firstTag(key: String): String?
|
||||
|
||||
fun firstTagFor(vararg key: String): String?
|
||||
|
||||
fun firstTaggedAddress(): ATag?
|
||||
|
@ -0,0 +1,141 @@
|
||||
/**
|
||||
* 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.encoders.Nip92MediaAttachments
|
||||
import com.vitorpamplona.quartz.signers.NostrSigner
|
||||
import com.vitorpamplona.quartz.utils.TimeUtils
|
||||
|
||||
@Immutable
|
||||
class TorrentCommentEvent(
|
||||
id: HexKey,
|
||||
pubKey: HexKey,
|
||||
createdAt: Long,
|
||||
tags: Array<Array<String>>,
|
||||
content: String,
|
||||
sig: HexKey,
|
||||
) : BaseTextNoteEvent(id, pubKey, createdAt, KIND, tags, content, sig) {
|
||||
private fun innerTorrent() =
|
||||
tags.firstOrNull { it.size > 3 && it[0] == "e" && it[3] == "root" }
|
||||
?: tags.firstOrNull { it.size > 1 && it[0] == "e" }
|
||||
|
||||
fun torrent() = innerTorrent()?.getOrNull(1)
|
||||
|
||||
companion object {
|
||||
const val KIND = 2004
|
||||
const val ALT = "Comment for a Torrent file"
|
||||
|
||||
fun create(
|
||||
message: String,
|
||||
torrent: HexKey,
|
||||
replyTos: List<String>? = null,
|
||||
mentions: List<String>? = null,
|
||||
addresses: List<ATag>? = null,
|
||||
extraTags: List<String>? = null,
|
||||
zapReceiver: List<ZapSplitSetup>? = null,
|
||||
signer: NostrSigner,
|
||||
createdAt: Long = TimeUtils.now(),
|
||||
markAsSensitive: Boolean,
|
||||
replyingTo: String? = null,
|
||||
directMentions: Set<HexKey> = emptySet(),
|
||||
zapRaiserAmount: Long?,
|
||||
geohash: String? = null,
|
||||
nip94attachments: List<FileHeaderEvent>? = null,
|
||||
forkedFrom: Event? = null,
|
||||
isDraft: Boolean,
|
||||
onReady: (TorrentCommentEvent) -> Unit,
|
||||
) {
|
||||
val content = message
|
||||
|
||||
val tags = mutableListOf<Array<String>>()
|
||||
replyTos?.let {
|
||||
tags.addAll(
|
||||
it.positionalMarkedTags(
|
||||
tagName = "e",
|
||||
root = torrent,
|
||||
replyingTo = replyingTo,
|
||||
directMentions = directMentions,
|
||||
forkedFrom = forkedFrom?.id,
|
||||
),
|
||||
)
|
||||
}
|
||||
mentions?.forEach {
|
||||
if (it in directMentions) {
|
||||
tags.add(arrayOf("p", it, "", "mention"))
|
||||
} else {
|
||||
tags.add(arrayOf("p", it))
|
||||
}
|
||||
}
|
||||
replyTos?.forEach {
|
||||
if (it in directMentions) {
|
||||
tags.add(arrayOf("q", it))
|
||||
}
|
||||
}
|
||||
addresses
|
||||
?.map { it.toTag() }
|
||||
?.let {
|
||||
tags.addAll(
|
||||
it.positionalMarkedTags(
|
||||
tagName = "a",
|
||||
root = torrent,
|
||||
replyingTo = replyingTo,
|
||||
directMentions = directMentions,
|
||||
forkedFrom = (forkedFrom as? AddressableEvent)?.address()?.toTag(),
|
||||
),
|
||||
)
|
||||
}
|
||||
findHashtags(message).forEach {
|
||||
val lowercaseTag = it.lowercase()
|
||||
tags.add(arrayOf("t", it))
|
||||
if (it != lowercaseTag) {
|
||||
tags.add(arrayOf("t", it.lowercase()))
|
||||
}
|
||||
}
|
||||
extraTags?.forEach { tags.add(arrayOf("t", it)) }
|
||||
zapReceiver?.forEach {
|
||||
tags.add(arrayOf("zap", it.lnAddressOrPubKeyHex, it.relay ?: "", it.weight.toString()))
|
||||
}
|
||||
findURLs(message).forEach { tags.add(arrayOf("r", it)) }
|
||||
if (markAsSensitive) {
|
||||
tags.add(arrayOf("content-warning", ""))
|
||||
}
|
||||
zapRaiserAmount?.let { tags.add(arrayOf("zapraiser", "$it")) }
|
||||
geohash?.let { tags.addAll(geohashMipMap(it)) }
|
||||
nip94attachments?.let {
|
||||
it.forEach {
|
||||
Nip92MediaAttachments().convertFromFileHeader(it)?.let {
|
||||
tags.add(it)
|
||||
}
|
||||
}
|
||||
}
|
||||
tags.add(arrayOf("alt", ALT))
|
||||
|
||||
if (isDraft) {
|
||||
signer.assembleRumor(createdAt, KIND, tags.toTypedArray(), content, onReady)
|
||||
} else {
|
||||
signer.sign(createdAt, KIND, tags.toTypedArray(), content, onReady)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,127 @@
|
||||
/**
|
||||
* 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 android.net.Uri
|
||||
import androidx.compose.runtime.Immutable
|
||||
import com.vitorpamplona.quartz.encoders.HexKey
|
||||
import com.vitorpamplona.quartz.signers.NostrSigner
|
||||
import com.vitorpamplona.quartz.utils.TimeUtils
|
||||
|
||||
@Immutable
|
||||
class TorrentEvent(
|
||||
id: HexKey,
|
||||
pubKey: HexKey,
|
||||
createdAt: Long,
|
||||
tags: Array<Array<String>>,
|
||||
content: String,
|
||||
sig: HexKey,
|
||||
) : Event(id, pubKey, createdAt, KIND, tags, content, sig) {
|
||||
fun title() = firstTag("title")
|
||||
|
||||
fun btih() = firstTag("btih")
|
||||
|
||||
fun x() = firstTag("x")
|
||||
|
||||
fun trackers() = tags.filter { it.size > 1 && it[0] == "tracker" }.map { it[1] }
|
||||
|
||||
fun files() = tags.filter { it.size > 1 && it[0] == "file" }.map { TorrentFile(it[1], it.getOrNull(2)?.toLongOrNull()) }
|
||||
|
||||
fun toMagnetLink(): String {
|
||||
val builder = Uri.Builder()
|
||||
builder
|
||||
.scheme("magnet")
|
||||
.appendQueryParameter("xt", "urn:btih:${btih()}")
|
||||
.appendQueryParameter("dn", title())
|
||||
|
||||
trackers().ifEmpty { DEFAULT_TRACKERS }.forEach {
|
||||
builder.appendQueryParameter("tr", it)
|
||||
}
|
||||
|
||||
return builder.build().toString()
|
||||
}
|
||||
|
||||
fun totalSizeBytes(): Long = tags.filter { it.size > 1 && it[0] == "file" }.sumOf { it.getOrNull(2)?.toLongOrNull() ?: 0L }
|
||||
|
||||
companion object {
|
||||
const val KIND = 2003
|
||||
const val ALT_DESCRIPTION = "A torrent file"
|
||||
|
||||
val DEFAULT_TRACKERS =
|
||||
listOf(
|
||||
"http://tracker.loadpeers.org:8080/xvRKfvAlnfuf5EfxTT5T0KIVPtbqAHnX/announce",
|
||||
"udp://tracker.coppersurfer.tk:6969/announce",
|
||||
"udp://tracker.openbittorrent.com:6969/announce",
|
||||
"udp://open.stealth.si:80/announce",
|
||||
"udp://tracker.torrent.eu.org:451/announce",
|
||||
"udp://tracker.opentrackr.org:1337",
|
||||
)
|
||||
|
||||
fun create(
|
||||
title: String,
|
||||
btih: String,
|
||||
files: List<TorrentFile>,
|
||||
description: String? = null,
|
||||
x: String? = null,
|
||||
trackers: List<String>? = null,
|
||||
alt: String? = null,
|
||||
sensitiveContent: Boolean? = null,
|
||||
signer: NostrSigner,
|
||||
createdAt: Long = TimeUtils.now(),
|
||||
onReady: (TorrentEvent) -> Unit,
|
||||
) {
|
||||
val tags =
|
||||
listOfNotNull(
|
||||
arrayOf("title", title),
|
||||
arrayOf("btih", btih),
|
||||
x?.let { arrayOf("x", it) },
|
||||
alt?.let { arrayOf("alt", it) } ?: arrayOf("alt", ALT_DESCRIPTION),
|
||||
sensitiveContent?.let {
|
||||
if (it) {
|
||||
arrayOf("content-warning", "")
|
||||
} else {
|
||||
null
|
||||
}
|
||||
},
|
||||
) +
|
||||
files.map {
|
||||
if (it.bytes != null) {
|
||||
arrayOf(it.fileName, it.bytes.toString())
|
||||
} else {
|
||||
arrayOf(it.fileName)
|
||||
}
|
||||
} +
|
||||
(
|
||||
trackers?.map {
|
||||
arrayOf(it)
|
||||
} ?: emptyList()
|
||||
)
|
||||
|
||||
val content = description ?: ""
|
||||
signer.sign(createdAt, KIND, tags.toTypedArray(), content, onReady)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class TorrentFile(
|
||||
val fileName: String,
|
||||
val bytes: Long?,
|
||||
)
|
Loading…
x
Reference in New Issue
Block a user