Adds support for NIP-35 torrents and their comments
This commit is contained in:
@@ -102,6 +102,7 @@ import com.vitorpamplona.quartz.events.SearchRelayListEvent
|
|||||||
import com.vitorpamplona.quartz.events.StatusEvent
|
import com.vitorpamplona.quartz.events.StatusEvent
|
||||||
import com.vitorpamplona.quartz.events.TextNoteEvent
|
import com.vitorpamplona.quartz.events.TextNoteEvent
|
||||||
import com.vitorpamplona.quartz.events.TextNoteModificationEvent
|
import com.vitorpamplona.quartz.events.TextNoteModificationEvent
|
||||||
|
import com.vitorpamplona.quartz.events.TorrentCommentEvent
|
||||||
import com.vitorpamplona.quartz.events.WrappedEvent
|
import com.vitorpamplona.quartz.events.WrappedEvent
|
||||||
import com.vitorpamplona.quartz.events.ZapSplitSetup
|
import com.vitorpamplona.quartz.events.ZapSplitSetup
|
||||||
import com.vitorpamplona.quartz.signers.NostrSigner
|
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) {
|
fun deleteDraft(draftTag: String) {
|
||||||
val key = DraftEvent.createAddressTag(userProfile().pubkeyHex, draftTag)
|
val key = DraftEvent.createAddressTag(userProfile().pubkeyHex, draftTag)
|
||||||
LocalCache.getAddressableNoteIfExists(key)?.let { note ->
|
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.StatusEvent
|
||||||
import com.vitorpamplona.quartz.events.TextNoteEvent
|
import com.vitorpamplona.quartz.events.TextNoteEvent
|
||||||
import com.vitorpamplona.quartz.events.TextNoteModificationEvent
|
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.VideoHorizontalEvent
|
||||||
import com.vitorpamplona.quartz.events.VideoVerticalEvent
|
import com.vitorpamplona.quartz.events.VideoVerticalEvent
|
||||||
import com.vitorpamplona.quartz.events.WikiNoteEvent
|
import com.vitorpamplona.quartz.events.WikiNoteEvent
|
||||||
@@ -463,6 +465,61 @@ object LocalCache {
|
|||||||
refreshObservers(note)
|
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(
|
fun consume(
|
||||||
event: NIP90ContentDiscoveryResponseEvent,
|
event: NIP90ContentDiscoveryResponseEvent,
|
||||||
relay: Relay? = null,
|
relay: Relay? = null,
|
||||||
@@ -795,6 +852,8 @@ object LocalCache {
|
|||||||
.tagsWithoutCitations()
|
.tagsWithoutCitations()
|
||||||
.filter { it != event.activity()?.toTag() }
|
.filter { it != event.activity()?.toTag() }
|
||||||
.mapNotNull { checkGetOrCreateNote(it) }
|
.mapNotNull { checkGetOrCreateNote(it) }
|
||||||
|
is TorrentCommentEvent ->
|
||||||
|
event.tagsWithoutCitations().mapNotNull { checkGetOrCreateNote(it) }
|
||||||
|
|
||||||
is DraftEvent -> {
|
is DraftEvent -> {
|
||||||
event.mapTaggedEvent { checkGetOrCreateNote(it) } + event.mapTaggedAddress { checkGetOrCreateAddressableNote(it) }
|
event.mapTaggedEvent { checkGetOrCreateNote(it) } + event.mapTaggedAddress { checkGetOrCreateAddressableNote(it) }
|
||||||
@@ -1303,6 +1362,10 @@ object LocalCache {
|
|||||||
getChannelIfExists(it.toTag())?.removeNote(deleteNote)
|
getChannelIfExists(it.toTag())?.removeNote(deleteNote)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
(deletedEvent as? TorrentCommentEvent)?.torrent()?.let {
|
||||||
|
getNoteIfExists(it)?.removeReply(deleteNote)
|
||||||
|
}
|
||||||
|
|
||||||
if (deletedEvent is PrivateDmEvent) {
|
if (deletedEvent is PrivateDmEvent) {
|
||||||
val author = deleteNote.author
|
val author = deleteNote.author
|
||||||
val recipient =
|
val recipient =
|
||||||
@@ -2664,6 +2727,8 @@ object LocalCache {
|
|||||||
is StatusEvent -> consume(event, relay)
|
is StatusEvent -> consume(event, relay)
|
||||||
is TextNoteEvent -> consume(event, relay)
|
is TextNoteEvent -> consume(event, relay)
|
||||||
is TextNoteModificationEvent -> consume(event, relay)
|
is TextNoteModificationEvent -> consume(event, relay)
|
||||||
|
is TorrentEvent -> consume(event, relay)
|
||||||
|
is TorrentCommentEvent -> consume(event, relay)
|
||||||
is VideoHorizontalEvent -> consume(event, relay)
|
is VideoHorizontalEvent -> consume(event, relay)
|
||||||
is VideoVerticalEvent -> consume(event, relay)
|
is VideoVerticalEvent -> consume(event, relay)
|
||||||
is WikiNoteEvent -> consume(event, relay)
|
is WikiNoteEvent -> consume(event, relay)
|
||||||
|
|||||||
+2
@@ -42,6 +42,7 @@ import com.vitorpamplona.quartz.events.ReportEvent
|
|||||||
import com.vitorpamplona.quartz.events.RepostEvent
|
import com.vitorpamplona.quartz.events.RepostEvent
|
||||||
import com.vitorpamplona.quartz.events.TextNoteEvent
|
import com.vitorpamplona.quartz.events.TextNoteEvent
|
||||||
import com.vitorpamplona.quartz.events.TextNoteModificationEvent
|
import com.vitorpamplona.quartz.events.TextNoteModificationEvent
|
||||||
|
import com.vitorpamplona.quartz.events.TorrentCommentEvent
|
||||||
|
|
||||||
object NostrSingleEventDataSource : AmethystNostrDataSource("SingleEventFeed") {
|
object NostrSingleEventDataSource : AmethystNostrDataSource("SingleEventFeed") {
|
||||||
private var nextEventsToWatch = setOf<Note>()
|
private var nextEventsToWatch = setOf<Note>()
|
||||||
@@ -179,6 +180,7 @@ object NostrSingleEventDataSource : AmethystNostrDataSource("SingleEventFeed") {
|
|||||||
DeletionEvent.KIND,
|
DeletionEvent.KIND,
|
||||||
NIP90ContentDiscoveryResponseEvent.KIND,
|
NIP90ContentDiscoveryResponseEvent.KIND,
|
||||||
NIP90StatusEvent.KIND,
|
NIP90StatusEvent.KIND,
|
||||||
|
TorrentCommentEvent.KIND,
|
||||||
),
|
),
|
||||||
tags = mapOf("e" to it.map { it.idHex }),
|
tags = mapOf("e" to it.map { it.idHex }),
|
||||||
since = findMinimumEOSEs(it),
|
since = findMinimumEOSEs(it),
|
||||||
|
|||||||
+20
@@ -42,6 +42,8 @@ import com.vitorpamplona.quartz.events.PollNoteEvent
|
|||||||
import com.vitorpamplona.quartz.events.ProfileGalleryEntryEvent
|
import com.vitorpamplona.quartz.events.ProfileGalleryEntryEvent
|
||||||
import com.vitorpamplona.quartz.events.RepostEvent
|
import com.vitorpamplona.quartz.events.RepostEvent
|
||||||
import com.vitorpamplona.quartz.events.TextNoteEvent
|
import com.vitorpamplona.quartz.events.TextNoteEvent
|
||||||
|
import com.vitorpamplona.quartz.events.TorrentCommentEvent
|
||||||
|
import com.vitorpamplona.quartz.events.TorrentEvent
|
||||||
import com.vitorpamplona.quartz.events.WikiNoteEvent
|
import com.vitorpamplona.quartz.events.WikiNoteEvent
|
||||||
|
|
||||||
object NostrUserProfileDataSource : AmethystNostrDataSource("UserProfileFeed") {
|
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() =
|
fun createUserReceivedZapsFilter() =
|
||||||
user?.let {
|
user?.let {
|
||||||
TypedFilter(
|
TypedFilter(
|
||||||
@@ -188,6 +207,7 @@ object NostrUserProfileDataSource : AmethystNostrDataSource("UserProfileFeed") {
|
|||||||
listOfNotNull(
|
listOfNotNull(
|
||||||
createUserInfoFilter(),
|
createUserInfoFilter(),
|
||||||
createUserPostsFilter(),
|
createUserPostsFilter(),
|
||||||
|
createUserPostsFilter2(),
|
||||||
createProfileGalleryFilter(),
|
createProfileGalleryFilter(),
|
||||||
createFollowFilter(),
|
createFollowFilter(),
|
||||||
createFollowersFilter(),
|
createFollowersFilter(),
|
||||||
|
|||||||
@@ -69,6 +69,8 @@ import com.vitorpamplona.quartz.events.GitIssueEvent
|
|||||||
import com.vitorpamplona.quartz.events.Price
|
import com.vitorpamplona.quartz.events.Price
|
||||||
import com.vitorpamplona.quartz.events.PrivateDmEvent
|
import com.vitorpamplona.quartz.events.PrivateDmEvent
|
||||||
import com.vitorpamplona.quartz.events.TextNoteEvent
|
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.ZapSplitSetup
|
||||||
import com.vitorpamplona.quartz.events.findURLs
|
import com.vitorpamplona.quartz.events.findURLs
|
||||||
import kotlinx.coroutines.CancellationException
|
import kotlinx.coroutines.CancellationException
|
||||||
@@ -681,6 +683,74 @@ open class NewPostViewModel : ViewModel() {
|
|||||||
nip94attachments = usedAttachments,
|
nip94attachments = usedAttachments,
|
||||||
draftTag = localDraft,
|
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 {
|
} else {
|
||||||
if (wantsPoll) {
|
if (wantsPoll) {
|
||||||
account?.sendPoll(
|
account?.sendPoll(
|
||||||
|
|||||||
+3
-1
@@ -28,6 +28,7 @@ import com.vitorpamplona.quartz.events.ChannelMessageEvent
|
|||||||
import com.vitorpamplona.quartz.events.LiveActivitiesChatMessageEvent
|
import com.vitorpamplona.quartz.events.LiveActivitiesChatMessageEvent
|
||||||
import com.vitorpamplona.quartz.events.PollNoteEvent
|
import com.vitorpamplona.quartz.events.PollNoteEvent
|
||||||
import com.vitorpamplona.quartz.events.TextNoteEvent
|
import com.vitorpamplona.quartz.events.TextNoteEvent
|
||||||
|
import com.vitorpamplona.quartz.events.TorrentCommentEvent
|
||||||
|
|
||||||
class UserProfileConversationsFeedFilter(
|
class UserProfileConversationsFeedFilter(
|
||||||
val user: User,
|
val user: User,
|
||||||
@@ -59,7 +60,8 @@ class UserProfileConversationsFeedFilter(
|
|||||||
it.event is TextNoteEvent ||
|
it.event is TextNoteEvent ||
|
||||||
it.event is PollNoteEvent ||
|
it.event is PollNoteEvent ||
|
||||||
it.event is ChannelMessageEvent ||
|
it.event is ChannelMessageEvent ||
|
||||||
it.event is LiveActivitiesChatMessageEvent
|
it.event is LiveActivitiesChatMessageEvent ||
|
||||||
|
it.event is TorrentCommentEvent
|
||||||
) &&
|
) &&
|
||||||
!it.isNewThread() &&
|
!it.isNewThread() &&
|
||||||
account.isAcceptable(it)
|
account.isAcceptable(it)
|
||||||
|
|||||||
+3
-1
@@ -35,6 +35,7 @@ import com.vitorpamplona.quartz.events.LongTextNoteEvent
|
|||||||
import com.vitorpamplona.quartz.events.PollNoteEvent
|
import com.vitorpamplona.quartz.events.PollNoteEvent
|
||||||
import com.vitorpamplona.quartz.events.RepostEvent
|
import com.vitorpamplona.quartz.events.RepostEvent
|
||||||
import com.vitorpamplona.quartz.events.TextNoteEvent
|
import com.vitorpamplona.quartz.events.TextNoteEvent
|
||||||
|
import com.vitorpamplona.quartz.events.TorrentEvent
|
||||||
import com.vitorpamplona.quartz.events.WikiNoteEvent
|
import com.vitorpamplona.quartz.events.WikiNoteEvent
|
||||||
|
|
||||||
class UserProfileNewThreadFeedFilter(
|
class UserProfileNewThreadFeedFilter(
|
||||||
@@ -73,7 +74,8 @@ class UserProfileNewThreadFeedFilter(
|
|||||||
it.event is PollNoteEvent ||
|
it.event is PollNoteEvent ||
|
||||||
it.event is HighlightEvent ||
|
it.event is HighlightEvent ||
|
||||||
it.event is AudioTrackEvent ||
|
it.event is AudioTrackEvent ||
|
||||||
it.event is AudioHeaderEvent
|
it.event is AudioHeaderEvent ||
|
||||||
|
it.event is TorrentEvent
|
||||||
) &&
|
) &&
|
||||||
it.isNewThread() &&
|
it.isNewThread() &&
|
||||||
account.isAcceptable(it)
|
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.RenderReport
|
||||||
import com.vitorpamplona.amethyst.ui.note.types.RenderTextEvent
|
import com.vitorpamplona.amethyst.ui.note.types.RenderTextEvent
|
||||||
import com.vitorpamplona.amethyst.ui.note.types.RenderTextModificationEvent
|
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.RenderWikiContent
|
||||||
import com.vitorpamplona.amethyst.ui.note.types.VideoDisplay
|
import com.vitorpamplona.amethyst.ui.note.types.VideoDisplay
|
||||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
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.SearchRelayListEvent
|
||||||
import com.vitorpamplona.quartz.events.TextNoteEvent
|
import com.vitorpamplona.quartz.events.TextNoteEvent
|
||||||
import com.vitorpamplona.quartz.events.TextNoteModificationEvent
|
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.VideoEvent
|
||||||
import com.vitorpamplona.quartz.events.VideoHorizontalEvent
|
import com.vitorpamplona.quartz.events.VideoHorizontalEvent
|
||||||
import com.vitorpamplona.quartz.events.VideoVerticalEvent
|
import com.vitorpamplona.quartz.events.VideoVerticalEvent
|
||||||
@@ -754,6 +758,27 @@ private fun RenderNoteRow(
|
|||||||
accountViewModel,
|
accountViewModel,
|
||||||
nav,
|
nav,
|
||||||
)
|
)
|
||||||
|
is TorrentEvent ->
|
||||||
|
RenderTorrent(
|
||||||
|
baseNote,
|
||||||
|
backgroundColor,
|
||||||
|
accountViewModel,
|
||||||
|
nav,
|
||||||
|
)
|
||||||
|
|
||||||
|
is TorrentCommentEvent ->
|
||||||
|
RenderTorrentComment(
|
||||||
|
baseNote,
|
||||||
|
makeItShort,
|
||||||
|
canPreview,
|
||||||
|
quotesLeft,
|
||||||
|
unPackReply,
|
||||||
|
backgroundColor,
|
||||||
|
editState,
|
||||||
|
accountViewModel,
|
||||||
|
nav,
|
||||||
|
)
|
||||||
|
|
||||||
else -> {
|
else -> {
|
||||||
RenderTextEvent(
|
RenderTextEvent(
|
||||||
baseNote,
|
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.RenderPrivateMessage
|
||||||
import com.vitorpamplona.amethyst.ui.note.types.RenderTextEvent
|
import com.vitorpamplona.amethyst.ui.note.types.RenderTextEvent
|
||||||
import com.vitorpamplona.amethyst.ui.note.types.RenderTextModificationEvent
|
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.note.types.VideoDisplay
|
||||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
||||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.chatrooms.ChannelHeader
|
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.RepostEvent
|
||||||
import com.vitorpamplona.quartz.events.SearchRelayListEvent
|
import com.vitorpamplona.quartz.events.SearchRelayListEvent
|
||||||
import com.vitorpamplona.quartz.events.TextNoteModificationEvent
|
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.VideoEvent
|
||||||
import com.vitorpamplona.quartz.events.WikiNoteEvent
|
import com.vitorpamplona.quartz.events.WikiNoteEvent
|
||||||
import kotlinx.collections.immutable.toImmutableList
|
import kotlinx.collections.immutable.toImmutableList
|
||||||
@@ -607,6 +611,25 @@ private fun FullBleedNoteCompose(
|
|||||||
accountViewModel,
|
accountViewModel,
|
||||||
nav,
|
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 {
|
} else {
|
||||||
RenderTextEvent(
|
RenderTextEvent(
|
||||||
baseNote,
|
baseNote,
|
||||||
|
|||||||
@@ -964,4 +964,9 @@
|
|||||||
<string name="delete_all_drafts_confirmation">Are you sure you want to delete all drafts?</string>
|
<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="and_more">" … and %1$s more"</string>
|
||||||
<string name="stack">Stack:</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>
|
</resources>
|
||||||
|
|||||||
@@ -117,6 +117,21 @@ class DraftEvent(
|
|||||||
dTag: String,
|
dTag: String,
|
||||||
): String = ATag.assembleATag(KIND, pubKey, dTag)
|
): 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(
|
fun create(
|
||||||
dTag: String,
|
dTag: String,
|
||||||
originalNote: LiveActivitiesChatMessageEvent,
|
originalNote: LiveActivitiesChatMessageEvent,
|
||||||
@@ -125,10 +140,10 @@ class DraftEvent(
|
|||||||
onReady: (DraftEvent) -> Unit,
|
onReady: (DraftEvent) -> Unit,
|
||||||
) {
|
) {
|
||||||
val tags = mutableListOf<Array<String>>()
|
val tags = mutableListOf<Array<String>>()
|
||||||
originalNote.activity()?.let { tags.add(arrayOf("a", it.toTag())) }
|
originalNote.activity()?.let { tags.add(arrayOf("a", it.toTag(), "", "root")) }
|
||||||
originalNote.replyingTo()?.let { tags.add(arrayOf("e", it)) }
|
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(
|
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 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 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] }
|
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)
|
StatusEvent.KIND -> StatusEvent(id, pubKey, createdAt, tags, content, sig)
|
||||||
TextNoteEvent.KIND -> TextNoteEvent(id, pubKey, createdAt, tags, content, sig)
|
TextNoteEvent.KIND -> TextNoteEvent(id, pubKey, createdAt, tags, content, sig)
|
||||||
TextNoteModificationEvent.KIND -> TextNoteModificationEvent(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)
|
VideoHorizontalEvent.KIND -> VideoHorizontalEvent(id, pubKey, createdAt, tags, content, sig)
|
||||||
VideoVerticalEvent.KIND -> VideoVerticalEvent(id, pubKey, createdAt, tags, content, sig)
|
VideoVerticalEvent.KIND -> VideoVerticalEvent(id, pubKey, createdAt, tags, content, sig)
|
||||||
VideoViewEvent.KIND -> VideoViewEvent(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 taggedUrls(): List<String>
|
||||||
|
|
||||||
|
fun firstTag(key: String): String?
|
||||||
|
|
||||||
fun firstTagFor(vararg key: String): String?
|
fun firstTagFor(vararg key: String): String?
|
||||||
|
|
||||||
fun firstTaggedAddress(): ATag?
|
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?,
|
||||||
|
)
|
||||||
Reference in New Issue
Block a user