diff --git a/app/src/main/java/com/vitorpamplona/amethyst/model/LocalCache.kt b/app/src/main/java/com/vitorpamplona/amethyst/model/LocalCache.kt index f68689df6..7d472f062 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/model/LocalCache.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/model/LocalCache.kt @@ -14,27 +14,21 @@ import com.vitorpamplona.amethyst.service.model.ChannelHideMessageEvent import com.vitorpamplona.amethyst.service.model.ChannelMessageEvent import com.vitorpamplona.amethyst.service.model.ChannelMetadataEvent import com.vitorpamplona.amethyst.service.model.ChannelMuteUserEvent -import com.vitorpamplona.amethyst.service.model.LnZapEvent -import com.vitorpamplona.amethyst.service.model.LnZapRequestEvent -import com.vitorpamplona.amethyst.service.model.LongTextNoteEvent -import com.vitorpamplona.amethyst.service.model.ReactionEvent -import com.vitorpamplona.amethyst.service.model.ReportEvent -import com.vitorpamplona.amethyst.service.model.RepostEvent import com.vitorpamplona.amethyst.service.model.ContactListEvent import com.vitorpamplona.amethyst.service.model.DeletionEvent import com.vitorpamplona.amethyst.service.model.Event +import com.vitorpamplona.amethyst.service.model.LnZapEvent +import com.vitorpamplona.amethyst.service.model.LnZapRequestEvent +import com.vitorpamplona.amethyst.service.model.LongTextNoteEvent import com.vitorpamplona.amethyst.service.model.MetadataEvent import com.vitorpamplona.amethyst.service.model.PrivateDmEvent +import com.vitorpamplona.amethyst.service.model.ReactionEvent import com.vitorpamplona.amethyst.service.model.RecommendRelayEvent +import com.vitorpamplona.amethyst.service.model.ReportEvent +import com.vitorpamplona.amethyst.service.model.RepostEvent import com.vitorpamplona.amethyst.service.model.TextNoteEvent import com.vitorpamplona.amethyst.service.relays.Relay import fr.acinq.secp256k1.Hex -import java.io.ByteArrayInputStream -import java.time.Instant -import java.time.ZoneId -import java.time.format.DateTimeFormatter -import java.util.concurrent.ConcurrentHashMap -import java.util.concurrent.atomic.AtomicBoolean import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Job @@ -43,893 +37,892 @@ import kotlinx.coroutines.delay import kotlinx.coroutines.launch import kotlinx.coroutines.withContext import nostr.postr.toNpub - +import java.io.ByteArrayInputStream +import java.time.Instant +import java.time.ZoneId +import java.time.format.DateTimeFormatter +import java.util.concurrent.ConcurrentHashMap +import java.util.concurrent.atomic.AtomicBoolean object LocalCache { - val metadataParser = jacksonObjectMapper() - .configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false) - .readerFor(UserMetadata::class.java) - - val antiSpam = AntiSpamFilter() - - val users = ConcurrentHashMap() - val notes = ConcurrentHashMap() - val channels = ConcurrentHashMap() - val addressables = ConcurrentHashMap() - - fun checkGetOrCreateUser(key: String): User? { - return try { - val checkHex = Hex.decode(key).toNpub() // Checks if this is a valid Hex - getOrCreateUser(key) - } catch (e: IllegalArgumentException) { - Log.e("LocalCache", "Invalid Key to create user: $key", e) - null - } - } - - @Synchronized - fun getOrCreateUser(key: HexKey): User { - return users[key] ?: run { - val answer = User(key) - users.put(key, answer) - answer - } - } - - fun checkGetOrCreateNote(key: String): Note? { - if (ATag.isATag(key)) { - return checkGetOrCreateAddressableNote(key) - } - return try { - val checkHex = Hex.decode(key).toNote() // Checks if this is a valid Hex - getOrCreateNote(key) - } catch (e: IllegalArgumentException) { - Log.e("LocalCache", "Invalid Key to create note: $key", e) - null - } - } - - @Synchronized - fun getOrCreateNote(idHex: String): Note { - return notes[idHex] ?: run { - val answer = Note(idHex) - notes.put(idHex, answer) - answer - } - } - - fun checkGetOrCreateChannel(key: String): Channel? { - return try { - val checkHex = Hex.decode(key).toNote() // Checks if this is a valid Hex - getOrCreateChannel(key) - } catch (e: IllegalArgumentException) { - Log.e("LocalCache", "Invalid Key to create channel: $key", e) - null - } - } - - - @Synchronized - fun getOrCreateChannel(key: String): Channel { - return channels[key] ?: run { - val answer = Channel(key) - channels.put(key, answer) - answer - } - } - - fun checkGetOrCreateAddressableNote(key: String): AddressableNote? { - return try { - val addr = ATag.parse(key, null) // relay doesn't matter for the index. - if (addr != null) - getOrCreateAddressableNote(addr) - else - null - } catch (e: IllegalArgumentException) { - Log.e("LocalCache", "Invalid Key to create channel: $key", e) - null - } - } - - @Synchronized - fun getOrCreateAddressableNote(key: ATag): AddressableNote { - // we can't use naddr here because naddr might include relay info and - // the preferred relay should not be part of the index. - return addressables[key.toTag()] ?: run { - val answer = AddressableNote(key) - answer.author = checkGetOrCreateUser(key.pubKeyHex) - addressables.put(key.toTag(), answer) - answer - } - } - - fun consume(event: MetadataEvent) { - // new event - val oldUser = getOrCreateUser(event.pubKey) - if (oldUser.info == null || event.createdAt > oldUser.info!!.updatedMetadataAt) { - val newUser = try { - metadataParser.readValue( - ByteArrayInputStream(event.content.toByteArray(Charsets.UTF_8)), - UserMetadata::class.java - ) - } catch (e: Exception) { - e.printStackTrace() - Log.w("MT", "Content Parse Error ${e.localizedMessage} ${event.content}") - return - } - - oldUser.updateUserInfo(newUser, event) - //Log.d("MT", "New User Metadata ${oldUser.pubkeyDisplayHex} ${oldUser.toBestDisplayName()}") - } else { - //Log.d("MT","Relay sent a previous Metadata Event ${oldUser.toBestDisplayName()} ${formattedDateTime(event.createdAt)} > ${formattedDateTime(oldUser.updatedAt)}") - } - } - - fun formattedDateTime(timestamp: Long): String { - return Instant.ofEpochSecond(timestamp).atZone(ZoneId.systemDefault()) - .format(DateTimeFormatter.ofPattern("uuuu MMM d hh:mm a")) - } - - - fun consume(event: TextNoteEvent, relay: Relay? = null) { - 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?.let { - it.spamCounter++ - } - return - } - - val mentions = event.mentions().mapNotNull { checkGetOrCreateUser(it) } - val replyTo = replyToWithoutCitations(event).mapNotNull { checkGetOrCreateNote(it) } + - event.taggedAddresses().mapNotNull { getOrCreateAddressableNote(it) } - - note.loadEvent(event, author, mentions, replyTo) - - //Log.d("TN", "New Note (${notes.size},${users.size}) ${note.author?.toBestDisplayName()} ${note.event?.content()?.take(100)} ${formattedDateTime(event.createdAt)}") - - // Prepares user's profile view. - author.addNote(note) - - // Adds notifications to users. - mentions.forEach { - it.addTaggedPost(note) - } - replyTo.forEach { - it.author?.addTaggedPost(note) - } - - // Counts the replies - replyTo.forEach { - it.addReply(note) - } - - refreshObservers() - } - - fun consume(event: LongTextNoteEvent, relay: Relay?) { - val note = getOrCreateAddressableNote(event.address()) - val author = getOrCreateUser(event.pubKey) - - if (relay != null) { - author.addRelayBeingUsed(relay, event.createdAt) - note.addRelay(relay) - } - - // Already processed this event. - if (note.event?.id() == event.id()) return - - if (antiSpam.isSpam(event)) { - relay?.let { - it.spamCounter++ - } - return - } - - val mentions = event.mentions().mapNotNull { checkGetOrCreateUser(it) } - val replyTo = replyToWithoutCitations(event).mapNotNull { checkGetOrCreateNote(it) } - - if (event.createdAt > (note.createdAt() ?: 0)) { - note.loadEvent(event, author, mentions, replyTo) - - author.addNote(note) - - // Adds notifications to users. - mentions.forEach { - it.addTaggedPost(note) - } - replyTo.forEach { - it.author?.addTaggedPost(note) - } - - refreshObservers() - } - } - - fun consume(event: BadgeDefinitionEvent) { - val note = getOrCreateAddressableNote(event.address()) - val author = getOrCreateUser(event.pubKey) - - // Already processed this event. - if (note.event?.id() == event.id()) return - - if (event.createdAt > (note.createdAt() ?: 0)) { - note.loadEvent(event, author, emptyList(), emptyList()) - - refreshObservers() - } - } - - fun consume(event: BadgeProfilesEvent) { - val note = getOrCreateAddressableNote(event.address()) - val author = getOrCreateUser(event.pubKey) - - // Already processed this event. - if (note.event?.id() == event.id()) return - - val replyTo = event.badgeAwardEvents().mapNotNull { checkGetOrCreateNote(it) } + - event.badgeAwardDefinitions().mapNotNull { getOrCreateAddressableNote(it) } - - if (event.createdAt > (note.createdAt() ?: 0)) { - note.loadEvent(event, author, emptyList(), replyTo) - - author.updateAcceptedBadges(note) - - refreshObservers() - } - } - - fun consume(event: BadgeAwardEvent) { - val note = getOrCreateNote(event.id) - - // Already processed this event. - if (note.event != null) return - - //Log.d("TN", "New Boost (${notes.size},${users.size}) ${note.author?.toBestDisplayName()} ${formattedDateTime(event.createdAt)}") - - val author = getOrCreateUser(event.pubKey) - val awardees = event.awardees().mapNotNull { checkGetOrCreateUser(it) } - val awardDefinition = event.awardDefinition().map { getOrCreateAddressableNote(it) } - - note.loadEvent(event, author, awardees, awardDefinition) - - // Adds notifications to users. - awardees.forEach { - it.addTaggedPost(note) - } - - // Counts the replies - awardees.forEach { - it.addBadgeAward(note) - } - - // Replies of an Badge Definition are Award Events - awardDefinition.forEach { - it.addReply(note) - } - - refreshObservers() - } - - private fun findCitations(event: Event): Set { - var citations = mutableSetOf() - // Removes citations from replies: - val matcher = tagSearch.matcher(event.content) - while (matcher.find()) { - try { - val tag = matcher.group(1)?.let { event.tags[it.toInt()] } - if (tag != null && tag[0] == "e") { - citations.add(tag[1]) - } - } catch (e: Exception) { - - } - } - return citations - } - - private fun replyToWithoutCitations(event: TextNoteEvent): List { - val repliesTo = event.replyTos() - if (repliesTo.isEmpty()) return repliesTo - - val citations = findCitations(event) - - return if (citations.isEmpty()) { - repliesTo - } else { - repliesTo.filter { it !in citations } - } - } - - private fun replyToWithoutCitations(event: LongTextNoteEvent): List { - val repliesTo = event.replyTos() - if (repliesTo.isEmpty()) return repliesTo - - val citations = findCitations(event) - - return if (citations.isEmpty()) { - repliesTo - } else { - repliesTo.filter { it !in citations } - } - } - - fun consume(event: RecommendRelayEvent) { - //Log.d("RR", event.toJson()) - } - - fun consume(event: ContactListEvent) { - val user = getOrCreateUser(event.pubKey) - val follows = event.follows() - - if (event.createdAt > user.updatedFollowsAt && !follows.isNullOrEmpty()) { - // Saves relay list only if it's a user that is currently been seen - user.latestContactList = event - - user.updateFollows( - follows.map { - try { - val pubKey = decodePublicKey(it.pubKeyHex) - getOrCreateUser(pubKey.toHexKey()) - } catch (e: Exception) { - Log.w("ContactList Parser", "Ignoring: Could not parse Hex key: ${it.pubKeyHex} in ${event.toJson()}") - //e.printStackTrace() + val metadataParser = jacksonObjectMapper() + .configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false) + .readerFor(UserMetadata::class.java) + + val antiSpam = AntiSpamFilter() + + val users = ConcurrentHashMap() + val notes = ConcurrentHashMap() + val channels = ConcurrentHashMap() + val addressables = ConcurrentHashMap() + + fun checkGetOrCreateUser(key: String): User? { + return try { + val checkHex = Hex.decode(key).toNpub() // Checks if this is a valid Hex + getOrCreateUser(key) + } catch (e: IllegalArgumentException) { + Log.e("LocalCache", "Invalid Key to create user: $key", e) null - } - }.filterNotNull().toSet(), - event.createdAt - ) + } + } - // Saves relay list only if it's a user that is currently been seen - try { - if (event.content.isNotEmpty()) { - val relays: Map = - Event.gson.fromJson( - event.content, - object : TypeToken>() {}.type + @Synchronized + fun getOrCreateUser(key: HexKey): User { + return users[key] ?: run { + val answer = User(key) + users.put(key, answer) + answer + } + } + + fun checkGetOrCreateNote(key: String): Note? { + if (ATag.isATag(key)) { + return checkGetOrCreateAddressableNote(key) + } + return try { + val checkHex = Hex.decode(key).toNote() // Checks if this is a valid Hex + getOrCreateNote(key) + } catch (e: IllegalArgumentException) { + Log.e("LocalCache", "Invalid Key to create note: $key", e) + null + } + } + + @Synchronized + fun getOrCreateNote(idHex: String): Note { + return notes[idHex] ?: run { + val answer = Note(idHex) + notes.put(idHex, answer) + answer + } + } + + fun checkGetOrCreateChannel(key: String): Channel? { + return try { + val checkHex = Hex.decode(key).toNote() // Checks if this is a valid Hex + getOrCreateChannel(key) + } catch (e: IllegalArgumentException) { + Log.e("LocalCache", "Invalid Key to create channel: $key", e) + null + } + } + + @Synchronized + fun getOrCreateChannel(key: String): Channel { + return channels[key] ?: run { + val answer = Channel(key) + channels.put(key, answer) + answer + } + } + + fun checkGetOrCreateAddressableNote(key: String): AddressableNote? { + return try { + val addr = ATag.parse(key, null) // relay doesn't matter for the index. + if (addr != null) { + getOrCreateAddressableNote(addr) + } else { + null + } + } catch (e: IllegalArgumentException) { + Log.e("LocalCache", "Invalid Key to create channel: $key", e) + null + } + } + + @Synchronized + fun getOrCreateAddressableNote(key: ATag): AddressableNote { + // we can't use naddr here because naddr might include relay info and + // the preferred relay should not be part of the index. + return addressables[key.toTag()] ?: run { + val answer = AddressableNote(key) + answer.author = checkGetOrCreateUser(key.pubKeyHex) + addressables.put(key.toTag(), answer) + answer + } + } + + fun consume(event: MetadataEvent) { + // new event + val oldUser = getOrCreateUser(event.pubKey) + if (oldUser.info == null || event.createdAt > oldUser.info!!.updatedMetadataAt) { + val newUser = try { + metadataParser.readValue( + ByteArrayInputStream(event.content.toByteArray(Charsets.UTF_8)), + UserMetadata::class.java + ) + } catch (e: Exception) { + e.printStackTrace() + Log.w("MT", "Content Parse Error ${e.localizedMessage} ${event.content}") + return + } + + oldUser.updateUserInfo(newUser, event) + // Log.d("MT", "New User Metadata ${oldUser.pubkeyDisplayHex} ${oldUser.toBestDisplayName()}") + } else { + // Log.d("MT","Relay sent a previous Metadata Event ${oldUser.toBestDisplayName()} ${formattedDateTime(event.createdAt)} > ${formattedDateTime(oldUser.updatedAt)}") + } + } + + fun formattedDateTime(timestamp: Long): String { + return Instant.ofEpochSecond(timestamp).atZone(ZoneId.systemDefault()) + .format(DateTimeFormatter.ofPattern("uuuu MMM d hh:mm a")) + } + + fun consume(event: TextNoteEvent, relay: Relay? = null) { + 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?.let { + it.spamCounter++ + } + return + } + + val mentions = event.mentions().mapNotNull { checkGetOrCreateUser(it) } + val replyTo = replyToWithoutCitations(event).mapNotNull { checkGetOrCreateNote(it) } + + event.taggedAddresses().mapNotNull { getOrCreateAddressableNote(it) } + + note.loadEvent(event, author, mentions, replyTo) + + // Log.d("TN", "New Note (${notes.size},${users.size}) ${note.author?.toBestDisplayName()} ${note.event?.content()?.take(100)} ${formattedDateTime(event.createdAt)}") + + // Prepares user's profile view. + author.addNote(note) + + // Adds notifications to users. + mentions.forEach { + it.addTaggedPost(note) + } + replyTo.forEach { + it.author?.addTaggedPost(note) + } + + // Counts the replies + replyTo.forEach { + it.addReply(note) + } + + refreshObservers() + } + + fun consume(event: LongTextNoteEvent, relay: Relay?) { + val note = getOrCreateAddressableNote(event.address()) + val author = getOrCreateUser(event.pubKey) + + if (relay != null) { + author.addRelayBeingUsed(relay, event.createdAt) + note.addRelay(relay) + } + + // Already processed this event. + if (note.event?.id() == event.id()) return + + if (antiSpam.isSpam(event)) { + relay?.let { + it.spamCounter++ + } + return + } + + val mentions = event.mentions().mapNotNull { checkGetOrCreateUser(it) } + val replyTo = replyToWithoutCitations(event).mapNotNull { checkGetOrCreateNote(it) } + + if (event.createdAt > (note.createdAt() ?: 0)) { + note.loadEvent(event, author, mentions, replyTo) + + author.addNote(note) + + // Adds notifications to users. + mentions.forEach { + it.addTaggedPost(note) + } + replyTo.forEach { + it.author?.addTaggedPost(note) + } + + refreshObservers() + } + } + + fun consume(event: BadgeDefinitionEvent) { + val note = getOrCreateAddressableNote(event.address()) + val author = getOrCreateUser(event.pubKey) + + // Already processed this event. + if (note.event?.id() == event.id()) return + + if (event.createdAt > (note.createdAt() ?: 0)) { + note.loadEvent(event, author, emptyList(), emptyList()) + + refreshObservers() + } + } + + fun consume(event: BadgeProfilesEvent) { + val note = getOrCreateAddressableNote(event.address()) + val author = getOrCreateUser(event.pubKey) + + // Already processed this event. + if (note.event?.id() == event.id()) return + + val replyTo = event.badgeAwardEvents().mapNotNull { checkGetOrCreateNote(it) } + + event.badgeAwardDefinitions().mapNotNull { getOrCreateAddressableNote(it) } + + if (event.createdAt > (note.createdAt() ?: 0)) { + note.loadEvent(event, author, emptyList(), replyTo) + + author.updateAcceptedBadges(note) + + refreshObservers() + } + } + + fun consume(event: BadgeAwardEvent) { + val note = getOrCreateNote(event.id) + + // Already processed this event. + if (note.event != null) return + + // Log.d("TN", "New Boost (${notes.size},${users.size}) ${note.author?.toBestDisplayName()} ${formattedDateTime(event.createdAt)}") + + val author = getOrCreateUser(event.pubKey) + val awardees = event.awardees().mapNotNull { checkGetOrCreateUser(it) } + val awardDefinition = event.awardDefinition().map { getOrCreateAddressableNote(it) } + + note.loadEvent(event, author, awardees, awardDefinition) + + // Adds notifications to users. + awardees.forEach { + it.addTaggedPost(note) + } + + // Counts the replies + awardees.forEach { + it.addBadgeAward(note) + } + + // Replies of an Badge Definition are Award Events + awardDefinition.forEach { + it.addReply(note) + } + + refreshObservers() + } + + private fun findCitations(event: Event): Set { + var citations = mutableSetOf() + // Removes citations from replies: + val matcher = tagSearch.matcher(event.content) + while (matcher.find()) { + try { + val tag = matcher.group(1)?.let { event.tags[it.toInt()] } + if (tag != null && tag[0] == "e") { + citations.add(tag[1]) + } + } catch (e: Exception) { + } + } + return citations + } + + private fun replyToWithoutCitations(event: TextNoteEvent): List { + val repliesTo = event.replyTos() + if (repliesTo.isEmpty()) return repliesTo + + val citations = findCitations(event) + + return if (citations.isEmpty()) { + repliesTo + } else { + repliesTo.filter { it !in citations } + } + } + + private fun replyToWithoutCitations(event: LongTextNoteEvent): List { + val repliesTo = event.replyTos() + if (repliesTo.isEmpty()) return repliesTo + + val citations = findCitations(event) + + return if (citations.isEmpty()) { + repliesTo + } else { + repliesTo.filter { it !in citations } + } + } + + fun consume(event: RecommendRelayEvent) { + // Log.d("RR", event.toJson()) + } + + fun consume(event: ContactListEvent) { + val user = getOrCreateUser(event.pubKey) + val follows = event.follows() + + if (event.createdAt > user.updatedFollowsAt && !follows.isNullOrEmpty()) { + // Saves relay list only if it's a user that is currently been seen + user.latestContactList = event + + user.updateFollows( + follows.map { + try { + val pubKey = decodePublicKey(it.pubKeyHex) + getOrCreateUser(pubKey.toHexKey()) + } catch (e: Exception) { + Log.w("ContactList Parser", "Ignoring: Could not parse Hex key: ${it.pubKeyHex} in ${event.toJson()}") + // e.printStackTrace() + null + } + }.filterNotNull().toSet(), + event.createdAt ) - user.updateRelays(relays) + // Saves relay list only if it's a user that is currently been seen + try { + if (event.content.isNotEmpty()) { + val relays: Map = + Event.gson.fromJson( + event.content, + object : TypeToken>() {}.type + ) + + user.updateRelays(relays) + } + } catch (e: Exception) { + Log.w("Relay List Parser", "Relay import issue ${e.message}", e) + e.printStackTrace() + } + + Log.d("CL", "AAA ${user.toBestDisplayName()} ${follows.size}") } - } catch (e: Exception) { - Log.w("Relay List Parser","Relay import issue ${e.message}", e) - e.printStackTrace() - } - - Log.d("CL", "AAA ${user.toBestDisplayName()} ${follows.size}") - } - } - - fun consume(event: PrivateDmEvent, 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 + fun consume(event: PrivateDmEvent, relay: Relay?) { + val note = getOrCreateNote(event.id) + val author = getOrCreateUser(event.pubKey) - val recipient = event.recipientPubKey()?.let { getOrCreateUser(it) } - - //Log.d("PM", "${author.toBestDisplayName()} to ${recipient?.toBestDisplayName()}") - - val repliesTo = event.tags.filter { it.firstOrNull() == "e" }.mapNotNull { it.getOrNull(1) }.mapNotNull { checkGetOrCreateNote(it) } - val mentions = event.tags.filter { it.firstOrNull() == "p" }.mapNotNull { it.getOrNull(1) }.mapNotNull { checkGetOrCreateUser(it) } - - note.loadEvent(event, author, mentions, repliesTo) - - if (recipient != null) { - author.addMessage(recipient, note) - recipient.addMessage(author, note) - } - - refreshObservers() - } - - fun consume(event: DeletionEvent) { - var deletedAtLeastOne = false - - event.deleteEvents().mapNotNull { notes[it] }.forEach { deleteNote -> - // must be the same author - if (deleteNote.author?.pubkeyHex == event.pubKey) { - deleteNote.author?.removeNote(deleteNote) - - // reverts the add - deleteNote.mentions?.forEach { user -> - user.removeTaggedPost(deleteNote) - user.removeReport(deleteNote) + if (relay != null) { + author.addRelayBeingUsed(relay, event.createdAt) + note.addRelay(relay) } - deleteNote.replyTo?.forEach { replyingNote -> - replyingNote.author?.removeTaggedPost(deleteNote) + // Already processed this event. + if (note.event != null) return + + val recipient = event.recipientPubKey()?.let { getOrCreateUser(it) } + + // Log.d("PM", "${author.toBestDisplayName()} to ${recipient?.toBestDisplayName()}") + + val repliesTo = event.tags.filter { it.firstOrNull() == "e" }.mapNotNull { it.getOrNull(1) }.mapNotNull { checkGetOrCreateNote(it) } + val mentions = event.tags.filter { it.firstOrNull() == "p" }.mapNotNull { it.getOrNull(1) }.mapNotNull { checkGetOrCreateUser(it) } + + note.loadEvent(event, author, mentions, repliesTo) + + if (recipient != null) { + author.addMessage(recipient, note) + recipient.addMessage(author, note) + } + + refreshObservers() + } + + fun consume(event: DeletionEvent) { + var deletedAtLeastOne = false + + event.deleteEvents().mapNotNull { notes[it] }.forEach { deleteNote -> + // must be the same author + if (deleteNote.author?.pubkeyHex == event.pubKey) { + deleteNote.author?.removeNote(deleteNote) + + // reverts the add + deleteNote.mentions?.forEach { user -> + user.removeTaggedPost(deleteNote) + user.removeReport(deleteNote) + } + + deleteNote.replyTo?.forEach { replyingNote -> + replyingNote.author?.removeTaggedPost(deleteNote) + } + + // Counts the replies + deleteNote.replyTo?.forEach { masterNote -> + masterNote.removeReply(deleteNote) + masterNote.removeBoost(deleteNote) + masterNote.removeReaction(deleteNote) + masterNote.removeZap(deleteNote) + masterNote.removeReport(deleteNote) + } + + notes.remove(deleteNote.idHex) + + deletedAtLeastOne = true + } + } + + if (deletedAtLeastOne) { + live.invalidateData() + } + } + + fun consume(event: RepostEvent) { + val note = getOrCreateNote(event.id) + + // Already processed this event. + if (note.event != null) return + + // Log.d("TN", "New Boost (${notes.size},${users.size}) ${note.author?.toBestDisplayName()} ${formattedDateTime(event.createdAt)}") + + val author = getOrCreateUser(event.pubKey) + val mentions = event.originalAuthor().mapNotNull { checkGetOrCreateUser(it) } + val repliesTo = event.boostedPost().mapNotNull { checkGetOrCreateNote(it) } + + event.taggedAddresses().mapNotNull { getOrCreateAddressableNote(it) } + + note.loadEvent(event, author, mentions, repliesTo) + + // Prepares user's profile view. + author.addNote(note) + + // Adds notifications to users. + mentions.forEach { + it.addTaggedPost(note) + } + repliesTo.forEach { + it.author?.addTaggedPost(note) } // Counts the replies - deleteNote.replyTo?.forEach { masterNote -> - masterNote.removeReply(deleteNote) - masterNote.removeBoost(deleteNote) - masterNote.removeReaction(deleteNote) - masterNote.removeZap(deleteNote) - masterNote.removeReport(deleteNote) + repliesTo.forEach { + it.addBoost(note) } - notes.remove(deleteNote.idHex) - - deletedAtLeastOne = true - } - } - - if (deletedAtLeastOne) { - live.invalidateData() - } - } - - fun consume(event: RepostEvent) { - val note = getOrCreateNote(event.id) - - // Already processed this event. - if (note.event != null) return - - //Log.d("TN", "New Boost (${notes.size},${users.size}) ${note.author?.toBestDisplayName()} ${formattedDateTime(event.createdAt)}") - - val author = getOrCreateUser(event.pubKey) - val mentions = event.originalAuthor().mapNotNull { checkGetOrCreateUser(it) } - val repliesTo = event.boostedPost().mapNotNull { checkGetOrCreateNote(it) } + - event.taggedAddresses().mapNotNull { getOrCreateAddressableNote(it) } - - note.loadEvent(event, author, mentions, repliesTo) - - // Prepares user's profile view. - author.addNote(note) - - // Adds notifications to users. - mentions.forEach { - it.addTaggedPost(note) - } - repliesTo.forEach { - it.author?.addTaggedPost(note) - } - - // Counts the replies - repliesTo.forEach { - it.addBoost(note) - } - - refreshObservers() - } - - fun consume(event: ReactionEvent) { - val note = getOrCreateNote(event.id) - - // Already processed this event. - if (note.event != null) return - - val author = getOrCreateUser(event.pubKey) - val mentions = event.originalAuthor().mapNotNull { checkGetOrCreateUser(it) } - val repliesTo = event.originalPost().mapNotNull { checkGetOrCreateNote(it) } + - event.taggedAddresses().mapNotNull { getOrCreateAddressableNote(it) } - - note.loadEvent(event, author, mentions, repliesTo) - - //Log.d("RE", "New Reaction ${event.content} (${notes.size},${users.size}) ${note.author?.toBestDisplayName()} ${formattedDateTime(event.createdAt)}") - - // Adds notifications to users. - mentions.forEach { - it.addTaggedPost(note) - } - repliesTo.forEach { - it.author?.addTaggedPost(note) - } - - if ( - event.content == "" || - event.content == "+" || - event.content == "\u2764\uFE0F" || // red heart - event.content == "\uD83E\uDD19" || // call me hand - event.content == "\uD83D\uDC4D" // thumbs up - ) { - // Counts the replies - repliesTo.forEach { - it.addReaction(note) - } - } - - if (event.content == "!" // nostr_console hide. - || event.content == "\u26A0\uFE0F" // Warning sign - ) { - // Counts the replies - repliesTo.forEach { - it.addReport(note) - } - } - } - - fun consume(event: ReportEvent, 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 - - val mentions = event.reportedAuthor().mapNotNull { checkGetOrCreateUser(it.key) } - val repliesTo = event.reportedPost().mapNotNull { checkGetOrCreateNote(it.key) } + - event.taggedAddresses().mapNotNull { getOrCreateAddressableNote(it) } - - note.loadEvent(event, author, mentions, repliesTo) - - //Log.d("RP", "New Report ${event.content} by ${note.author?.toBestDisplayName()} ${formattedDateTime(event.createdAt)}") - // Adds notifications to users. - if (repliesTo.isEmpty()) { - mentions.forEach { - it.addReport(note) - } - } - repliesTo.forEach { - it.addReport(note) - } - } - - fun consume(event: ChannelCreateEvent) { - //Log.d("MT", "New Event ${event.content} ${event.id.toHex()}") - // new event - val oldChannel = getOrCreateChannel(event.id) - val author = getOrCreateUser(event.pubKey) - if (event.createdAt > oldChannel.updatedMetadataAt) { - if (oldChannel.creator == null || oldChannel.creator == author) { - oldChannel.updateChannelInfo(author, event.channelInfo(), event.createdAt) - - val note = getOrCreateNote(event.id) - oldChannel.addNote(note) - note.loadEvent(event, author, emptyList(), emptyList()) - refreshObservers() - } - } else { - // older data, does nothing } - } - - fun consume(event: ChannelMetadataEvent) { - val channelId = event.channel() - //Log.d("MT", "New User ${users.size} ${event.contactMetaData.name}") - if (channelId.isNullOrBlank()) return - - // new event - val oldChannel = checkGetOrCreateChannel(channelId) ?: return - val author = getOrCreateUser(event.pubKey) - if (event.createdAt > oldChannel.updatedMetadataAt) { - if (oldChannel.creator == null || oldChannel.creator == author) { - oldChannel.updateChannelInfo(author, event.channelInfo(), event.createdAt) + fun consume(event: ReactionEvent) { val note = getOrCreateNote(event.id) - oldChannel.addNote(note) - note.loadEvent(event, author, emptyList(), emptyList()) - refreshObservers() - } - } else { - //Log.d("MT","Relay sent a previous Metadata Event ${oldUser.toBestDisplayName()} ${formattedDateTime(event.createdAt)} > ${formattedDateTime(oldUser.updatedAt)}") - } - } + // Already processed this event. + if (note.event != null) return - fun consume(event: ChannelMessageEvent, relay: Relay?) { - val channelId = event.channel() + val author = getOrCreateUser(event.pubKey) + val mentions = event.originalAuthor().mapNotNull { checkGetOrCreateUser(it) } + val repliesTo = event.originalPost().mapNotNull { checkGetOrCreateNote(it) } + + event.taggedAddresses().mapNotNull { getOrCreateAddressableNote(it) } - if (channelId.isNullOrBlank()) return + note.loadEvent(event, author, mentions, repliesTo) - val channel = checkGetOrCreateChannel(channelId) ?: return + // Log.d("RE", "New Reaction ${event.content} (${notes.size},${users.size}) ${note.author?.toBestDisplayName()} ${formattedDateTime(event.createdAt)}") - val note = getOrCreateNote(event.id) - channel.addNote(note) - - 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?.let { - it.spamCounter++ - } - return - } - - val mentions = event.mentions().mapNotNull { checkGetOrCreateUser(it) } - val replyTo = event.replyTos() - .mapNotNull { checkGetOrCreateNote(it) } - .filter { it.event !is ChannelCreateEvent } - - note.loadEvent(event, author, mentions, replyTo) - - //Log.d("CM", "New Note (${notes.size},${users.size}) ${note.author?.toBestDisplayName()} ${note.event?.content()} ${formattedDateTime(event.createdAt)}") - - // Adds notifications to users. - mentions.forEach { - it.addTaggedPost(note) - } - replyTo.forEach { - it.author?.addTaggedPost(note) - } - - // Counts the replies - replyTo.forEach { - it.addReply(note) - } - - refreshObservers() - } - - fun consume(event: ChannelHideMessageEvent) { - - } - - fun consume(event: ChannelMuteUserEvent) { - - } - - fun consume(event: LnZapEvent) { - val note = getOrCreateNote(event.id) - - // Already processed this event. - if (note.event != null) return - - val zapRequest = event.containedPost()?.id?.let { getOrCreateNote(it) } - - val author = getOrCreateUser(event.pubKey) - val mentions = event.zappedAuthor().mapNotNull { checkGetOrCreateUser(it) } - val repliesTo = event.zappedPost().mapNotNull { checkGetOrCreateNote(it) } + - event.taggedAddresses().map { getOrCreateAddressableNote(it) } + - ((zapRequest?.event as? LnZapRequestEvent)?.taggedAddresses()?.map { getOrCreateAddressableNote(it) } ?: emptySet()) - - note.loadEvent(event, author, mentions, repliesTo) - - if (zapRequest == null) { - Log.e("ZP","Zap Request not found. Unable to process Zap {${event.toJson()}}") - return - } - - //Log.d("ZP", "New ZapEvent ${event.content} (${notes.size},${users.size}) ${note.author?.toBestDisplayName()} ${formattedDateTime(event.createdAt)}") - - // Adds notifications to users. - mentions.forEach { - it.addTaggedPost(note) - } - repliesTo.forEach { - it.author?.addTaggedPost(note) - } - - repliesTo.forEach { - it.addZap(zapRequest, note) - } - mentions.forEach { - it.addZap(zapRequest, note) - } - } - - fun consume(event: LnZapRequestEvent) { - val note = getOrCreateNote(event.id) - - // Already processed this event. - if (note.event != null) return - - val author = getOrCreateUser(event.pubKey) - val mentions = event.zappedAuthor().mapNotNull { checkGetOrCreateUser(it) } - val repliesTo = event.zappedPost().mapNotNull { checkGetOrCreateNote(it) } + - event.taggedAddresses().map { getOrCreateAddressableNote(it) } - - note.loadEvent(event, author, mentions, repliesTo) - - //Log.d("ZP", "New Zap Request ${event.content} (${notes.size},${users.size}) ${note.author?.toBestDisplayName()} ${formattedDateTime(event.createdAt)}") - - // Adds notifications to users. - mentions.forEach { - it.addTaggedPost(note) - } - repliesTo.forEach { - it.author?.addTaggedPost(note) - } - - repliesTo.forEach { - it.addZap(note, null) - } - mentions.forEach { - it.addZap(note, null) - } - } - - fun findUsersStartingWith(username: String): List { - return users.values.filter { - (it.anyNameStartsWith(username)) - || it.pubkeyHex.startsWith(username, true) - || it.pubkeyNpub().startsWith(username, true) - } - } - - fun findNotesStartingWith(text: String): List { - return notes.values.filter { - (it.event is TextNoteEvent && it.event?.content()?.contains(text, true) ?: false) - || (it.event is ChannelMessageEvent && it.event?.content()?.contains(text, true) ?: false) - || it.idHex.startsWith(text, true) - || it.idNote().startsWith(text, true) - } + addressables.values.filter { - (it.event as? LongTextNoteEvent)?.content?.contains(text, true) ?: false - || (it.event as? LongTextNoteEvent)?.title()?.contains(text, true) ?: false - || (it.event as? LongTextNoteEvent)?.summary()?.contains(text, true) ?: false - || it.idHex.startsWith(text, true) - } - } - - fun findChannelsStartingWith(text: String): List { - return channels.values.filter { - it.anyNameStartsWith(text) - || it.idHex.startsWith(text, true) - || it.idNote().startsWith(text, true) - } - } - - fun cleanObservers() { - notes.forEach { - it.value.clearLive() - } - - users.forEach { - it.value.clearLive() - } - } - - fun pruneOldAndHiddenMessages(account: Account) { - channels.forEach { - val toBeRemoved = it.value.pruneOldAndHiddenMessages(account) - - toBeRemoved.forEach { - notes.remove(it.idHex) - // Doesn't need to clean up the replies and mentions.. Too small to matter. - - // reverts the add - it.mentions?.forEach { user -> - user.removeTaggedPost(it) + // Adds notifications to users. + mentions.forEach { + it.addTaggedPost(note) } - it.replyTo?.forEach { replyingNote -> - replyingNote.author?.removeTaggedPost(it) + repliesTo.forEach { + it.author?.addTaggedPost(note) + } + + if ( + event.content == "" || + event.content == "+" || + event.content == "\u2764\uFE0F" || // red heart + event.content == "\uD83E\uDD19" || // call me hand + event.content == "\uD83D\uDC4D" // thumbs up + ) { + // Counts the replies + repliesTo.forEach { + it.addReaction(note) + } + } + + if (event.content == "!" || // nostr_console hide. + event.content == "\u26A0\uFE0F" // Warning sign + ) { + // Counts the replies + repliesTo.forEach { + it.addReport(note) + } + } + } + + fun consume(event: ReportEvent, 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 + + val mentions = event.reportedAuthor().mapNotNull { checkGetOrCreateUser(it.key) } + val repliesTo = event.reportedPost().mapNotNull { checkGetOrCreateNote(it.key) } + + event.taggedAddresses().mapNotNull { getOrCreateAddressableNote(it) } + + note.loadEvent(event, author, mentions, repliesTo) + + // Log.d("RP", "New Report ${event.content} by ${note.author?.toBestDisplayName()} ${formattedDateTime(event.createdAt)}") + // Adds notifications to users. + if (repliesTo.isEmpty()) { + mentions.forEach { + it.addReport(note) + } + } + repliesTo.forEach { + it.addReport(note) + } + } + + fun consume(event: ChannelCreateEvent) { + // Log.d("MT", "New Event ${event.content} ${event.id.toHex()}") + // new event + val oldChannel = getOrCreateChannel(event.id) + val author = getOrCreateUser(event.pubKey) + if (event.createdAt > oldChannel.updatedMetadataAt) { + if (oldChannel.creator == null || oldChannel.creator == author) { + oldChannel.updateChannelInfo(author, event.channelInfo(), event.createdAt) + + val note = getOrCreateNote(event.id) + oldChannel.addNote(note) + note.loadEvent(event, author, emptyList(), emptyList()) + + refreshObservers() + } + } else { + // older data, does nothing + } + } + + fun consume(event: ChannelMetadataEvent) { + val channelId = event.channel() + // Log.d("MT", "New User ${users.size} ${event.contactMetaData.name}") + if (channelId.isNullOrBlank()) return + + // new event + val oldChannel = checkGetOrCreateChannel(channelId) ?: return + val author = getOrCreateUser(event.pubKey) + if (event.createdAt > oldChannel.updatedMetadataAt) { + if (oldChannel.creator == null || oldChannel.creator == author) { + oldChannel.updateChannelInfo(author, event.channelInfo(), event.createdAt) + + val note = getOrCreateNote(event.id) + oldChannel.addNote(note) + note.loadEvent(event, author, emptyList(), emptyList()) + + refreshObservers() + } + } else { + // Log.d("MT","Relay sent a previous Metadata Event ${oldUser.toBestDisplayName()} ${formattedDateTime(event.createdAt)} > ${formattedDateTime(oldUser.updatedAt)}") + } + } + + fun consume(event: ChannelMessageEvent, relay: Relay?) { + val channelId = event.channel() + + if (channelId.isNullOrBlank()) return + + val channel = checkGetOrCreateChannel(channelId) ?: return + + val note = getOrCreateNote(event.id) + channel.addNote(note) + + 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?.let { + it.spamCounter++ + } + return + } + + val mentions = event.mentions().mapNotNull { checkGetOrCreateUser(it) } + val replyTo = event.replyTos() + .mapNotNull { checkGetOrCreateNote(it) } + .filter { it.event !is ChannelCreateEvent } + + note.loadEvent(event, author, mentions, replyTo) + + // Log.d("CM", "New Note (${notes.size},${users.size}) ${note.author?.toBestDisplayName()} ${note.event?.content()} ${formattedDateTime(event.createdAt)}") + + // Adds notifications to users. + mentions.forEach { + it.addTaggedPost(note) + } + replyTo.forEach { + it.author?.addTaggedPost(note) } // Counts the replies - it.replyTo?.forEach { replyingNote -> - it.removeReply(it) + replyTo.forEach { + it.addReply(note) } - } - println("PRUNE: ${toBeRemoved.size} messages removed from ${it.value.info.name}") - } - } - - fun pruneNonFollows(account: Account) { - val follows = account.userProfile().follows - val knownPMs = account.userProfile().privateChatrooms.filter { - account.userProfile().hasSentMessagesTo(it.key) && account.isAcceptable(it.key) + refreshObservers() } - val followsFollow = follows.map { - it.follows - }.flatten() - - val followSet = follows.plus(knownPMs).plus(account.userProfile()).plus(followsFollow) - - val toBeRemoved = notes - .filter { - (it.value.author == null || it.value.author!! !in followSet) && it.value.event?.kind() == TextNoteEvent.kind && it.value.liveSet?.isInUse() != true - } - - toBeRemoved.forEach { - notes.remove(it.key) + fun consume(event: ChannelHideMessageEvent) { } - val toBeRemovedUsers = users - .filter { - (it.value !in followSet) && it.value.liveSet?.isInUse() != true - } - - toBeRemovedUsers.forEach { - users.remove(it.key) + fun consume(event: ChannelMuteUserEvent) { } - println("PRUNE: ${toBeRemoved.size} messages removed because they came from NonFollows") - println("PRUNE: ${toBeRemovedUsers.size} users removed because are NonFollows") - } + fun consume(event: LnZapEvent) { + val note = getOrCreateNote(event.id) - fun pruneHiddenMessages(account: Account) { - val toBeRemoved = account.hiddenUsers.map { - (users[it]?.notes ?: emptySet()) - }.flatten() + // Already processed this event. + if (note.event != null) return - account.hiddenUsers.forEach { - users[it]?.clearNotes() - } + val zapRequest = event.containedPost()?.id?.let { getOrCreateNote(it) } - toBeRemoved.forEach { - it.author?.removeNote(it) + val author = getOrCreateUser(event.pubKey) + val mentions = event.zappedAuthor().mapNotNull { checkGetOrCreateUser(it) } + val repliesTo = event.zappedPost().mapNotNull { checkGetOrCreateNote(it) } + + event.taggedAddresses().map { getOrCreateAddressableNote(it) } + + ((zapRequest?.event as? LnZapRequestEvent)?.taggedAddresses()?.map { getOrCreateAddressableNote(it) } ?: emptySet()) - // reverts the add - it.mentions?.forEach { user -> - user.removeTaggedPost(it) - } - it.replyTo?.forEach { replyingNote -> - replyingNote.author?.removeTaggedPost(it) - } + note.loadEvent(event, author, mentions, repliesTo) - // Counts the replies - it.replyTo?.forEach { masterNote -> - masterNote.removeReply(it) - masterNote.removeBoost(it) - masterNote.removeReaction(it) - masterNote.removeZap(it) - masterNote.removeReport(it) - } - - notes.remove(it.idHex) - } - - println("PRUNE: ${toBeRemoved.size} messages removed because they were Hidden") - } - - // Observers line up here. - val live: LocalCacheLiveData = LocalCacheLiveData(this) - - private fun refreshObservers() { - live.invalidateData() - } -} - -class LocalCacheLiveData(val cache: LocalCache): LiveData(LocalCacheState(cache)) { - - // Refreshes observers in batches. - var handlerWaiting = AtomicBoolean() - - fun invalidateData() { - if (!hasActiveObservers()) return - if (handlerWaiting.getAndSet(true)) return - - val scope = CoroutineScope(Job() + Dispatchers.Main) - scope.launch { - try { - delay(50) - refresh() - } finally { - withContext(NonCancellable) { - handlerWaiting.set(false) + if (zapRequest == null) { + Log.e("ZP", "Zap Request not found. Unable to process Zap {${event.toJson()}}") + return + } + + // Log.d("ZP", "New ZapEvent ${event.content} (${notes.size},${users.size}) ${note.author?.toBestDisplayName()} ${formattedDateTime(event.createdAt)}") + + // Adds notifications to users. + mentions.forEach { + it.addTaggedPost(note) + } + repliesTo.forEach { + it.author?.addTaggedPost(note) + } + + repliesTo.forEach { + it.addZap(zapRequest, note) + } + mentions.forEach { + it.addZap(zapRequest, note) } - } } - } - private fun refresh() { - postValue(LocalCacheState(cache)) - } + fun consume(event: LnZapRequestEvent) { + val note = getOrCreateNote(event.id) + + // Already processed this event. + if (note.event != null) return + + val author = getOrCreateUser(event.pubKey) + val mentions = event.zappedAuthor().mapNotNull { checkGetOrCreateUser(it) } + val repliesTo = event.zappedPost().mapNotNull { checkGetOrCreateNote(it) } + + event.taggedAddresses().map { getOrCreateAddressableNote(it) } + + note.loadEvent(event, author, mentions, repliesTo) + + // Log.d("ZP", "New Zap Request ${event.content} (${notes.size},${users.size}) ${note.author?.toBestDisplayName()} ${formattedDateTime(event.createdAt)}") + + // Adds notifications to users. + mentions.forEach { + it.addTaggedPost(note) + } + repliesTo.forEach { + it.author?.addTaggedPost(note) + } + + repliesTo.forEach { + it.addZap(note, null) + } + mentions.forEach { + it.addZap(note, null) + } + } + + fun findUsersStartingWith(username: String): List { + return users.values.filter { + (it.anyNameStartsWith(username)) || + it.pubkeyHex.startsWith(username, true) || + it.pubkeyNpub().startsWith(username, true) + } + } + + fun findNotesStartingWith(text: String): List { + return notes.values.filter { + (it.event is TextNoteEvent && it.event?.content()?.contains(text, true) ?: false) || + (it.event is ChannelMessageEvent && it.event?.content()?.contains(text, true) ?: false) || + it.idHex.startsWith(text, true) || + it.idNote().startsWith(text, true) + } + addressables.values.filter { + (it.event as? LongTextNoteEvent)?.content?.contains(text, true) ?: false || + (it.event as? LongTextNoteEvent)?.title()?.contains(text, true) ?: false || + (it.event as? LongTextNoteEvent)?.summary()?.contains(text, true) ?: false || + it.idHex.startsWith(text, true) + } + } + + fun findChannelsStartingWith(text: String): List { + return channels.values.filter { + it.anyNameStartsWith(text) || + it.idHex.startsWith(text, true) || + it.idNote().startsWith(text, true) + } + } + + fun cleanObservers() { + notes.forEach { + it.value.clearLive() + } + + users.forEach { + it.value.clearLive() + } + } + + fun pruneOldAndHiddenMessages(account: Account) { + channels.forEach { + val toBeRemoved = it.value.pruneOldAndHiddenMessages(account) + + toBeRemoved.forEach { + notes.remove(it.idHex) + // Doesn't need to clean up the replies and mentions.. Too small to matter. + + // reverts the add + it.mentions?.forEach { user -> + user.removeTaggedPost(it) + } + it.replyTo?.forEach { replyingNote -> + replyingNote.author?.removeTaggedPost(it) + } + + // Counts the replies + it.replyTo?.forEach { replyingNote -> + it.removeReply(it) + } + } + + println("PRUNE: ${toBeRemoved.size} messages removed from ${it.value.info.name}") + } + } + + fun pruneNonFollows(account: Account) { + val follows = account.userProfile().follows + val knownPMs = account.userProfile().privateChatrooms.filter { + account.userProfile().hasSentMessagesTo(it.key) && account.isAcceptable(it.key) + } + + val followsFollow = follows.map { + it.follows + }.flatten() + + val followSet = follows.plus(knownPMs).plus(account.userProfile()).plus(followsFollow) + + val toBeRemoved = notes + .filter { + (it.value.author == null || it.value.author!! !in followSet) && it.value.event?.kind() == TextNoteEvent.kind && it.value.liveSet?.isInUse() != true + } + + toBeRemoved.forEach { + notes.remove(it.key) + } + + val toBeRemovedUsers = users + .filter { + (it.value !in followSet) && it.value.liveSet?.isInUse() != true + } + + toBeRemovedUsers.forEach { + users.remove(it.key) + } + + println("PRUNE: ${toBeRemoved.size} messages removed because they came from NonFollows") + println("PRUNE: ${toBeRemovedUsers.size} users removed because are NonFollows") + } + + fun pruneHiddenMessages(account: Account) { + val toBeRemoved = account.hiddenUsers.map { + (users[it]?.notes ?: emptySet()) + }.flatten() + + account.hiddenUsers.forEach { + users[it]?.clearNotes() + } + + toBeRemoved.forEach { + it.author?.removeNote(it) + + // reverts the add + it.mentions?.forEach { user -> + user.removeTaggedPost(it) + } + it.replyTo?.forEach { replyingNote -> + replyingNote.author?.removeTaggedPost(it) + } + + // Counts the replies + it.replyTo?.forEach { masterNote -> + masterNote.removeReply(it) + masterNote.removeBoost(it) + masterNote.removeReaction(it) + masterNote.removeZap(it) + masterNote.removeReport(it) + } + + notes.remove(it.idHex) + } + + println("PRUNE: ${toBeRemoved.size} messages removed because they were Hidden") + } + + // Observers line up here. + val live: LocalCacheLiveData = LocalCacheLiveData(this) + + private fun refreshObservers() { + live.invalidateData() + } } -class LocalCacheState(val cache: LocalCache) { +class LocalCacheLiveData(val cache: LocalCache) : LiveData(LocalCacheState(cache)) { + // Refreshes observers in batches. + var handlerWaiting = AtomicBoolean() + + fun invalidateData() { + if (!hasActiveObservers()) return + if (handlerWaiting.getAndSet(true)) return + + val scope = CoroutineScope(Job() + Dispatchers.Main) + scope.launch { + try { + delay(50) + refresh() + } finally { + withContext(NonCancellable) { + handlerWaiting.set(false) + } + } + } + } + + private fun refresh() { + postValue(LocalCacheState(cache)) + } } + +class LocalCacheState(val cache: LocalCache) diff --git a/app/src/main/java/com/vitorpamplona/amethyst/model/Note.kt b/app/src/main/java/com/vitorpamplona/amethyst/model/Note.kt index 9e9a58dfc..8437d532b 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/model/Note.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/model/Note.kt @@ -6,13 +6,6 @@ import com.vitorpamplona.amethyst.service.model.* import com.vitorpamplona.amethyst.service.relays.Relay import com.vitorpamplona.amethyst.ui.note.toShortenHex import fr.acinq.secp256k1.Hex -import java.math.BigDecimal -import java.time.Instant -import java.time.ZoneId -import java.time.format.DateTimeFormatter -import java.util.Date -import java.util.concurrent.atomic.AtomicBoolean -import java.util.regex.Pattern import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Job @@ -20,11 +13,17 @@ import kotlinx.coroutines.NonCancellable import kotlinx.coroutines.delay import kotlinx.coroutines.launch import kotlinx.coroutines.withContext +import java.math.BigDecimal +import java.time.Instant +import java.time.ZoneId +import java.time.format.DateTimeFormatter +import java.util.Date +import java.util.concurrent.atomic.AtomicBoolean +import java.util.regex.Pattern val tagSearch = Pattern.compile("(?:\\s|\\A)\\#\\[([0-9]+)\\]") - -class AddressableNote(val address: ATag): Note(address.toTag()) { +class AddressableNote(val address: ATag) : Note(address.toTag()) { override fun idNote() = address.toNAddr() override fun idDisplayNote() = idNote().toShortenHex() override fun address() = address @@ -62,9 +61,9 @@ open class Note(val idHex: String) { fun channel(): Channel? { val channelHex = - (event as? ChannelMessageEvent)?.channel() ?: - (event as? ChannelMetadataEvent)?.channel() ?: - (event as? ChannelCreateEvent)?.let { it.id } + (event as? ChannelMessageEvent)?.channel() + ?: (event as? ChannelMetadataEvent)?.channel() + ?: (event as? ChannelCreateEvent)?.let { it.id } return channelHex?.let { LocalCache.checkGetOrCreateChannel(it) } } @@ -137,7 +136,7 @@ open class Note(val idHex: String) { fun removeReport(deleteNote: Note) { val author = deleteNote.author ?: return - if (author in reports.keys && reports[author]?.contains(deleteNote) == true ) { + if (author in reports.keys && reports[author]?.contains(deleteNote) == true) { reports[author]?.let { reports = reports + Pair(author, it.minus(deleteNote)) liveSet?.reports?.invalidateData() @@ -156,7 +155,6 @@ open class Note(val idHex: String) { } } - fun addBoost(note: Note) { if (note !in boosts) { boosts = boosts + note @@ -240,11 +238,13 @@ open class Note(val idHex: String) { } fun hasAnyReports(): Boolean { - val dayAgo = Date().time / 1000 - 24*60*60 + val dayAgo = Date().time / 1000 - 24 * 60 * 60 return reports.isNotEmpty() || - (author?.reports?.values?.filter { - it.firstOrNull { ( it.createdAt() ?: 0 ) > dayAgo } != null - }?.isNotEmpty() ?: false) + ( + author?.reports?.values?.filter { + it.firstOrNull { (it.createdAt() ?: 0) > dayAgo } != null + }?.isNotEmpty() ?: false + ) } fun directlyCiteUsersHex(): Set { @@ -257,7 +257,6 @@ open class Note(val idHex: String) { returningList.add(tag[1]) } } catch (e: Exception) { - } } return returningList @@ -275,17 +274,16 @@ open class Note(val idHex: String) { } } } catch (e: Exception) { - } } return returningList } fun directlyCites(userProfile: User): Boolean { - return author == userProfile - || (userProfile in directlyCiteUsers()) - || (event is ReactionEvent && replyTo?.lastOrNull()?.directlyCites(userProfile) == true) - || (event is RepostEvent && replyTo?.lastOrNull()?.directlyCites(userProfile) == true) + return author == userProfile || + (userProfile in directlyCiteUsers()) || + (event is ReactionEvent && replyTo?.lastOrNull()?.directlyCites(userProfile) == true) || + (event is RepostEvent && replyTo?.lastOrNull()?.directlyCites(userProfile) == true) } fun isNewThread(): Boolean { @@ -306,7 +304,7 @@ open class Note(val idHex: String) { fun hasBoostedInTheLast5Minutes(loggedIn: User): Boolean { val currentTime = Date().time / 1000 - return boosts.firstOrNull { it.author == loggedIn && (it.createdAt() ?: 0) > currentTime - (60 * 5)} != null // 5 minute protection + return boosts.firstOrNull { it.author == loggedIn && (it.createdAt() ?: 0) > currentTime - (60 * 5) } != null // 5 minute protection } fun boostedBy(loggedIn: User): List { @@ -329,7 +327,6 @@ open class Note(val idHex: String) { } } - class NoteLiveSet(u: Note) { // Observers line up here. val metadata: NoteLiveData = NoteLiveData(u) @@ -342,17 +339,17 @@ class NoteLiveSet(u: Note) { val zaps: NoteLiveData = NoteLiveData(u) fun isInUse(): Boolean { - return metadata.hasObservers() - || reactions.hasObservers() - || boosts.hasObservers() - || replies.hasObservers() - || reports.hasObservers() - || relays.hasObservers() - || zaps.hasObservers() + return metadata.hasObservers() || + reactions.hasObservers() || + boosts.hasObservers() || + replies.hasObservers() || + reports.hasObservers() || + relays.hasObservers() || + zaps.hasObservers() } } -class NoteLiveData(val note: Note): LiveData(NoteState(note)) { +class NoteLiveData(val note: Note) : LiveData(NoteState(note)) { // Refreshes observers in batches. var handlerWaiting = AtomicBoolean() @@ -384,7 +381,6 @@ class NoteLiveData(val note: Note): LiveData(NoteState(note)) { } else { NostrSingleEventDataSource.add(note) } - } override fun onInactive() { diff --git a/app/src/main/java/com/vitorpamplona/amethyst/model/ThreadAssembler.kt b/app/src/main/java/com/vitorpamplona/amethyst/model/ThreadAssembler.kt index 6c3bde56c..8b7617784 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/model/ThreadAssembler.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/model/ThreadAssembler.kt @@ -6,71 +6,72 @@ import kotlin.time.measureTimedValue class ThreadAssembler { - fun searchRoot(note: Note, testedNotes: MutableSet = mutableSetOf()): Note? { - if (note.replyTo == null || note.replyTo?.isEmpty() == true) return note + fun searchRoot(note: Note, testedNotes: MutableSet = mutableSetOf()): Note? { + if (note.replyTo == null || note.replyTo?.isEmpty() == true) return note - testedNotes.add(note) + testedNotes.add(note) - val markedAsRoot = note.event?.tags()?.firstOrNull { it[0] == "e" && it.size > 3 && it[3] == "root" }?.getOrNull(1) - if (markedAsRoot != null) return LocalCache.checkGetOrCreateNote(markedAsRoot) + val markedAsRoot = note.event?.tags()?.firstOrNull { it[0] == "e" && it.size > 3 && it[3] == "root" }?.getOrNull(1) + if (markedAsRoot != null) return LocalCache.checkGetOrCreateNote(markedAsRoot) - val hasNoReplyTo = note.replyTo?.firstOrNull { it.replyTo?.isEmpty() == true } - if (hasNoReplyTo != null) return hasNoReplyTo + val hasNoReplyTo = note.replyTo?.firstOrNull { it.replyTo?.isEmpty() == true } + if (hasNoReplyTo != null) return hasNoReplyTo - // recursive - val roots = note.replyTo?.map { - if (it !in testedNotes) - searchRoot(it, testedNotes) - else - null - }?.filterNotNull() + // recursive + val roots = note.replyTo?.map { + if (it !in testedNotes) { + searchRoot(it, testedNotes) + } else { + null + } + }?.filterNotNull() - if (roots != null && roots.isNotEmpty()) { - return roots[0] + if (roots != null && roots.isNotEmpty()) { + return roots[0] + } + + return null } - return null - } + @OptIn(ExperimentalTime::class) + fun findThreadFor(noteId: String): Set { + val (result, elapsed) = measureTimedValue { + val note = if (noteId.contains(":")) { + val aTag = ATag.parse(noteId, null) + if (aTag != null) { + LocalCache.getOrCreateAddressableNote(aTag) + } else { + return emptySet() + } + } else { + LocalCache.getOrCreateNote(noteId) + } - @OptIn(ExperimentalTime::class) - fun findThreadFor(noteId: String): Set { - val (result, elapsed) = measureTimedValue { - val note = if (noteId.contains(":")) { - val aTag = ATag.parse(noteId, null) - if (aTag != null) - LocalCache.getOrCreateAddressableNote(aTag) - else - return emptySet() - } else { - LocalCache.getOrCreateNote(noteId) - } + if (note.event != null) { + val thread = mutableSetOf() + val threadRoot = searchRoot(note, thread) ?: note - if (note.event != null) { - val thread = mutableSetOf() + loadDown(threadRoot, thread) - val threadRoot = searchRoot(note, thread) ?: note + thread.toSet() + } else { + setOf(note) + } + } - loadDown(threadRoot, thread) + println("Model Refresh: Thread loaded in $elapsed") - thread.toSet() - } else { - setOf(note) - } + return result } - println("Model Refresh: Thread loaded in ${elapsed}") + fun loadDown(note: Note, thread: MutableSet) { + if (note !in thread) { + thread.add(note) - return result - } - - fun loadDown(note: Note, thread: MutableSet) { - if (note !in thread) { - thread.add(note) - - note.replies.forEach { - loadDown(it, thread) - } + note.replies.forEach { + loadDown(it, thread) + } + } } - } } diff --git a/app/src/main/java/com/vitorpamplona/amethyst/service/Nip19.kt b/app/src/main/java/com/vitorpamplona/amethyst/service/Nip19.kt index 00db14164..73b0a56bc 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/service/Nip19.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/service/Nip19.kt @@ -7,132 +7,132 @@ import java.nio.ByteOrder class Nip19 { - enum class Type { - USER, NOTE, RELAY, ADDRESS - } - - data class Return(val type: Type, val hex: String, val relay: String?) - - fun uriToRoute(uri: String?): Return? { - try { - val key = uri?.removePrefix("nostr:") ?: return null - - val bytes = key.bechToBytes() - if (key.startsWith("npub")) { - return npub(bytes) - } else if (key.startsWith("note")) { - return note(bytes) - } else if (key.startsWith("nprofile")) { - return nprofile(bytes) - } else if (key.startsWith("nevent")) { - return nevent(bytes) - } else if (key.startsWith("nrelay")) { - return nrelay(bytes) - } else if (key.startsWith("naddr")) { - return naddr(bytes) - } - } catch (e: Throwable) { - println("Issue trying to Decode NIP19 ${uri}: ${e.message}") + enum class Type { + USER, NOTE, RELAY, ADDRESS } - return null - } + data class Return(val type: Type, val hex: String, val relay: String?) - private fun npub(bytes: ByteArray): Return { - return Return(Type.USER, bytes.toHexKey(), null) - } + fun uriToRoute(uri: String?): Return? { + try { + val key = uri?.removePrefix("nostr:") ?: return null - private fun note(bytes: ByteArray): Return { - return Return(Type.NOTE, bytes.toHexKey(), null); - } + val bytes = key.bechToBytes() + if (key.startsWith("npub")) { + return npub(bytes) + } else if (key.startsWith("note")) { + return note(bytes) + } else if (key.startsWith("nprofile")) { + return nprofile(bytes) + } else if (key.startsWith("nevent")) { + return nevent(bytes) + } else if (key.startsWith("nrelay")) { + return nrelay(bytes) + } else if (key.startsWith("naddr")) { + return naddr(bytes) + } + } catch (e: Throwable) { + println("Issue trying to Decode NIP19 $uri: ${e.message}") + } - private fun nprofile(bytes: ByteArray): Return? { - val tlv = parseTLV(bytes) + return null + } - val hex = tlv.get(NIP19TLVTypes.SPECIAL.id) - ?.get(0) - ?.toHexKey() ?: return null + private fun npub(bytes: ByteArray): Return { + return Return(Type.USER, bytes.toHexKey(), null) + } - val relay = tlv.get(NIP19TLVTypes.RELAY.id) - ?.get(0) - ?.toString(Charsets.UTF_8) + private fun note(bytes: ByteArray): Return { + return Return(Type.NOTE, bytes.toHexKey(), null) + } - return Return(Type.USER, hex, relay) - } + private fun nprofile(bytes: ByteArray): Return? { + val tlv = parseTLV(bytes) - private fun nevent(bytes: ByteArray): Return? { - val tlv = parseTLV(bytes) + val hex = tlv.get(NIP19TLVTypes.SPECIAL.id) + ?.get(0) + ?.toHexKey() ?: return null - val hex = tlv.get(NIP19TLVTypes.SPECIAL.id) - ?.get(0) - ?.toHexKey() ?: return null + val relay = tlv.get(NIP19TLVTypes.RELAY.id) + ?.get(0) + ?.toString(Charsets.UTF_8) - val relay = tlv.get(NIP19TLVTypes.RELAY.id) - ?.get(0) - ?.toString(Charsets.UTF_8) + return Return(Type.USER, hex, relay) + } - return Return(Type.USER, hex, relay) - } + private fun nevent(bytes: ByteArray): Return? { + val tlv = parseTLV(bytes) - private fun nrelay(bytes: ByteArray): Return? { - val relayUrl = parseTLV(bytes) - .get(NIP19TLVTypes.SPECIAL.id) - ?.get(0) - ?.toString(Charsets.UTF_8) ?: return null + val hex = tlv.get(NIP19TLVTypes.SPECIAL.id) + ?.get(0) + ?.toHexKey() ?: return null - return Return(Type.RELAY, relayUrl, null) - } + val relay = tlv.get(NIP19TLVTypes.RELAY.id) + ?.get(0) + ?.toString(Charsets.UTF_8) - private fun naddr(bytes: ByteArray): Return? { - val tlv = parseTLV(bytes) + return Return(Type.USER, hex, relay) + } - val d = tlv.get(NIP19TLVTypes.SPECIAL.id) - ?.get(0) - ?.toString(Charsets.UTF_8) ?: return null + private fun nrelay(bytes: ByteArray): Return? { + val relayUrl = parseTLV(bytes) + .get(NIP19TLVTypes.SPECIAL.id) + ?.get(0) + ?.toString(Charsets.UTF_8) ?: return null - val relay = tlv.get(NIP19TLVTypes.RELAY.id) - ?.get(0) - ?.toString(Charsets.UTF_8) + return Return(Type.RELAY, relayUrl, null) + } - val author = tlv.get(NIP19TLVTypes.AUTHOR.id) - ?.get(0) - ?.toHexKey() + private fun naddr(bytes: ByteArray): Return? { + val tlv = parseTLV(bytes) - val kind = tlv.get(NIP19TLVTypes.KIND.id) - ?.get(0) - ?.let { toInt32(it) } + val d = tlv.get(NIP19TLVTypes.SPECIAL.id) + ?.get(0) + ?.toString(Charsets.UTF_8) ?: return null - return Return(Type.ADDRESS, "$kind:$author:$d", relay) - } + val relay = tlv.get(NIP19TLVTypes.RELAY.id) + ?.get(0) + ?.toString(Charsets.UTF_8) + + val author = tlv.get(NIP19TLVTypes.AUTHOR.id) + ?.get(0) + ?.toHexKey() + + val kind = tlv.get(NIP19TLVTypes.KIND.id) + ?.get(0) + ?.let { toInt32(it) } + + return Return(Type.ADDRESS, "$kind:$author:$d", relay) + } } // Classes should start with an uppercase letter in kotlin enum class NIP19TLVTypes(val id: Byte) { - SPECIAL(0), - RELAY(1), - AUTHOR(2), - KIND(3); + SPECIAL(0), + RELAY(1), + AUTHOR(2), + KIND(3); } fun toInt32(bytes: ByteArray): Int { - require(bytes.size == 4) { "length must be 4, got: ${bytes.size}" } - return ByteBuffer.wrap(bytes, 0, 4).order(ByteOrder.BIG_ENDIAN).int + require(bytes.size == 4) { "length must be 4, got: ${bytes.size}" } + return ByteBuffer.wrap(bytes, 0, 4).order(ByteOrder.BIG_ENDIAN).int } fun parseTLV(data: ByteArray): Map> { - val result = mutableMapOf>() - var rest = data - while (rest.isNotEmpty()) { - val t = rest[0] - val l = rest[1] - val v = rest.sliceArray(IntRange(2, (2 + l) - 1)) - rest = rest.sliceArray(IntRange(2 + l, rest.size - 1)) - if (v.size < l) continue + val result = mutableMapOf>() + var rest = data + while (rest.isNotEmpty()) { + val t = rest[0] + val l = rest[1] + val v = rest.sliceArray(IntRange(2, (2 + l) - 1)) + rest = rest.sliceArray(IntRange(2 + l, rest.size - 1)) + if (v.size < l) continue - if (!result.containsKey(t)) { - result[t] = mutableListOf() + if (!result.containsKey(t)) { + result[t] = mutableListOf() + } + result[t]?.add(v) } - result[t]?.add(v) - } - return result + return result } diff --git a/app/src/main/java/com/vitorpamplona/amethyst/service/NostrUserProfileDataSource.kt b/app/src/main/java/com/vitorpamplona/amethyst/service/NostrUserProfileDataSource.kt index cace13279..39c85a97b 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/service/NostrUserProfileDataSource.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/service/NostrUserProfileDataSource.kt @@ -4,114 +4,114 @@ import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.amethyst.model.User import com.vitorpamplona.amethyst.service.model.BadgeAwardEvent import com.vitorpamplona.amethyst.service.model.BadgeProfilesEvent +import com.vitorpamplona.amethyst.service.model.ContactListEvent import com.vitorpamplona.amethyst.service.model.LnZapEvent import com.vitorpamplona.amethyst.service.model.LongTextNoteEvent -import com.vitorpamplona.amethyst.service.relays.FeedType -import com.vitorpamplona.amethyst.service.relays.TypedFilter -import com.vitorpamplona.amethyst.service.relays.JsonFilter -import com.vitorpamplona.amethyst.service.model.ContactListEvent import com.vitorpamplona.amethyst.service.model.MetadataEvent import com.vitorpamplona.amethyst.service.model.TextNoteEvent +import com.vitorpamplona.amethyst.service.relays.FeedType +import com.vitorpamplona.amethyst.service.relays.JsonFilter +import com.vitorpamplona.amethyst.service.relays.TypedFilter -object NostrUserProfileDataSource: NostrDataSource("UserProfileFeed") { - var user: User? = null +object NostrUserProfileDataSource : NostrDataSource("UserProfileFeed") { + var user: User? = null - fun loadUserProfile(userId: String?) { - if (userId != null) { - user = LocalCache.getOrCreateUser(userId) - } else { - user = null + fun loadUserProfile(userId: String?) { + if (userId != null) { + user = LocalCache.getOrCreateUser(userId) + } else { + user = null + } + + resetFilters() } - resetFilters() - } + fun createUserInfoFilter() = user?.let { + TypedFilter( + types = FeedType.values().toSet(), + filter = JsonFilter( + kinds = listOf(MetadataEvent.kind), + authors = listOf(it.pubkeyHex), + limit = 1 + ) + ) + } - fun createUserInfoFilter() = user?.let { - TypedFilter( - types = FeedType.values().toSet(), - filter = JsonFilter( - kinds = listOf(MetadataEvent.kind), - authors = listOf(it.pubkeyHex), - limit = 1 - ) - ) - } + fun createUserPostsFilter() = user?.let { + TypedFilter( + types = FeedType.values().toSet(), + filter = JsonFilter( + kinds = listOf(TextNoteEvent.kind, LongTextNoteEvent.kind), + authors = listOf(it.pubkeyHex), + limit = 200 + ) + ) + } - fun createUserPostsFilter() = user?.let { - TypedFilter( - types = FeedType.values().toSet(), - filter = JsonFilter( - kinds = listOf(TextNoteEvent.kind, LongTextNoteEvent.kind), - authors = listOf(it.pubkeyHex), - limit = 200 - ) - ) - } + fun createUserReceivedZapsFilter() = user?.let { + TypedFilter( + types = FeedType.values().toSet(), + filter = JsonFilter( + kinds = listOf(LnZapEvent.kind), + tags = mapOf("p" to listOf(it.pubkeyHex)) + ) + ) + } - fun createUserReceivedZapsFilter() = user?.let { - TypedFilter( - types = FeedType.values().toSet(), - filter = JsonFilter( - kinds = listOf(LnZapEvent.kind), - tags = mapOf("p" to listOf(it.pubkeyHex)) - ) - ) - } + fun createFollowFilter() = user?.let { + TypedFilter( + types = FeedType.values().toSet(), + filter = JsonFilter( + kinds = listOf(ContactListEvent.kind), + authors = listOf(it.pubkeyHex), + limit = 1 + ) + ) + } - fun createFollowFilter() = user?.let { - TypedFilter( - types = FeedType.values().toSet(), - filter = JsonFilter( - kinds = listOf(ContactListEvent.kind), - authors = listOf(it.pubkeyHex), - limit = 1 - ) - ) - } + fun createFollowersFilter() = user?.let { + TypedFilter( + types = FeedType.values().toSet(), + filter = JsonFilter( + kinds = listOf(ContactListEvent.kind), + tags = mapOf("p" to listOf(it.pubkeyHex)) + ) + ) + } - fun createFollowersFilter() = user?.let { - TypedFilter( - types = FeedType.values().toSet(), - filter = JsonFilter( - kinds = listOf(ContactListEvent.kind), - tags = mapOf("p" to listOf(it.pubkeyHex)) - ) - ) - } + fun createAcceptedAwardsFilter() = user?.let { + TypedFilter( + types = FeedType.values().toSet(), + filter = JsonFilter( + kinds = listOf(BadgeProfilesEvent.kind), + authors = listOf(it.pubkeyHex), + limit = 1 + ) + ) + } - fun createAcceptedAwardsFilter() = user?.let { - TypedFilter( - types = FeedType.values().toSet(), - filter = JsonFilter( - kinds = listOf(BadgeProfilesEvent.kind), - authors = listOf(it.pubkeyHex), - limit = 1 - ) - ) - } + fun createReceivedAwardsFilter() = user?.let { + TypedFilter( + types = FeedType.values().toSet(), + filter = JsonFilter( + kinds = listOf(BadgeAwardEvent.kind), + tags = mapOf("p" to listOf(it.pubkeyHex)), + limit = 20 + ) + ) + } - fun createReceivedAwardsFilter() = user?.let { - TypedFilter( - types = FeedType.values().toSet(), - filter = JsonFilter( - kinds = listOf(BadgeAwardEvent.kind), - tags = mapOf("p" to listOf(it.pubkeyHex)), - limit = 20 - ) - ) - } + val userInfoChannel = requestNewChannel() - val userInfoChannel = requestNewChannel() - - override fun updateChannelFilters() { - userInfoChannel.typedFilters = listOfNotNull( - createUserInfoFilter(), - createUserPostsFilter(), - createFollowFilter(), - createFollowersFilter(), - createUserReceivedZapsFilter(), - createAcceptedAwardsFilter(), - createReceivedAwardsFilter() - ).ifEmpty { null } - } -} \ No newline at end of file + override fun updateChannelFilters() { + userInfoChannel.typedFilters = listOfNotNull( + createUserInfoFilter(), + createUserPostsFilter(), + createFollowFilter(), + createFollowersFilter(), + createUserReceivedZapsFilter(), + createAcceptedAwardsFilter(), + createReceivedAwardsFilter() + ).ifEmpty { null } + } +} diff --git a/app/src/main/java/com/vitorpamplona/amethyst/service/model/ATag.kt b/app/src/main/java/com/vitorpamplona/amethyst/service/model/ATag.kt index ebdd47928..f54a4c886 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/service/model/ATag.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/service/model/ATag.kt @@ -23,8 +23,9 @@ data class ATag(val kind: Int, val pubKeyHex: String, val dTag: String, val rela var fullArray = byteArrayOf(NIP19TLVTypes.SPECIAL.id, dTag.size.toByte()) + dTag - if (relay != null) + if (relay != null) { fullArray = fullArray + byteArrayOf(NIP19TLVTypes.RELAY.id, relay.size.toByte()) + relay + } fullArray = fullArray + byteArrayOf(NIP19TLVTypes.AUTHOR.id, author.size.toByte()) + author + @@ -39,19 +40,20 @@ data class ATag(val kind: Int, val pubKeyHex: String, val dTag: String, val rela } fun parse(address: String, relay: String?): ATag? { - return if (address.startsWith("naddr") || address.startsWith("nostr:naddr")) + return if (address.startsWith("naddr") || address.startsWith("nostr:naddr")) { parseNAddr(address) - else + } else { parseAtag(address, relay) + } } fun parseAtag(atag: String, relay: String?): ATag? { return try { val parts = atag.split(":") - Hex.decode(parts[1]) - ATag(parts[0].toInt(), parts[1], parts[2], relay) + Hex.decode(parts[1]) + ATag(parts[0].toInt(), parts[1], parts[2], relay) } catch (t: Throwable) { - Log.w("ATag", "Error parsing A Tag: ${atag}: ${t.message}") + Log.w("ATag", "Error parsing A Tag: $atag: ${t.message}") null } } @@ -67,16 +69,16 @@ data class ATag(val kind: Int, val pubKeyHex: String, val dTag: String, val rela val author = tlv.get(NIP19TLVTypes.AUTHOR.id)?.get(0)?.toHexKey() val kind = tlv.get(NIP19TLVTypes.KIND.id)?.get(0)?.let { toInt32(it) } - if (kind != null && author != null) + if (kind != null && author != null) { return ATag(kind, author, d, relay) + } } - } catch (e: Throwable) { - Log.w( "ATag", "Issue trying to Decode NIP19 ${this}: ${e.message}") - //e.printStackTrace() + Log.w("ATag", "Issue trying to Decode NIP19 $this: ${e.message}") + // e.printStackTrace() } return null } } -} \ No newline at end of file +} diff --git a/app/src/main/java/com/vitorpamplona/amethyst/service/model/LnZapEvent.kt b/app/src/main/java/com/vitorpamplona/amethyst/service/model/LnZapEvent.kt index 20448a433..7d5f949c9 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/service/model/LnZapEvent.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/service/model/LnZapEvent.kt @@ -6,57 +6,57 @@ import com.vitorpamplona.amethyst.service.relays.Client import java.math.BigDecimal class LnZapEvent( - id: HexKey, - pubKey: HexKey, - createdAt: Long, - tags: List>, - content: String, - sig: HexKey -): LnZapEventInterface, Event(id, pubKey, createdAt, kind, tags, content, sig) { + id: HexKey, + pubKey: HexKey, + createdAt: Long, + tags: List>, + content: String, + sig: HexKey +) : LnZapEventInterface, Event(id, pubKey, createdAt, kind, tags, content, sig) { - override fun zappedPost() = tags - .filter { it.firstOrNull() == "e" } - .mapNotNull { it.getOrNull(1) } + override fun zappedPost() = tags + .filter { it.firstOrNull() == "e" } + .mapNotNull { it.getOrNull(1) } - override fun zappedAuthor() = tags - .filter { it.firstOrNull() == "p" } - .mapNotNull { it.getOrNull(1) } + override fun zappedAuthor() = tags + .filter { it.firstOrNull() == "p" } + .mapNotNull { it.getOrNull(1) } - override fun taggedAddresses(): List = tags - .filter { it.firstOrNull() == "a" } - .mapNotNull { - val aTagValue = it.getOrNull(1) - val relay = it.getOrNull(2) + override fun taggedAddresses(): List = tags + .filter { it.firstOrNull() == "a" } + .mapNotNull { + val aTagValue = it.getOrNull(1) + val relay = it.getOrNull(2) - if (aTagValue != null) ATag.parse(aTagValue, relay) else null + if (aTagValue != null) ATag.parse(aTagValue, relay) else null + } + + override fun amount(): BigDecimal? { + return lnInvoice()?.let { LnInvoiceUtil.getAmountInSats(it) } } - override fun amount(): BigDecimal? { - return lnInvoice()?.let { LnInvoiceUtil.getAmountInSats(it) } - } + // Keeps this as a field because it's a heavier function used everywhere. + val amount = lnInvoice()?.let { LnInvoiceUtil.getAmountInSats(it) } - // Keeps this as a field because it's a heavier function used everywhere. - val amount = lnInvoice()?.let { LnInvoiceUtil.getAmountInSats(it) } - - override fun containedPost(): Event? = try { - description()?.let { - fromJson(it, Client.lenient) + override fun containedPost(): Event? = try { + description()?.let { + fromJson(it, Client.lenient) + } + } catch (e: Exception) { + null } - } catch (e: Exception) { - null - } - private fun lnInvoice(): String? = tags - .filter { it.firstOrNull() == "bolt11" } - .mapNotNull { it.getOrNull(1) } - .firstOrNull() + private fun lnInvoice(): String? = tags + .filter { it.firstOrNull() == "bolt11" } + .mapNotNull { it.getOrNull(1) } + .firstOrNull() - private fun description(): String? = tags - .filter { it.firstOrNull() == "description" } - .mapNotNull { it.getOrNull(1) } - .firstOrNull() + private fun description(): String? = tags + .filter { it.firstOrNull() == "description" } + .mapNotNull { it.getOrNull(1) } + .firstOrNull() - companion object { - const val kind = 9735 - } + companion object { + const val kind = 9735 + } } diff --git a/app/src/main/java/com/vitorpamplona/amethyst/service/model/LnZapRequestEvent.kt b/app/src/main/java/com/vitorpamplona/amethyst/service/model/LnZapRequestEvent.kt index 4d7f3f911..75ce41ca2 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/service/model/LnZapRequestEvent.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/service/model/LnZapRequestEvent.kt @@ -2,59 +2,58 @@ package com.vitorpamplona.amethyst.service.model import com.vitorpamplona.amethyst.model.HexKey import com.vitorpamplona.amethyst.model.toHexKey -import java.util.Date import nostr.postr.Utils -import nostr.postr.toHex +import java.util.Date -class LnZapRequestEvent ( - id: HexKey, - pubKey: HexKey, - createdAt: Long, - tags: List>, - content: String, - sig: HexKey -): Event(id, pubKey, createdAt, kind, tags, content, sig) { - fun zappedPost() = tags.filter { it.firstOrNull() == "e" }.mapNotNull { it.getOrNull(1) } - fun zappedAuthor() = tags.filter { it.firstOrNull() == "p" }.mapNotNull { it.getOrNull(1) } - fun taggedAddresses() = tags.filter { it.firstOrNull() == "a" }.mapNotNull { - val aTagValue = it.getOrNull(1) - val relay = it.getOrNull(2) +class LnZapRequestEvent( + id: HexKey, + pubKey: HexKey, + createdAt: Long, + tags: List>, + content: String, + sig: HexKey +) : Event(id, pubKey, createdAt, kind, tags, content, sig) { + fun zappedPost() = tags.filter { it.firstOrNull() == "e" }.mapNotNull { it.getOrNull(1) } + fun zappedAuthor() = tags.filter { it.firstOrNull() == "p" }.mapNotNull { it.getOrNull(1) } + fun taggedAddresses() = tags.filter { it.firstOrNull() == "a" }.mapNotNull { + val aTagValue = it.getOrNull(1) + val relay = it.getOrNull(2) - if (aTagValue != null) ATag.parse(aTagValue, relay) else null - } - - companion object { - const val kind = 9734 - - fun create(originalNote: EventInterface, relays: Set, privateKey: ByteArray, createdAt: Long = Date().time / 1000): LnZapRequestEvent { - val content = "" - val pubKey = Utils.pubkeyCreate(privateKey).toHexKey() - var tags = listOf( - listOf("e", originalNote.id()), - listOf("p", originalNote.pubKey()), - listOf("relays") + relays - ) - if (originalNote is LongTextNoteEvent) { - tags = tags + listOf( listOf("a", originalNote.address().toTag()) ) - } - - val id = generateId(pubKey, createdAt, kind, tags, content) - val sig = Utils.sign(id, privateKey) - return LnZapRequestEvent(id.toHexKey(), pubKey, createdAt, tags, content, sig.toHexKey()) + if (aTagValue != null) ATag.parse(aTagValue, relay) else null } - fun create(userHex: String, relays: Set, privateKey: ByteArray, createdAt: Long = Date().time / 1000): LnZapRequestEvent { - val content = "" - val pubKey = Utils.pubkeyCreate(privateKey).toHexKey() - val tags = listOf( - listOf("p", userHex), - listOf("relays") + relays - ) - val id = generateId(pubKey, createdAt, kind, tags, content) - val sig = Utils.sign(id, privateKey) - return LnZapRequestEvent(id.toHexKey(), pubKey, createdAt, tags, content, sig.toHexKey()) + companion object { + const val kind = 9734 + + fun create(originalNote: EventInterface, relays: Set, privateKey: ByteArray, createdAt: Long = Date().time / 1000): LnZapRequestEvent { + val content = "" + val pubKey = Utils.pubkeyCreate(privateKey).toHexKey() + var tags = listOf( + listOf("e", originalNote.id()), + listOf("p", originalNote.pubKey()), + listOf("relays") + relays + ) + if (originalNote is LongTextNoteEvent) { + tags = tags + listOf(listOf("a", originalNote.address().toTag())) + } + + val id = generateId(pubKey, createdAt, kind, tags, content) + val sig = Utils.sign(id, privateKey) + return LnZapRequestEvent(id.toHexKey(), pubKey, createdAt, tags, content, sig.toHexKey()) + } + + fun create(userHex: String, relays: Set, privateKey: ByteArray, createdAt: Long = Date().time / 1000): LnZapRequestEvent { + val content = "" + val pubKey = Utils.pubkeyCreate(privateKey).toHexKey() + val tags = listOf( + listOf("p", userHex), + listOf("relays") + relays + ) + val id = generateId(pubKey, createdAt, kind, tags, content) + val sig = Utils.sign(id, privateKey) + return LnZapRequestEvent(id.toHexKey(), pubKey, createdAt, tags, content, sig.toHexKey()) + } } - } } /* diff --git a/app/src/main/java/com/vitorpamplona/amethyst/service/model/ReactionEvent.kt b/app/src/main/java/com/vitorpamplona/amethyst/service/model/ReactionEvent.kt index 5525ff7f5..75c3eee5b 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/service/model/ReactionEvent.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/service/model/ReactionEvent.kt @@ -2,50 +2,49 @@ package com.vitorpamplona.amethyst.service.model import com.vitorpamplona.amethyst.model.HexKey import com.vitorpamplona.amethyst.model.toHexKey -import java.util.Date import nostr.postr.Utils -import nostr.postr.toHex +import java.util.Date -class ReactionEvent ( - id: HexKey, - pubKey: HexKey, - createdAt: Long, - tags: List>, - content: String, - sig: HexKey -): Event(id, pubKey, createdAt, kind, tags, content, sig) { +class ReactionEvent( + id: HexKey, + pubKey: HexKey, + createdAt: Long, + tags: List>, + content: String, + sig: HexKey +) : Event(id, pubKey, createdAt, kind, tags, content, sig) { - fun originalPost() = tags.filter { it.firstOrNull() == "e" }.mapNotNull { it.getOrNull(1) } - fun originalAuthor() = tags.filter { it.firstOrNull() == "p" }.mapNotNull { it.getOrNull(1) } - fun taggedAddresses() = tags.filter { it.firstOrNull() == "a" }.mapNotNull { - val aTagValue = it.getOrNull(1) - val relay = it.getOrNull(2) + fun originalPost() = tags.filter { it.firstOrNull() == "e" }.mapNotNull { it.getOrNull(1) } + fun originalAuthor() = tags.filter { it.firstOrNull() == "p" }.mapNotNull { it.getOrNull(1) } + fun taggedAddresses() = tags.filter { it.firstOrNull() == "a" }.mapNotNull { + val aTagValue = it.getOrNull(1) + val relay = it.getOrNull(2) - if (aTagValue != null) ATag.parse(aTagValue, relay) else null - } - - companion object { - const val kind = 7 - - fun createWarning(originalNote: EventInterface, privateKey: ByteArray, createdAt: Long = Date().time / 1000): ReactionEvent { - return create("\u26A0\uFE0F", originalNote, privateKey, createdAt) + if (aTagValue != null) ATag.parse(aTagValue, relay) else null } - fun createLike(originalNote: EventInterface, privateKey: ByteArray, createdAt: Long = Date().time / 1000): ReactionEvent { - return create("+", originalNote, privateKey, createdAt) + companion object { + const val kind = 7 + + fun createWarning(originalNote: EventInterface, privateKey: ByteArray, createdAt: Long = Date().time / 1000): ReactionEvent { + return create("\u26A0\uFE0F", originalNote, privateKey, createdAt) + } + + fun createLike(originalNote: EventInterface, privateKey: ByteArray, createdAt: Long = Date().time / 1000): ReactionEvent { + return create("+", originalNote, privateKey, createdAt) + } + + fun create(content: String, originalNote: EventInterface, privateKey: ByteArray, createdAt: Long = Date().time / 1000): ReactionEvent { + val pubKey = Utils.pubkeyCreate(privateKey).toHexKey() + + var tags = listOf(listOf("e", originalNote.id()), listOf("p", originalNote.pubKey())) + if (originalNote is LongTextNoteEvent) { + tags = tags + listOf(listOf("a", originalNote.address().toTag())) + } + + val id = generateId(pubKey, createdAt, kind, tags, content) + val sig = Utils.sign(id, privateKey) + return ReactionEvent(id.toHexKey(), pubKey, createdAt, tags, content, sig.toHexKey()) + } } - - fun create(content: String, originalNote: EventInterface, privateKey: ByteArray, createdAt: Long = Date().time / 1000): ReactionEvent { - val pubKey = Utils.pubkeyCreate(privateKey).toHexKey() - - var tags = listOf( listOf("e", originalNote.id()), listOf("p", originalNote.pubKey())) - if (originalNote is LongTextNoteEvent) { - tags = tags + listOf( listOf("a", originalNote.address().toTag()) ) - } - - val id = generateId(pubKey, createdAt, kind, tags, content) - val sig = Utils.sign(id, privateKey) - return ReactionEvent(id.toHexKey(), pubKey, createdAt, tags, content, sig.toHexKey()) - } - } } diff --git a/app/src/main/java/com/vitorpamplona/amethyst/service/model/ReportEvent.kt b/app/src/main/java/com/vitorpamplona/amethyst/service/model/ReportEvent.kt index 8ca58d7d8..4b2021c26 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/service/model/ReportEvent.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/service/model/ReportEvent.kt @@ -2,99 +2,98 @@ package com.vitorpamplona.amethyst.service.model import com.vitorpamplona.amethyst.model.HexKey import com.vitorpamplona.amethyst.model.toHexKey -import java.util.Date import nostr.postr.Utils -import nostr.postr.toHex +import java.util.Date data class ReportedKey(val key: String, val reportType: ReportEvent.ReportType) // NIP 56 event. -class ReportEvent ( - id: HexKey, - pubKey: HexKey, - createdAt: Long, - tags: List>, - content: String, - sig: HexKey -): Event(id, pubKey, createdAt, kind, tags, content, sig) { +class ReportEvent( + id: HexKey, + pubKey: HexKey, + createdAt: Long, + tags: List>, + content: String, + sig: HexKey +) : Event(id, pubKey, createdAt, kind, tags, content, sig) { - private fun defaultReportType(): ReportType { - // Works with old and new structures for report. - var reportType = tags.filter { it.firstOrNull() == "report" }.mapNotNull { it.getOrNull(1) }.map { ReportType.valueOf(it.toUpperCase()) }.firstOrNull() - if (reportType == null) { - reportType = tags.mapNotNull { it.getOrNull(2) }.map { ReportType.valueOf(it.toUpperCase()) }.firstOrNull() - } - if (reportType == null) { - reportType = ReportType.SPAM - } - return reportType - } - - fun reportedPost() = tags - .filter { it.firstOrNull() == "e" && it.getOrNull(1) != null } - .map { - ReportedKey( - it[1], - it.getOrNull(2)?.toUpperCase()?.let { it1 -> ReportType.valueOf(it1) }?: defaultReportType() - ) + private fun defaultReportType(): ReportType { + // Works with old and new structures for report. + var reportType = tags.filter { it.firstOrNull() == "report" }.mapNotNull { it.getOrNull(1) }.map { ReportType.valueOf(it.toUpperCase()) }.firstOrNull() + if (reportType == null) { + reportType = tags.mapNotNull { it.getOrNull(2) }.map { ReportType.valueOf(it.toUpperCase()) }.firstOrNull() + } + if (reportType == null) { + reportType = ReportType.SPAM + } + return reportType } - fun reportedAuthor() = tags - .filter { it.firstOrNull() == "p" && it.getOrNull(1) != null } - .map { - ReportedKey( - it[1], - it.getOrNull(2)?.toUpperCase()?.let { it1 -> ReportType.valueOf(it1) }?: defaultReportType() - ) + fun reportedPost() = tags + .filter { it.firstOrNull() == "e" && it.getOrNull(1) != null } + .map { + ReportedKey( + it[1], + it.getOrNull(2)?.toUpperCase()?.let { it1 -> ReportType.valueOf(it1) } ?: defaultReportType() + ) + } + + fun reportedAuthor() = tags + .filter { it.firstOrNull() == "p" && it.getOrNull(1) != null } + .map { + ReportedKey( + it[1], + it.getOrNull(2)?.toUpperCase()?.let { it1 -> ReportType.valueOf(it1) } ?: defaultReportType() + ) + } + + fun taggedAddresses() = tags.filter { it.firstOrNull() == "a" }.mapNotNull { + val aTagValue = it.getOrNull(1) + val relay = it.getOrNull(2) + + if (aTagValue != null) ATag.parse(aTagValue, relay) else null } - fun taggedAddresses() = tags.filter { it.firstOrNull() == "a" }.mapNotNull { - val aTagValue = it.getOrNull(1) - val relay = it.getOrNull(2) + companion object { + const val kind = 1984 - if (aTagValue != null) ATag.parse(aTagValue, relay) else null - } + fun create(reportedPost: EventInterface, type: ReportType, privateKey: ByteArray, createdAt: Long = Date().time / 1000): ReportEvent { + val content = "" - companion object { - const val kind = 1984 + val reportPostTag = listOf("e", reportedPost.id(), type.name.lowercase()) + val reportAuthorTag = listOf("p", reportedPost.pubKey(), type.name.lowercase()) - fun create(reportedPost: EventInterface, type: ReportType, privateKey: ByteArray, createdAt: Long = Date().time / 1000): ReportEvent { - val content = "" + val pubKey = Utils.pubkeyCreate(privateKey).toHexKey() + var tags: List> = listOf(reportPostTag, reportAuthorTag) - val reportPostTag = listOf("e", reportedPost.id(), type.name.lowercase()) - val reportAuthorTag = listOf("p", reportedPost.pubKey(), type.name.lowercase()) + if (reportedPost is LongTextNoteEvent) { + tags = tags + listOf(listOf("a", reportedPost.address().toTag())) + } - val pubKey = Utils.pubkeyCreate(privateKey).toHexKey() - var tags:List> = listOf(reportPostTag, reportAuthorTag) + val id = generateId(pubKey, createdAt, kind, tags, content) + val sig = Utils.sign(id, privateKey) + return ReportEvent(id.toHexKey(), pubKey, createdAt, tags, content, sig.toHexKey()) + } - if (reportedPost is LongTextNoteEvent) { - tags = tags + listOf( listOf("a", reportedPost.address().toTag()) ) - } + fun create(reportedUser: String, type: ReportType, privateKey: ByteArray, createdAt: Long = Date().time / 1000): ReportEvent { + val content = "" - val id = generateId(pubKey, createdAt, kind, tags, content) - val sig = Utils.sign(id, privateKey) - return ReportEvent(id.toHexKey(), pubKey, createdAt, tags, content, sig.toHexKey()) + val reportAuthorTag = listOf("p", reportedUser, type.name.lowercase()) + + val pubKey = Utils.pubkeyCreate(privateKey).toHexKey() + val tags: List> = listOf(reportAuthorTag) + val id = generateId(pubKey, createdAt, kind, tags, content) + val sig = Utils.sign(id, privateKey) + return ReportEvent(id.toHexKey(), pubKey, createdAt, tags, content, sig.toHexKey()) + } } - fun create(reportedUser: String, type: ReportType, privateKey: ByteArray, createdAt: Long = Date().time / 1000): ReportEvent { - val content = "" - - val reportAuthorTag = listOf("p", reportedUser, type.name.lowercase()) - - val pubKey = Utils.pubkeyCreate(privateKey).toHexKey() - val tags:List> = listOf(reportAuthorTag) - val id = generateId(pubKey, createdAt, kind, tags, content) - val sig = Utils.sign(id, privateKey) - return ReportEvent(id.toHexKey(), pubKey, createdAt, tags, content, sig.toHexKey()) + enum class ReportType() { + EXPLICIT, // Not used anymore. + ILLEGAL, + SPAM, + IMPERSONATION, + NUDITY, + PROFANITY } - } - - enum class ReportType() { - EXPLICIT, // Not used anymore. - ILLEGAL, - SPAM, - IMPERSONATION, - NUDITY, - PROFANITY, - } } diff --git a/app/src/main/java/com/vitorpamplona/amethyst/service/model/RepostEvent.kt b/app/src/main/java/com/vitorpamplona/amethyst/service/model/RepostEvent.kt index aec19a407..ce13a6e5a 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/service/model/RepostEvent.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/service/model/RepostEvent.kt @@ -3,53 +3,52 @@ package com.vitorpamplona.amethyst.service.model import com.vitorpamplona.amethyst.model.HexKey import com.vitorpamplona.amethyst.model.toHexKey import com.vitorpamplona.amethyst.service.relays.Client -import java.util.Date import nostr.postr.Utils +import java.util.Date -class RepostEvent ( - id: HexKey, - pubKey: HexKey, - createdAt: Long, - tags: List>, - content: String, - sig: HexKey -): Event(id, pubKey, createdAt, kind, tags, content, sig) { +class RepostEvent( + id: HexKey, + pubKey: HexKey, + createdAt: Long, + tags: List>, + content: String, + sig: HexKey +) : Event(id, pubKey, createdAt, kind, tags, content, sig) { + fun boostedPost() = tags.filter { it.firstOrNull() == "e" }.mapNotNull { it.getOrNull(1) } + fun originalAuthor() = tags.filter { it.firstOrNull() == "p" }.mapNotNull { it.getOrNull(1) } + fun taggedAddresses() = tags.filter { it.firstOrNull() == "a" }.mapNotNull { + val aTagValue = it.getOrNull(1) + val relay = it.getOrNull(2) - fun boostedPost() = tags.filter { it.firstOrNull() == "e" }.mapNotNull { it.getOrNull(1) } - fun originalAuthor() = tags.filter { it.firstOrNull() == "p" }.mapNotNull { it.getOrNull(1) } - fun taggedAddresses() = tags.filter { it.firstOrNull() == "a" }.mapNotNull { - val aTagValue = it.getOrNull(1) - val relay = it.getOrNull(2) - - if (aTagValue != null) ATag.parse(aTagValue, relay) else null - } - - fun containedPost() = try { - fromJson(content, Client.lenient) - } catch (e: Exception) { - null - } - - companion object { - const val kind = 6 - - fun create(boostedPost: EventInterface, privateKey: ByteArray, createdAt: Long = Date().time / 1000): RepostEvent { - val content = boostedPost.toJson() - - val replyToPost = listOf("e", boostedPost.id()) - val replyToAuthor = listOf("p", boostedPost.pubKey()) - - val pubKey = Utils.pubkeyCreate(privateKey).toHexKey() - var tags:List> = boostedPost.tags().plus(listOf(replyToPost, replyToAuthor)) - - if (boostedPost is LongTextNoteEvent) { - tags = tags + listOf( listOf("a", boostedPost.address().toTag()) ) - } - - val id = generateId(pubKey, createdAt, kind, tags, content) - val sig = Utils.sign(id, privateKey) - return RepostEvent(id.toHexKey(), pubKey, createdAt, tags, content, sig.toHexKey()) + if (aTagValue != null) ATag.parse(aTagValue, relay) else null + } + + fun containedPost() = try { + fromJson(content, Client.lenient) + } catch (e: Exception) { + null + } + + companion object { + const val kind = 6 + + fun create(boostedPost: EventInterface, privateKey: ByteArray, createdAt: Long = Date().time / 1000): RepostEvent { + val content = boostedPost.toJson() + + val replyToPost = listOf("e", boostedPost.id()) + val replyToAuthor = listOf("p", boostedPost.pubKey()) + + val pubKey = Utils.pubkeyCreate(privateKey).toHexKey() + var tags: List> = boostedPost.tags().plus(listOf(replyToPost, replyToAuthor)) + + if (boostedPost is LongTextNoteEvent) { + tags = tags + listOf(listOf("a", boostedPost.address().toTag())) + } + + val id = generateId(pubKey, createdAt, kind, tags, content) + val sig = Utils.sign(id, privateKey) + return RepostEvent(id.toHexKey(), pubKey, createdAt, tags, content, sig.toHexKey()) + } } - } } diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/components/ClickableRoute.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/components/ClickableRoute.kt index cbd22d9e7..353de6161 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/components/ClickableRoute.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/components/ClickableRoute.kt @@ -15,67 +15,67 @@ import com.vitorpamplona.amethyst.service.model.ChannelCreateEvent @Composable fun ClickableRoute( - nip19: Nip19.Return, - navController: NavController + nip19: Nip19.Return, + navController: NavController ) { - if (nip19.type == Nip19.Type.USER) { - val userBase = LocalCache.getOrCreateUser(nip19.hex) + if (nip19.type == Nip19.Type.USER) { + val userBase = LocalCache.getOrCreateUser(nip19.hex) - val userState by userBase.live().metadata.observeAsState() - val user = userState?.user ?: return + val userState by userBase.live().metadata.observeAsState() + val user = userState?.user ?: return - val route = "User/${nip19.hex}" - val text = user.toBestDisplayName() + val route = "User/${nip19.hex}" + val text = user.toBestDisplayName() - ClickableText( - text = AnnotatedString("@${text} "), - onClick = { navController.navigate(route) }, - style = LocalTextStyle.current.copy(color = MaterialTheme.colors.primary) - ) - } else if (nip19.type == Nip19.Type.ADDRESS) { - val noteBase = LocalCache.checkGetOrCreateAddressableNote(nip19.hex) + ClickableText( + text = AnnotatedString("@$text "), + onClick = { navController.navigate(route) }, + style = LocalTextStyle.current.copy(color = MaterialTheme.colors.primary) + ) + } else if (nip19.type == Nip19.Type.ADDRESS) { + val noteBase = LocalCache.checkGetOrCreateAddressableNote(nip19.hex) - if (noteBase == null) { - Text( - "@${nip19.hex} " - ) + if (noteBase == null) { + Text( + "@${nip19.hex} " + ) + } else { + val noteState by noteBase.live().metadata.observeAsState() + val note = noteState?.note ?: return + + ClickableText( + text = AnnotatedString("@${note.idDisplayNote()} "), + onClick = { navController.navigate("Note/${nip19.hex}") }, + style = LocalTextStyle.current.copy(color = MaterialTheme.colors.primary) + ) + } + } else if (nip19.type == Nip19.Type.NOTE) { + val noteBase = LocalCache.getOrCreateNote(nip19.hex) + val noteState by noteBase.live().metadata.observeAsState() + val note = noteState?.note ?: return + + if (note.event is ChannelCreateEvent) { + ClickableText( + text = AnnotatedString("@${note.idDisplayNote()} "), + onClick = { navController.navigate("Channel/${nip19.hex}") }, + style = LocalTextStyle.current.copy(color = MaterialTheme.colors.primary) + ) + } else if (note.channel() != null) { + ClickableText( + text = AnnotatedString("@${note.channel()?.toBestDisplayName()} "), + onClick = { navController.navigate("Channel/${note.channel()?.idHex}") }, + style = LocalTextStyle.current.copy(color = MaterialTheme.colors.primary) + ) + } else { + ClickableText( + text = AnnotatedString("@${note.idDisplayNote()} "), + onClick = { navController.navigate("Note/${nip19.hex}") }, + style = LocalTextStyle.current.copy(color = MaterialTheme.colors.primary) + ) + } } else { - val noteState by noteBase.live().metadata.observeAsState() - val note = noteState?.note ?: return - - ClickableText( - text = AnnotatedString("@${note.idDisplayNote()} "), - onClick = { navController.navigate("Note/${nip19.hex}") }, - style = LocalTextStyle.current.copy(color = MaterialTheme.colors.primary) - ) + Text( + "@${nip19.hex} " + ) } - } else if (nip19.type == Nip19.Type.NOTE) { - val noteBase = LocalCache.getOrCreateNote(nip19.hex) - val noteState by noteBase.live().metadata.observeAsState() - val note = noteState?.note ?: return - - if (note.event is ChannelCreateEvent) { - ClickableText( - text = AnnotatedString("@${note.idDisplayNote()} "), - onClick = { navController.navigate("Channel/${nip19.hex}") }, - style = LocalTextStyle.current.copy(color = MaterialTheme.colors.primary) - ) - } else if (note.channel() != null) { - ClickableText( - text = AnnotatedString("@${note.channel()?.toBestDisplayName()} "), - onClick = { navController.navigate("Channel/${note.channel()?.idHex}") }, - style = LocalTextStyle.current.copy(color = MaterialTheme.colors.primary) - ) - } else { - ClickableText( - text = AnnotatedString("@${note.idDisplayNote()} "), - onClick = { navController.navigate("Note/${nip19.hex}") }, - style = LocalTextStyle.current.copy(color = MaterialTheme.colors.primary) - ) - } - } else { - Text( - "@${nip19.hex} " - ) - } -} \ No newline at end of file +} diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/components/RichTextViewer.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/components/RichTextViewer.kt index 109a353e9..e658be1c2 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/components/RichTextViewer.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/components/RichTextViewer.kt @@ -8,28 +8,20 @@ import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding import androidx.compose.foundation.shape.RoundedCornerShape -import androidx.compose.material.LocalContentColor import androidx.compose.material.LocalTextStyle import androidx.compose.material.MaterialTheme import androidx.compose.material.Text -import androidx.compose.material.contentColorFor import androidx.compose.runtime.Composable -import androidx.compose.runtime.CompositionLocalProvider import androidx.compose.runtime.getValue import androidx.compose.runtime.livedata.observeAsState -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.remember import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.compositeOver -import androidx.compose.ui.text.Paragraph import androidx.compose.ui.text.SpanStyle import androidx.compose.ui.text.TextStyle import androidx.compose.ui.text.font.FontFamily -import androidx.compose.ui.text.font.FontWeight -import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.text.style.TextDecoration import androidx.compose.ui.text.style.TextDirection import androidx.compose.ui.unit.dp @@ -41,9 +33,9 @@ import com.halilibo.richtext.markdown.MarkdownParseOptions import com.halilibo.richtext.ui.RichTextStyle import com.halilibo.richtext.ui.material.MaterialRichText import com.halilibo.richtext.ui.resolveDefaults -import com.vitorpamplona.amethyst.service.lnurl.LnInvoiceUtil import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.amethyst.service.Nip19 +import com.vitorpamplona.amethyst.service.lnurl.LnInvoiceUtil import com.vitorpamplona.amethyst.ui.note.NoteCompose import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel import java.net.MalformedURLException @@ -61,240 +53,236 @@ val hashTagsPattern: Pattern = Pattern.compile("#([A-Za-z0-9_-]+)") val urlPattern: Pattern = Patterns.WEB_URL fun isValidURL(url: String?): Boolean { - return try { - URL(url).toURI() - true - } catch (e: MalformedURLException) { - false - } catch (e: URISyntaxException) { - false - } + return try { + URL(url).toURI() + true + } catch (e: MalformedURLException) { + false + } catch (e: URISyntaxException) { + false + } } @Composable fun RichTextViewer( - content: String, - canPreview: Boolean, - modifier: Modifier = Modifier, - tags: List>?, - backgroundColor: Color, - accountViewModel: AccountViewModel, - navController: NavController, + content: String, + canPreview: Boolean, + modifier: Modifier = Modifier, + tags: List>?, + backgroundColor: Color, + accountViewModel: AccountViewModel, + navController: NavController ) { - - val myMarkDownStyle = RichTextStyle().resolveDefaults().copy( - codeBlockStyle = RichTextStyle().resolveDefaults().codeBlockStyle?.copy( - textStyle = TextStyle( - fontFamily = FontFamily.Monospace, - fontSize = 14.sp - ), - modifier = Modifier - .padding(0.dp) - .fillMaxWidth() - .clip(shape = RoundedCornerShape(15.dp)) - .border( - 1.dp, - MaterialTheme.colors.onSurface.copy(alpha = 0.12f), - RoundedCornerShape(15.dp) + val myMarkDownStyle = RichTextStyle().resolveDefaults().copy( + codeBlockStyle = RichTextStyle().resolveDefaults().codeBlockStyle?.copy( + textStyle = TextStyle( + fontFamily = FontFamily.Monospace, + fontSize = 14.sp + ), + modifier = Modifier + .padding(0.dp) + .fillMaxWidth() + .clip(shape = RoundedCornerShape(15.dp)) + .border( + 1.dp, + MaterialTheme.colors.onSurface.copy(alpha = 0.12f), + RoundedCornerShape(15.dp) + ) + .background(MaterialTheme.colors.onSurface.copy(alpha = 0.05f).compositeOver(backgroundColor)) + ), + stringStyle = RichTextStyle().resolveDefaults().stringStyle?.copy( + linkStyle = SpanStyle( + textDecoration = TextDecoration.Underline, + color = MaterialTheme.colors.primary + ), + codeStyle = SpanStyle( + fontFamily = FontFamily.Monospace, + fontSize = 14.sp, + background = MaterialTheme.colors.onSurface.copy(alpha = 0.22f).compositeOver(backgroundColor) + ) ) - .background(MaterialTheme.colors.onSurface.copy(alpha = 0.05f).compositeOver(backgroundColor)) - ), - stringStyle = RichTextStyle().resolveDefaults().stringStyle?.copy( - linkStyle = SpanStyle( - textDecoration = TextDecoration.Underline, - color = MaterialTheme.colors.primary - ), - codeStyle = SpanStyle( - fontFamily = FontFamily.Monospace, - fontSize = 14.sp, - background = MaterialTheme.colors.onSurface.copy(alpha = 0.22f).compositeOver(backgroundColor) - ) ) - ) - Column(modifier = modifier.animateContentSize()) { - - if ( content.startsWith("# ") - || content.contains("##") - || content.contains("**") - || content.contains("__") - || content.contains("```") - ) { - - MaterialRichText( - style = myMarkDownStyle, - ) { - Markdown( - content = content, - markdownParseOptions = MarkdownParseOptions.Default, - ) - } - } else { - // FlowRow doesn't work well with paragraphs. So we need to split them - content.split('\n').forEach { paragraph -> - FlowRow() { - val s = if (isArabic(paragraph)) paragraph.split(' ').reversed() else paragraph.split(' '); - s.forEach { word: String -> - if (canPreview) { - // Explicit URL - val lnInvoice = LnInvoiceUtil.findInvoice(word) - if (lnInvoice != null) { - InvoicePreview(lnInvoice) - } else if (isValidURL(word)) { - val removedParamsFromUrl = word.split("?")[0].lowercase() - if (imageExtension.matcher(removedParamsFromUrl).matches()) { - ZoomableImageView(word) - } else if (videoExtension.matcher(removedParamsFromUrl).matches()) { - VideoView(word) - } else { - UrlPreview(word, word) - } - } else if (Patterns.EMAIL_ADDRESS.matcher(word).matches()) { - ClickableEmail(word) - } else if (Patterns.PHONE.matcher(word).matches() && word.length > 6) { - ClickablePhone(word) - } else if (noProtocolUrlValidator.matcher(word).matches()) { - UrlPreview("https://$word", word) - } else if (tagIndex.matcher(word).matches() && tags != null) { - TagLink(word, tags, canPreview, backgroundColor, accountViewModel, navController) - } else if (isBechLink(word)) { - BechLink(word, navController) - } else { - Text( - text = "$word ", - style = LocalTextStyle.current.copy(textDirection = TextDirection.Content), + Column(modifier = modifier.animateContentSize()) { + if (content.startsWith("# ") || + content.contains("##") || + content.contains("**") || + content.contains("__") || + content.contains("```") + ) { + MaterialRichText( + style = myMarkDownStyle + ) { + Markdown( + content = content, + markdownParseOptions = MarkdownParseOptions.Default ) - } - } else { - if (isValidURL(word)) { - ClickableUrl("$word ", word) - } else if (Patterns.EMAIL_ADDRESS.matcher(word).matches()) { - ClickableEmail(word) - } else if (Patterns.PHONE.matcher(word).matches() && word.length > 6) { - ClickablePhone(word) - } else if (noProtocolUrlValidator.matcher(word).matches()) { - ClickableUrl(word, "https://$word") - } else if (tagIndex.matcher(word).matches() && tags != null) { - TagLink(word, tags, canPreview, backgroundColor, accountViewModel, navController) - } else if (isBechLink(word)) { - BechLink(word, navController) - } else { - Text( - text = "$word ", - style = LocalTextStyle.current.copy(textDirection = TextDirection.Content), - ) - } } - } + } else { + // FlowRow doesn't work well with paragraphs. So we need to split them + content.split('\n').forEach { paragraph -> + FlowRow() { + val s = if (isArabic(paragraph)) paragraph.split(' ').reversed() else paragraph.split(' ') + s.forEach { word: String -> + if (canPreview) { + // Explicit URL + val lnInvoice = LnInvoiceUtil.findInvoice(word) + if (lnInvoice != null) { + InvoicePreview(lnInvoice) + } else if (isValidURL(word)) { + val removedParamsFromUrl = word.split("?")[0].lowercase() + if (imageExtension.matcher(removedParamsFromUrl).matches()) { + ZoomableImageView(word) + } else if (videoExtension.matcher(removedParamsFromUrl).matches()) { + VideoView(word) + } else { + UrlPreview(word, word) + } + } else if (Patterns.EMAIL_ADDRESS.matcher(word).matches()) { + ClickableEmail(word) + } else if (Patterns.PHONE.matcher(word).matches() && word.length > 6) { + ClickablePhone(word) + } else if (noProtocolUrlValidator.matcher(word).matches()) { + UrlPreview("https://$word", word) + } else if (tagIndex.matcher(word).matches() && tags != null) { + TagLink(word, tags, canPreview, backgroundColor, accountViewModel, navController) + } else if (isBechLink(word)) { + BechLink(word, navController) + } else { + Text( + text = "$word ", + style = LocalTextStyle.current.copy(textDirection = TextDirection.Content) + ) + } + } else { + if (isValidURL(word)) { + ClickableUrl("$word ", word) + } else if (Patterns.EMAIL_ADDRESS.matcher(word).matches()) { + ClickableEmail(word) + } else if (Patterns.PHONE.matcher(word).matches() && word.length > 6) { + ClickablePhone(word) + } else if (noProtocolUrlValidator.matcher(word).matches()) { + ClickableUrl(word, "https://$word") + } else if (tagIndex.matcher(word).matches() && tags != null) { + TagLink(word, tags, canPreview, backgroundColor, accountViewModel, navController) + } else if (isBechLink(word)) { + BechLink(word, navController) + } else { + Text( + text = "$word ", + style = LocalTextStyle.current.copy(textDirection = TextDirection.Content) + ) + } + } + } + } + } } - } } - } } private fun isArabic(text: String): Boolean { - return text.any { it in '\u0600'..'\u06FF' || it in '\u0750'..'\u077F' } + return text.any { it in '\u0600'..'\u06FF' || it in '\u0750'..'\u077F' } } - fun isBechLink(word: String): Boolean { - return word.startsWith("nostr:", true) - || word.startsWith("npub1", true) - || word.startsWith("naddr1", true) - || word.startsWith("note1", true) - || word.startsWith("nprofile1", true) - || word.startsWith("nevent1", true) - || word.startsWith("@npub1", true) - || word.startsWith("@note1", true) - || word.startsWith("@addr1", true) - || word.startsWith("@nprofile1", true) - || word.startsWith("@nevent1", true) + return word.startsWith("nostr:", true) || + word.startsWith("npub1", true) || + word.startsWith("naddr1", true) || + word.startsWith("note1", true) || + word.startsWith("nprofile1", true) || + word.startsWith("nevent1", true) || + word.startsWith("@npub1", true) || + word.startsWith("@note1", true) || + word.startsWith("@addr1", true) || + word.startsWith("@nprofile1", true) || + word.startsWith("@nevent1", true) } @Composable fun BechLink(word: String, navController: NavController) { - val uri = if (word.startsWith("nostr", true)) { - word - } else if (word.startsWith("@")) { - word.replaceFirst("@", "nostr:") - } else { - "nostr:${word}" - } + val uri = if (word.startsWith("nostr", true)) { + word + } else if (word.startsWith("@")) { + word.replaceFirst("@", "nostr:") + } else { + "nostr:$word" + } - val nip19Route = try { - Nip19().uriToRoute(uri) - } catch (e: Exception) { - null - } + val nip19Route = try { + Nip19().uriToRoute(uri) + } catch (e: Exception) { + null + } - if (nip19Route == null) { - Text(text = "$word ") - } else { - ClickableRoute(nip19Route, navController) - } + if (nip19Route == null) { + Text(text = "$word ") + } else { + ClickableRoute(nip19Route, navController) + } } - @Composable fun TagLink(word: String, tags: List>, canPreview: Boolean, backgroundColor: Color, accountViewModel: AccountViewModel, navController: NavController) { - val matcher = tagIndex.matcher(word) + val matcher = tagIndex.matcher(word) - val index = try { - matcher.find() - matcher.group(1).toInt() - } catch (e: Exception) { - println("Couldn't link tag ${word}") - null - } + val index = try { + matcher.find() + matcher.group(1).toInt() + } catch (e: Exception) { + println("Couldn't link tag $word") + null + } - if (index == null) { - return Text(text = "$word ") - } + if (index == null) { + return Text(text = "$word ") + } - if (index >= 0 && index < tags.size) { - if (tags[index][0] == "p") { - val baseUser = LocalCache.checkGetOrCreateUser(tags[index][1]) - if (baseUser != null) { - val userState = baseUser.live().metadata.observeAsState() - val user = userState.value?.user - if (user != null) { - ClickableUserTag(user, navController) + if (index >= 0 && index < tags.size) { + if (tags[index][0] == "p") { + val baseUser = LocalCache.checkGetOrCreateUser(tags[index][1]) + if (baseUser != null) { + val userState = baseUser.live().metadata.observeAsState() + val user = userState.value?.user + if (user != null) { + ClickableUserTag(user, navController) + } else { + Text(text = "$word ") + } + } else { + // if here the tag is not a valid Nostr Hex + Text(text = "$word ") + } + } else if (tags[index][0] == "e") { + val note = LocalCache.checkGetOrCreateNote(tags[index][1]) + if (note != null) { + if (canPreview) { + NoteCompose( + baseNote = note, + accountViewModel = accountViewModel, + modifier = Modifier + .padding(0.dp) + .fillMaxWidth() + .clip(shape = RoundedCornerShape(15.dp)) + .border( + 1.dp, + MaterialTheme.colors.onSurface.copy(alpha = 0.12f), + RoundedCornerShape(15.dp) + ), + parentBackgroundColor = MaterialTheme.colors.onSurface.copy(alpha = 0.05f) + .compositeOver(backgroundColor), + isQuotedNote = true, + navController = navController + ) + } else { + ClickableNoteTag(note, navController) + } + } else { + // if here the tag is not a valid Nostr Hex + Text(text = "$word ") + } } else { - Text(text = "$word ") + Text(text = "$word ") } - } else { - // if here the tag is not a valid Nostr Hex - Text(text = "$word ") - } - } else if (tags[index][0] == "e") { - val note = LocalCache.checkGetOrCreateNote(tags[index][1]) - if (note != null) { - if (canPreview) { - NoteCompose( - baseNote = note, - accountViewModel = accountViewModel, - modifier = Modifier - .padding(0.dp) - .fillMaxWidth() - .clip(shape = RoundedCornerShape(15.dp)) - .border( - 1.dp, - MaterialTheme.colors.onSurface.copy(alpha = 0.12f), - RoundedCornerShape(15.dp) - ), - parentBackgroundColor = MaterialTheme.colors.onSurface.copy(alpha = 0.05f) - .compositeOver(backgroundColor), - isQuotedNote = true, - navController = navController - ) - } else { - ClickableNoteTag(note, navController) - } - } else { - // if here the tag is not a valid Nostr Hex - Text(text = "$word ") - } - } else - Text(text = "$word ") - } + } } diff --git a/app/src/test/java/com/vitorpamplona/amethyst/NIP19ParserTest.kt b/app/src/test/java/com/vitorpamplona/amethyst/NIP19ParserTest.kt index 2cba993cb..0fa7ec5b1 100644 --- a/app/src/test/java/com/vitorpamplona/amethyst/NIP19ParserTest.kt +++ b/app/src/test/java/com/vitorpamplona/amethyst/NIP19ParserTest.kt @@ -6,51 +6,51 @@ import org.junit.Assert.assertEquals import org.junit.Test class NIP19ParserTest { - @Test - fun nAddrParser() { - val result = Nip19().uriToRoute("nostr:naddr1qqqqygzxpsj7dqha57pjk5k37gkn6g4nzakewtmqmnwryyhd3jfwlpgxtspsgqqqw4rs3xyxus") - assertEquals("30023:460c25e682fda7832b52d1f22d3d22b3176d972f60dcdc3212ed8c92ef85065c:", result?.hex) - } + @Test + fun nAddrParser() { + val result = Nip19().uriToRoute("nostr:naddr1qqqqygzxpsj7dqha57pjk5k37gkn6g4nzakewtmqmnwryyhd3jfwlpgxtspsgqqqw4rs3xyxus") + assertEquals("30023:460c25e682fda7832b52d1f22d3d22b3176d972f60dcdc3212ed8c92ef85065c:", result?.hex) + } - @Test - fun nAddrParser2() { - val result = Nip19().uriToRoute("nostr:naddr1qq8kwatfv3jj6amfwfjkwatpwfjqygxsm6lelvfda7qlg0tud9pfhduysy4vrexj65azqtdk4tr75j6xdspsgqqqw4rsg32ag8") - assertEquals("30023:d0debf9fb12def81f43d7c69429bb784812ac1e4d2d53a202db6aac7ea4b466c:guide-wireguard", result?.hex) - } + @Test + fun nAddrParser2() { + val result = Nip19().uriToRoute("nostr:naddr1qq8kwatfv3jj6amfwfjkwatpwfjqygxsm6lelvfda7qlg0tud9pfhduysy4vrexj65azqtdk4tr75j6xdspsgqqqw4rsg32ag8") + assertEquals("30023:d0debf9fb12def81f43d7c69429bb784812ac1e4d2d53a202db6aac7ea4b466c:guide-wireguard", result?.hex) + } - @Test - fun nAddrParse3() { - val result = Nip19().uriToRoute("naddr1qqyrswtyv5mnjv3sqy28wumn8ghj7un9d3shjtnyv9kh2uewd9hsygx3uczxts4hwue9ayfn7ggq62anzstde2qs749pm9tx2csuthhpjvpsgqqqw4rs8pmj38") - assertEquals(Nip19.Type.ADDRESS, result?.type) - assertEquals("30023:d1e60465c2b777325e9133f2100d2bb31416dca810f54a1d95665621c5dee193:89de7920", result?.hex) - assertEquals("wss://relay.damus.io", result?.relay) - } + @Test + fun nAddrParse3() { + val result = Nip19().uriToRoute("naddr1qqyrswtyv5mnjv3sqy28wumn8ghj7un9d3shjtnyv9kh2uewd9hsygx3uczxts4hwue9ayfn7ggq62anzstde2qs749pm9tx2csuthhpjvpsgqqqw4rs8pmj38") + assertEquals(Nip19.Type.ADDRESS, result?.type) + assertEquals("30023:d1e60465c2b777325e9133f2100d2bb31416dca810f54a1d95665621c5dee193:89de7920", result?.hex) + assertEquals("wss://relay.damus.io", result?.relay) + } - @Test - fun nAddrATagParse3() { - val address = ATag.parse("30023:d1e60465c2b777325e9133f2100d2bb31416dca810f54a1d95665621c5dee193:89de7920", "wss://relay.damus.io") - assertEquals(30023, address?.kind) - assertEquals("d1e60465c2b777325e9133f2100d2bb31416dca810f54a1d95665621c5dee193", address?.pubKeyHex) - assertEquals("89de7920", address?.dTag) - assertEquals("wss://relay.damus.io" , address?.relay) - assertEquals("naddr1qqyrswtyv5mnjv3sqy28wumn8ghj7un9d3shjtnyv9kh2uewd9hsygx3uczxts4hwue9ayfn7ggq62anzstde2qs749pm9tx2csuthhpjvpsgqqqw4rs8pmj38", address?.toNAddr()) - } + @Test + fun nAddrATagParse3() { + val address = ATag.parse("30023:d1e60465c2b777325e9133f2100d2bb31416dca810f54a1d95665621c5dee193:89de7920", "wss://relay.damus.io") + assertEquals(30023, address?.kind) + assertEquals("d1e60465c2b777325e9133f2100d2bb31416dca810f54a1d95665621c5dee193", address?.pubKeyHex) + assertEquals("89de7920", address?.dTag) + assertEquals("wss://relay.damus.io", address?.relay) + assertEquals("naddr1qqyrswtyv5mnjv3sqy28wumn8ghj7un9d3shjtnyv9kh2uewd9hsygx3uczxts4hwue9ayfn7ggq62anzstde2qs749pm9tx2csuthhpjvpsgqqqw4rs8pmj38", address?.toNAddr()) + } - @Test - fun nAddrFormatter() { - val address = ATag(30023, "460c25e682fda7832b52d1f22d3d22b3176d972f60dcdc3212ed8c92ef85065c", "", null) - assertEquals("naddr1qqqqygzxpsj7dqha57pjk5k37gkn6g4nzakewtmqmnwryyhd3jfwlpgxtspsgqqqw4rs3xyxus", address.toNAddr()) - } + @Test + fun nAddrFormatter() { + val address = ATag(30023, "460c25e682fda7832b52d1f22d3d22b3176d972f60dcdc3212ed8c92ef85065c", "", null) + assertEquals("naddr1qqqqygzxpsj7dqha57pjk5k37gkn6g4nzakewtmqmnwryyhd3jfwlpgxtspsgqqqw4rs3xyxus", address.toNAddr()) + } - @Test - fun nAddrFormatter2() { - val address = ATag(30023, "d0debf9fb12def81f43d7c69429bb784812ac1e4d2d53a202db6aac7ea4b466c", "guide-wireguard", null) - assertEquals("naddr1qq8kwatfv3jj6amfwfjkwatpwfjqygxsm6lelvfda7qlg0tud9pfhduysy4vrexj65azqtdk4tr75j6xdspsgqqqw4rsg32ag8", address.toNAddr()) - } + @Test + fun nAddrFormatter2() { + val address = ATag(30023, "d0debf9fb12def81f43d7c69429bb784812ac1e4d2d53a202db6aac7ea4b466c", "guide-wireguard", null) + assertEquals("naddr1qq8kwatfv3jj6amfwfjkwatpwfjqygxsm6lelvfda7qlg0tud9pfhduysy4vrexj65azqtdk4tr75j6xdspsgqqqw4rsg32ag8", address.toNAddr()) + } - @Test - fun nAddrFormatter3() { - val address = ATag(30023, "d1e60465c2b777325e9133f2100d2bb31416dca810f54a1d95665621c5dee193", "89de7920", "wss://relay.damus.io") - assertEquals("naddr1qqyrswtyv5mnjv3sqy28wumn8ghj7un9d3shjtnyv9kh2uewd9hsygx3uczxts4hwue9ayfn7ggq62anzstde2qs749pm9tx2csuthhpjvpsgqqqw4rs8pmj38", address.toNAddr()) - } -} \ No newline at end of file + @Test + fun nAddrFormatter3() { + val address = ATag(30023, "d1e60465c2b777325e9133f2100d2bb31416dca810f54a1d95665621c5dee193", "89de7920", "wss://relay.damus.io") + assertEquals("naddr1qqyrswtyv5mnjv3sqy28wumn8ghj7un9d3shjtnyv9kh2uewd9hsygx3uczxts4hwue9ayfn7ggq62anzstde2qs749pm9tx2csuthhpjvpsgqqqw4rs8pmj38", address.toNAddr()) + } +}