removed online features

This commit is contained in:
greenart7c3
2023-07-31 08:11:31 -03:00
parent 26f483ecad
commit c723ef67b2
28 changed files with 95 additions and 1152 deletions
-3
View File
@@ -105,9 +105,6 @@ dependencies {
// Json Serialization
implementation 'com.squareup.retrofit2:converter-gson:2.9.0'
// Websockets API
implementation 'com.squareup.okhttp3:okhttp:5.0.0-alpha.11'
// Markdown (With fix for full-image bleeds)
implementation('com.github.vitorpamplona.compose-richtext:richtext-ui:a0954aba63')
implementation('com.github.vitorpamplona.compose-richtext:richtext-ui-material:a0954aba63')
+1 -2
View File
@@ -2,11 +2,10 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools">
<uses-permission android:name="android.permission.INTERNET"/>
<application
android:name=".nostrsigner"
android:allowBackup="false"
android:dataExtractionRules="@xml/data_extraction_rules"
android:fullBackupContent="@xml/backup_rules"
android:icon="@mipmap/ic_launcher"
@@ -20,8 +20,7 @@ class MainActivity : AppCompatActivity() {
setContent {
var event: IntentData? = null
if (intent.data != null) {
val list = intent?.extras?.getString("relays")?.split(",") ?: listOf()
event = IntentData(intent?.data?.toString() ?: "", list)
event = IntentData(intent?.data?.toString() ?: "")
}
NostrSignerTheme {
@@ -1,6 +0,0 @@
package com.greenart7c3.nostrsigner
fun String.toShortenHex(): String {
if (length <= 16) return this
return replaceRange(8, length - 8, ":")
}
@@ -1,9 +1,5 @@
package com.greenart7c3.nostrsigner.models
import com.greenart7c3.nostrsigner.service.KeyPair
import com.greenart7c3.nostrsigner.service.bechToBytes
import com.greenart7c3.nostrsigner.service.nip19.Nip19
import com.greenart7c3.nostrsigner.toShortenHex
import fr.acinq.secp256k1.Hex
/** Makes the distinction between String and Hex **/
@@ -16,37 +12,3 @@ fun ByteArray.toHexKey(): HexKey {
fun HexKey.hexToByteArray(): ByteArray {
return Hex.decode(this)
}
fun HexKey.toDisplayHexKey(): String {
return this.toShortenHex()
}
fun decodePublicKey(key: String): ByteArray {
val parsed = Nip19.uriToRoute(key)
val pubKeyParsed = parsed?.hex?.hexToByteArray()
return if (key.startsWith("nsec")) {
KeyPair(privKey = key.bechToBytes()).pubKey
} else if (pubKeyParsed != null) {
pubKeyParsed
} else {
Hex.decode(key)
}
}
fun decodePublicKeyAsHexOrNull(key: String): HexKey? {
return try {
val parsed = Nip19.uriToRoute(key)
val pubKeyParsed = parsed?.hex
if (key.startsWith("nsec")) {
KeyPair(privKey = key.bechToBytes()).pubKey.toHexKey()
} else if (pubKeyParsed != null) {
pubKeyParsed
} else {
Hex.decode(key).toHexKey()
}
} catch (e: Exception) {
null
}
}
@@ -1,6 +1,5 @@
package com.greenart7c3.nostrsigner.models
class IntentData(
val data: String,
val relays: List<String>
val data: String
)
@@ -1,13 +1,5 @@
package com.greenart7c3.nostrsigner.models
object TimeUtils {
const val fiveMinutes = 60 * 5
const val oneHour = 60 * 60
const val oneDay = 24 * 60 * 60
fun now() = System.currentTimeMillis() / 1000
fun fiveMinutesAgo() = now() - fiveMinutes
fun oneHourAgo() = now() - oneHour
fun oneDayAgo() = now() - oneDay
fun eightHoursAgo() = now() - (oneHour * 8)
}
@@ -201,10 +201,7 @@ object Bech32 {
fun ByteArray.toNsec() = Bech32.encodeBytes(hrp = "nsec", this, Bech32.Encoding.Bech32)
fun ByteArray.toNpub() = Bech32.encodeBytes(hrp = "npub", this, Bech32.Encoding.Bech32)
fun ByteArray.toNote() = Bech32.encodeBytes(hrp = "note", this, Bech32.Encoding.Bech32)
fun ByteArray.toNEvent() = Bech32.encodeBytes(hrp = "nevent", this, Bech32.Encoding.Bech32)
fun ByteArray.toNAddress() = Bech32.encodeBytes(hrp = "naddr", this, Bech32.Encoding.Bech32)
fun ByteArray.toLnUrl() = Bech32.encodeBytes(hrp = "lnurl", this, Bech32.Encoding.Bech32)
fun String.bechToBytes(hrp: String? = null): ByteArray {
val decodedForm = Bech32.decodeBytes(this)
@@ -2,37 +2,15 @@ package com.greenart7c3.nostrsigner.service.model
import android.util.Log
import androidx.compose.runtime.Immutable
import com.greenart7c3.nostrsigner.models.hexToByteArray
import com.greenart7c3.nostrsigner.models.toHexKey
import com.greenart7c3.nostrsigner.service.bechToBytes
import com.greenart7c3.nostrsigner.service.nip19.Tlv
import com.greenart7c3.nostrsigner.service.toByteArray
import com.greenart7c3.nostrsigner.service.toNAddress
import fr.acinq.secp256k1.Hex
@Immutable
data class ATag(val kind: Int, val pubKeyHex: String, val dTag: String, val relay: String?) {
fun toTag() = "$kind:$pubKeyHex:$dTag"
fun toNAddr(): String {
val kind = kind.toByteArray()
val author = pubKeyHex.hexToByteArray()
val dTag = dTag.toByteArray(Charsets.UTF_8)
val relay = relay?.toByteArray(Charsets.UTF_8)
var fullArray = byteArrayOf(Tlv.Type.SPECIAL.id, dTag.size.toByte()) + dTag
if (relay != null) {
fullArray = fullArray + byteArrayOf(Tlv.Type.RELAY.id, relay.size.toByte()) + relay
}
fullArray = fullArray +
byteArrayOf(Tlv.Type.AUTHOR.id, author.size.toByte()) + author +
byteArrayOf(Tlv.Type.KIND.id, kind.size.toByte()) + kind
return fullArray.toNAddress()
}
companion object {
fun isATag(key: String): Boolean {
return key.startsWith("naddr1") || key.contains(":")
@@ -1,56 +0,0 @@
package com.greenart7c3.nostrsigner.service.model
import androidx.compose.runtime.Immutable
import com.greenart7c3.nostrsigner.models.HexKey
import com.greenart7c3.nostrsigner.models.TimeUtils
import com.greenart7c3.nostrsigner.models.toHexKey
import com.greenart7c3.nostrsigner.service.CryptoUtils
@Immutable
class EmojiPackEvent(
id: HexKey,
pubKey: HexKey,
createdAt: Long,
tags: List<List<String>>,
content: String,
sig: HexKey
) : GeneralListEvent(id, pubKey, createdAt, kind, tags, content, sig) {
companion object {
const val kind = 30030
fun create(
name: String = "",
privateKey: ByteArray,
createdAt: Long = TimeUtils.now()
): EmojiPackEvent {
val content = ""
val pubKey = CryptoUtils.pubkeyCreate(privateKey)
val tags = mutableListOf<List<String>>()
tags.add(listOf("d", name))
val id = generateId(pubKey.toHexKey(), createdAt, kind, tags, content)
val sig = CryptoUtils.sign(id, privateKey)
return EmojiPackEvent(id.toHexKey(), pubKey.toHexKey(), createdAt, tags, content, sig.toHexKey())
}
}
}
@Immutable
data class EmojiUrl(val code: String, val url: String) {
fun encode(): String {
return ":$code:$url"
}
companion object {
fun decode(encodedEmojiSetup: String): EmojiUrl? {
val emojiParts = encodedEmojiSetup.split(":", limit = 3)
return if (emojiParts.size > 2) {
EmojiUrl(emojiParts[1], emojiParts[2])
} else {
null
}
}
}
}
@@ -0,0 +1,6 @@
package com.greenart7c3.nostrsigner.service.model
import androidx.compose.runtime.Immutable
@Immutable
data class EmojiUrl(val code: String, val url: String)
@@ -17,7 +17,6 @@ import com.greenart7c3.nostrsigner.models.HexKey
import com.greenart7c3.nostrsigner.models.TimeUtils
import com.greenart7c3.nostrsigner.models.toHexKey
import com.greenart7c3.nostrsigner.service.CryptoUtils
import com.greenart7c3.nostrsigner.service.nip19.Nip19
import fr.acinq.secp256k1.Hex
import java.lang.reflect.Type
import java.math.BigDecimal
@@ -147,18 +146,6 @@ open class Event(
}
}
open fun toNIP19(): String {
return if (this is AddressableEvent) {
ATag(kind, pubKey, dTag(), null).toNAddr()
} else {
Nip19.createNEvent(id, pubKey, kind, null)
}
}
fun toNostrUri(): String {
return "nostr:${toNIP19()}"
}
/**
* Checks if the ID is correct and then if the pubKey's secret key signed the event.
*/
@@ -279,11 +266,9 @@ open class Event(
.registerTypeAdapter(Request::class.java, RequestDeserializer())
.create()
fun fromJson(json: String, lenient: Boolean = false): Event = gson.fromJson(json, Event::class.java).getRefinedEvent(lenient)
fun fromJson(json: String): Event = gson.fromJson(json, Event::class.java).getRefinedEvent()
fun fromJson(json: JsonElement, lenient: Boolean = false): Event = gson.fromJson(json, Event::class.java).getRefinedEvent(lenient)
fun Event.getRefinedEvent(lenient: Boolean = false): Event = when (kind) {
fun Event.getRefinedEvent(): Event = when (kind) {
else -> this
}
@@ -1,36 +0,0 @@
package com.greenart7c3.nostrsigner.service.model
import androidx.compose.runtime.Immutable
import com.greenart7c3.nostrsigner.models.HexKey
import com.greenart7c3.nostrsigner.models.TimeUtils
import com.greenart7c3.nostrsigner.models.toHexKey
import com.greenart7c3.nostrsigner.service.CryptoUtils
@Immutable
class RelayAuthEvent(
id: HexKey,
pubKey: HexKey,
createdAt: Long,
tags: List<List<String>>,
content: String,
sig: HexKey
) : Event(id, pubKey, createdAt, kind, tags, content, sig) {
fun relay() = tags.firstOrNull() { it.size > 1 && it[0] == "relay" }?.get(1)
fun challenge() = tags.firstOrNull() { it.size > 1 && it[0] == "challenge" }?.get(1)
companion object {
const val kind = 22242
fun create(relay: String, challenge: String, privateKey: ByteArray, createdAt: Long = TimeUtils.now()): RelayAuthEvent {
val content = ""
val pubKey = CryptoUtils.pubkeyCreate(privateKey).toHexKey()
val tags = listOf(
listOf("relay", relay),
listOf("challenge", challenge)
)
val id = generateId(pubKey, createdAt, kind, tags, content)
val sig = CryptoUtils.sign(id, privateKey)
return RelayAuthEvent(id.toHexKey(), pubKey, createdAt, tags, content, sig.toHexKey())
}
}
}
@@ -1,172 +0,0 @@
package com.greenart7c3.nostrsigner.service.nip19
import android.util.Log
import androidx.compose.runtime.Immutable
import com.greenart7c3.nostrsigner.models.hexToByteArray
import com.greenart7c3.nostrsigner.models.toHexKey
import com.greenart7c3.nostrsigner.service.bechToBytes
import com.greenart7c3.nostrsigner.service.toByteArray
import com.greenart7c3.nostrsigner.service.toNEvent
import java.util.regex.Pattern
object Nip19 {
enum class Type {
USER, NOTE, EVENT, RELAY, ADDRESS
}
val nip19regex = Pattern.compile("(nostr:)?@?(nsec1|npub1|nevent1|naddr1|note1|nprofile1|nrelay1)([qpzry9x8gf2tvdw0s3jn54khce6mua7l]+)([\\S]*)", Pattern.CASE_INSENSITIVE)
@Immutable
data class Return(
val type: Type,
val hex: String,
val relay: String? = null,
val author: String? = null,
val kind: Int? = null,
val additionalChars: String = ""
)
fun uriToRoute(uri: String?): Return? {
if (uri == null) return null
try {
val matcher = nip19regex.matcher(uri)
if (!matcher.find()) {
return null
}
val uriScheme = matcher.group(1) // nostr:
val type = matcher.group(2) // npub1
val key = matcher.group(3) // bech32
val additionalChars = matcher.group(4) // additional chars
return parseComponents(uriScheme, type, key, additionalChars)
} catch (e: Throwable) {
Log.e("NIP19 Parser", "Issue trying to Decode NIP19 $uri: ${e.message}", e)
}
return null
}
fun parseComponents(
uriScheme: String?,
type: String,
key: String?,
additionalChars: String?
): Return? {
return try {
val bytes = (type + key).bechToBytes()
val parsed = when (type.lowercase()) {
"npub1" -> npub(bytes)
"note1" -> note(bytes)
"nprofile1" -> nprofile(bytes)
"nevent1" -> nevent(bytes)
"nrelay1" -> nrelay(bytes)
"naddr1" -> naddr(bytes)
else -> null
}
parsed?.copy(additionalChars = additionalChars ?: "")
} catch (e: Throwable) {
Log.w("NIP19 Parser", "Issue trying to Decode NIP19 $key: ${e.message}", e)
null
}
}
private fun npub(bytes: ByteArray): Return {
return Return(Type.USER, bytes.toHexKey())
}
private fun note(bytes: ByteArray): Return {
return Return(Type.NOTE, bytes.toHexKey())
}
private fun nprofile(bytes: ByteArray): Return? {
val tlv = Tlv.parse(bytes)
val hex = tlv.get(Tlv.Type.SPECIAL.id)
?.get(0)
?.toHexKey() ?: return null
val relay = tlv.get(Tlv.Type.RELAY.id)
?.get(0)
?.toString(Charsets.UTF_8)
return Return(Type.USER, hex, relay)
}
private fun nevent(bytes: ByteArray): Return? {
val tlv = Tlv.parse(bytes)
val hex = tlv.get(Tlv.Type.SPECIAL.id)
?.get(0)
?.toHexKey() ?: return null
val relay = tlv.get(Tlv.Type.RELAY.id)
?.get(0)
?.toString(Charsets.UTF_8)
val author = tlv.get(Tlv.Type.AUTHOR.id)
?.get(0)
?.toHexKey()
val kind = tlv.get(Tlv.Type.KIND.id)
?.get(0)
?.let { Tlv.toInt32(it) }
return Return(Type.EVENT, hex, relay, author, kind)
}
private fun nrelay(bytes: ByteArray): Return? {
val relayUrl = Tlv.parse(bytes)
.get(Tlv.Type.SPECIAL.id)
?.get(0)
?.toString(Charsets.UTF_8) ?: return null
return Return(Type.RELAY, relayUrl)
}
private fun naddr(bytes: ByteArray): Return? {
val tlv = Tlv.parse(bytes)
val d = tlv.get(Tlv.Type.SPECIAL.id)
?.get(0)
?.toString(Charsets.UTF_8) ?: return null
val relay = tlv.get(Tlv.Type.RELAY.id)
?.get(0)
?.toString(Charsets.UTF_8)
val author = tlv.get(Tlv.Type.AUTHOR.id)
?.get(0)
?.toHexKey()
val kind = tlv.get(Tlv.Type.KIND.id)
?.get(0)
?.let { Tlv.toInt32(it) }
return Return(Type.ADDRESS, "$kind:$author:$d", relay, author, kind)
}
public fun createNEvent(idHex: String, author: String?, kind: Int?, relay: String?): String {
val kind = kind?.toByteArray()
val author = author?.hexToByteArray()
val idHex = idHex.hexToByteArray()
val relay = relay?.toByteArray(Charsets.UTF_8)
var fullArray = byteArrayOf(Tlv.Type.SPECIAL.id, idHex.size.toByte()) + idHex
if (relay != null) {
fullArray = fullArray + byteArrayOf(Tlv.Type.RELAY.id, relay.size.toByte()) + relay
}
if (author != null) {
fullArray = fullArray + byteArrayOf(Tlv.Type.AUTHOR.id, author.size.toByte()) + author
}
if (kind != null) {
fullArray = fullArray + byteArrayOf(Tlv.Type.KIND.id, kind.size.toByte()) + kind
}
return fullArray.toNEvent()
}
}
@@ -1,7 +0,0 @@
package com.greenart7c3.nostrsigner.service.relays
class EOSETime(var time: Long) {
override fun toString(): String {
return time.toString()
}
}
@@ -1,65 +0,0 @@
package com.greenart7c3.nostrsigner.service.relays
import com.google.gson.Gson
import com.google.gson.GsonBuilder
import com.google.gson.JsonArray
import com.google.gson.JsonObject
class JsonFilter(
val ids: List<String>? = null,
val authors: List<String>? = null,
val kinds: List<Int>? = null,
val tags: Map<String, List<String>>? = null,
val since: Map<String, EOSETime>? = null,
val until: Long? = null,
val limit: Int? = null,
val search: String? = null
) {
fun toJson(forRelay: String? = null): String {
val jsonObject = JsonObject()
ids?.run {
jsonObject.add("ids", JsonArray().apply { ids.forEach { add(it) } })
}
authors?.run {
jsonObject.add("authors", JsonArray().apply { authors.forEach { add(it) } })
}
kinds?.run {
jsonObject.add("kinds", JsonArray().apply { kinds.forEach { add(it) } })
}
tags?.run {
entries.forEach { kv ->
jsonObject.add("#${kv.key}", JsonArray().apply { kv.value.forEach { add(it) } })
}
}
since?.run {
if (!isEmpty()) {
if (forRelay != null) {
val relaySince = get(forRelay)
if (relaySince != null) {
jsonObject.addProperty("since", relaySince.time)
}
} else {
val jsonObjectSince = JsonObject()
entries.forEach { sincePairs ->
jsonObjectSince.addProperty(sincePairs.key, "${sincePairs.value}")
}
jsonObject.add("since", jsonObjectSince)
}
}
}
until?.run {
jsonObject.addProperty("until", until)
}
limit?.run {
jsonObject.addProperty("limit", limit)
}
search?.run {
jsonObject.addProperty("search", search)
}
return gson.toJson(jsonObject)
}
companion object {
val gson: Gson = GsonBuilder().create()
}
}
@@ -1,283 +0,0 @@
package com.greenart7c3.nostrsigner.service.relays
import android.util.Log
import com.google.gson.JsonElement
import com.greenart7c3.nostrsigner.BuildConfig
import com.greenart7c3.nostrsigner.models.TimeUtils
import com.greenart7c3.nostrsigner.service.model.Event
import com.greenart7c3.nostrsigner.service.model.EventInterface
import com.greenart7c3.nostrsigner.service.model.RelayAuthEvent
import okhttp3.OkHttpClient
import okhttp3.Request
import okhttp3.Response
import okhttp3.WebSocket
import okhttp3.WebSocketListener
import java.util.concurrent.atomic.AtomicBoolean
enum class FeedType {
FOLLOWS, PUBLIC_CHATS, PRIVATE_DMS, GLOBAL, SEARCH, WALLET_CONNECT
}
val COMMON_FEED_TYPES = setOf(
FeedType.FOLLOWS,
FeedType.PUBLIC_CHATS,
FeedType.PRIVATE_DMS,
FeedType.GLOBAL
)
class Relay(
var url: String,
var read: Boolean = true,
var write: Boolean = true,
var activeTypes: Set<FeedType> = FeedType.values().toSet()
) {
companion object {
// waits 3 minutes to reconnect once things fail
const val RECONNECTING_IN_SECONDS = 60 * 3
}
private val httpClient = OkHttpClient.Builder()
.followRedirects(true)
.followSslRedirects(true)
.build()
private var listeners = setOf<Listener>()
private var socket: WebSocket? = null
private var isReady: Boolean = false
var eventDownloadCounterInBytes = 0
var eventUploadCounterInBytes = 0
var spamCounter = 0
var errorCounter = 0
var pingInMs: Long? = null
var closingTimeInSeconds = 0L
var afterEOSE = false
fun register(listener: Listener) {
listeners = listeners.plus(listener)
}
fun unregister(listener: Listener) {
listeners = listeners.minus(listener)
}
fun isConnected(): Boolean {
return socket != null
}
private var connectingBlock = AtomicBoolean()
fun connectAndRun(onConnected: (Relay) -> Unit) {
// If there is a connection, don't wait.
if (connectingBlock.getAndSet(true)) {
return
}
if (socket != null) return
try {
val request = Request.Builder()
.header("User-Agent", "Amber/${BuildConfig.VERSION_NAME}")
.url(url.trim())
.build()
socket = httpClient.newWebSocket(request, RelayListener(onConnected))
} catch (e: Exception) {
errorCounter++
markConnectionAsClosed()
Log.e("Relay", "Relay Invalid $url")
e.printStackTrace()
} finally {
connectingBlock.set(false)
}
}
inner class RelayListener(val onConnected: (Relay) -> Unit) : WebSocketListener() {
override fun onOpen(webSocket: WebSocket, response: Response) {
markConnectionAsReady(response.receivedResponseAtMillis - response.sentRequestAtMillis)
// Log.w("Relay", "Relay OnOpen, Loading All subscriptions $url")
onConnected(this@Relay)
listeners.forEach { it.onRelayStateChange(this@Relay, Type.CONNECT, null) }
}
override fun onMessage(webSocket: WebSocket, text: String) {
eventDownloadCounterInBytes += text.bytesUsedInMemory()
try {
processNewRelayMessage(text)
} catch (t: Throwable) {
t.printStackTrace()
text.chunked(2000) { chunked ->
listeners.forEach { it.onError(this@Relay, "", Error("Problem with $chunked")) }
}
}
}
override fun onClosing(webSocket: WebSocket, code: Int, reason: String) {
listeners.forEach {
it.onRelayStateChange(
this@Relay,
Type.DISCONNECTING,
null
)
}
}
override fun onClosed(webSocket: WebSocket, code: Int, reason: String) {
markConnectionAsClosed()
listeners.forEach { it.onRelayStateChange(this@Relay, Type.DISCONNECT, null) }
}
override fun onFailure(webSocket: WebSocket, t: Throwable, response: Response?) {
errorCounter++
socket?.close(1000, "Normal close")
// Failures disconnect the relay.
markConnectionAsClosed()
Log.w("Relay", "Relay onFailure $url, ${response?.message} $response")
t.printStackTrace()
listeners.forEach {
it.onError(this@Relay, "", Error("WebSocket Failure. Response: $response. Exception: ${t.message}", t))
}
}
}
fun markConnectionAsReady(pingInMs: Long) {
this.afterEOSE = false
this.isReady = true
this.pingInMs = pingInMs
}
fun markConnectionAsClosed() {
this.socket = null
this.isReady = false
this.afterEOSE = false
this.closingTimeInSeconds = TimeUtils.now()
}
fun processNewRelayMessage(newMessage: String) {
val msgArray = Event.gson.fromJson(newMessage, JsonElement::class.java).asJsonArray
val type = msgArray[0].asString
val channel = msgArray[1].asString
when (type) {
"EVENT" -> {
val event = Event.fromJson(msgArray[2], false)
// Log.w("Relay", "Relay onEVENT $url, $channel")
listeners.forEach {
it.onEvent(this@Relay, channel, event)
if (afterEOSE) {
it.onRelayStateChange(this@Relay, Type.EOSE, channel)
}
}
}
"EOSE" -> listeners.forEach {
afterEOSE = true
// Log.w("Relay", "Relay onEOSE $url, $channel")
it.onRelayStateChange(this@Relay, Type.EOSE, channel)
}
"NOTICE" -> listeners.forEach {
Log.w("Relay", "Relay onNotice $url, $channel")
it.onError(this@Relay, channel, Error("Relay sent notice: " + channel))
}
"OK" -> listeners.forEach {
Log.w("Relay", "Relay on OK $url, ${msgArray[1].asString}, ${msgArray[2].asBoolean}, ${msgArray[3].asString}")
it.onSendResponse(this@Relay, msgArray[1].asString, msgArray[2].asBoolean, msgArray[3].asString)
}
"AUTH" -> listeners.forEach {
// Log.w("Relay", "Relay$url, ${msg[1].asString}")
it.onAuth(this@Relay, msgArray[1].asString)
}
else -> listeners.forEach {
// Log.w("Relay", "Relay something else $url, $channel")
it.onError(
this@Relay,
channel,
Error("Unknown type $type on channel $channel. Msg was $newMessage")
)
}
}
}
fun disconnect() {
// httpClient.dispatcher.executorService.shutdown()
closingTimeInSeconds = TimeUtils.now()
socket?.close(1000, "Normal close")
socket = null
isReady = false
afterEOSE = false
}
fun send(signedEvent: EventInterface) {
if (signedEvent is RelayAuthEvent) {
val event = """["AUTH",${signedEvent.toJson()}]"""
socket?.send(event)
eventUploadCounterInBytes += event.bytesUsedInMemory()
}
if (write) {
if (signedEvent !is RelayAuthEvent) {
val event = """["EVENT",${signedEvent.toJson()}]"""
socket?.send(event)
eventUploadCounterInBytes += event.bytesUsedInMemory()
}
}
}
fun close(subscriptionId: String) {
socket?.send("""["CLOSE","$subscriptionId"]""")
}
fun isSameRelayConfig(other: Relay): Boolean {
return url == other.url &&
write == other.write &&
read == other.read &&
activeTypes == other.activeTypes
}
enum class Type {
// Websocket connected
CONNECT,
// Websocket disconnecting
DISCONNECTING,
// Websocket disconnected
DISCONNECT,
// End Of Stored Events
EOSE
}
interface Listener {
/**
* A new message was received
*/
fun onEvent(relay: Relay, subscriptionId: String, event: Event)
fun onError(relay: Relay, subscriptionId: String, error: Error)
fun onSendResponse(relay: Relay, eventId: String, success: Boolean, message: String)
fun onAuth(relay: Relay, challenge: String)
/**
* Connected to or disconnected from a relay
*
* @param type is 0 for disconnect and 1 for connect
*/
fun onRelayStateChange(relay: Relay, type: Type, channel: String?)
}
}
fun String.bytesUsedInMemory(): Int {
return (8 * ((((this.length) * 2) + 45) / 8))
}
@@ -1,63 +0,0 @@
package com.greenart7c3.nostrsigner.service.relays
import com.google.gson.GsonBuilder
import com.google.gson.JsonArray
import com.google.gson.JsonObject
class TypedFilter(
val types: Set<FeedType>,
val filter: JsonFilter
) {
fun toJson(): String {
return GsonBuilder().create().toJson(toJsonObject())
}
fun toJsonObject(): JsonObject {
val jsonObject = JsonObject()
jsonObject.add("types", typesToJson(types))
jsonObject.add("filter", filterToJson(filter))
return jsonObject
}
fun typesToJson(types: Set<FeedType>): JsonArray {
return JsonArray().apply { types.forEach { add(it.name.lowercase()) } }
}
fun filterToJson(filter: JsonFilter): JsonObject {
val jsonObject = JsonObject()
filter.ids?.run {
jsonObject.add("ids", JsonArray().apply { filter.ids.forEach { add(it) } })
}
filter.authors?.run {
jsonObject.add("authors", JsonArray().apply { filter.authors.forEach { add(it) } })
}
filter.kinds?.run {
jsonObject.add("kinds", JsonArray().apply { filter.kinds.forEach { add(it) } })
}
filter.tags?.run {
entries.forEach { kv ->
jsonObject.add("#${kv.key}", JsonArray().apply { kv.value.forEach { add(it) } })
}
}
/*
Does not include since in the json comparison
filter.since?.run {
val jsonObjectSince = JsonObject()
entries.forEach { sincePairs ->
jsonObjectSince.addProperty(sincePairs.key, "${sincePairs.value}")
}
jsonObject.add("since", jsonObjectSince)
}*/
filter.until?.run {
jsonObject.addProperty("until", filter.until)
}
filter.limit?.run {
jsonObject.addProperty("limit", filter.limit)
}
filter.search?.run {
jsonObject.addProperty("search", filter.search)
}
return jsonObject
}
}
@@ -31,5 +31,3 @@ fun AccountScreen(
}
}
}
@@ -16,11 +16,9 @@ import androidx.compose.material.Text
import androidx.compose.material.rememberDrawerState
import androidx.compose.material.rememberScaffoldState
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalClipboardManager
@@ -32,11 +30,9 @@ import com.greenart7c3.nostrsigner.models.Account
import com.greenart7c3.nostrsigner.models.IntentData
import com.greenart7c3.nostrsigner.models.toHexKey
import com.greenart7c3.nostrsigner.service.model.Event
import com.greenart7c3.nostrsigner.service.relays.Relay
import com.greenart7c3.nostrsigner.ui.actions.ButtonBorder
import com.greenart7c3.nostrsigner.ui.actions.RelaySelectionDialog
import com.greenart7c3.nostrsigner.ui.components.Drawer
import com.greenart7c3.nostrsigner.ui.components.MainAppBar
import com.greenart7c3.nostrsigner.ui.theme.ButtonBorder
import kotlinx.coroutines.launch
import org.json.JSONObject
@@ -47,11 +43,6 @@ fun MainScreen(account: Account, accountStateViewModel: AccountStateViewModel, j
mutableStateOf<Event?>(null)
}
val relays = listOf<Relay>().toMutableList()
var showRelaysDialog by remember {
mutableStateOf(false)
}
val relaysToPost = remember { mutableListOf<Relay>() }
val clipboardManager = LocalClipboardManager.current
val scaffoldState = rememberScaffoldState(rememberDrawerState(DrawerValue.Closed))
val coroutineScope = rememberCoroutineScope()
@@ -86,11 +77,7 @@ fun MainScreen(account: Account, accountStateViewModel: AccountStateViewModel, j
json.let {
val data = it.data.replace("nostrsigner:", "")
event.value = Event.fromJson(data)
val tempRelays = it.relays
relays.clear()
tempRelays.forEach { url ->
relays.add(Relay(url))
}
Column(
Modifier.fillMaxSize()
) {
@@ -103,17 +90,8 @@ fun MainScreen(account: Account, accountStateViewModel: AccountStateViewModel, j
Modifier
.fillMaxWidth()
.padding(10.dp),
Arrangement.SpaceBetween
Arrangement.Center
) {
Button(
shape = ButtonBorder,
onClick = {
showRelaysDialog = true
}
) {
Text("Relays")
}
Button(
shape = ButtonBorder,
onClick = {
@@ -145,65 +123,9 @@ fun MainScreen(account: Account, accountStateViewModel: AccountStateViewModel, j
) {
Text("Sign")
}
Button(
shape = ButtonBorder,
onClick = {
if (event.value == null) {
return@Button
}
if (event.value!!.pubKey != account.keyPair.pubKey.toHexKey()) {
coroutineScope.launch {
Toast.makeText(
context,
context.getString(R.string.event_pubkey_is_not_equal_to_current_logged_in_user),
Toast.LENGTH_SHORT
).show()
}
return@Button
}
val signedEvent = Event.create(
account.keyPair.privKey!!,
event.value!!.kind,
event.value!!.tags,
event.value!!.content,
event.value!!.createdAt
)
if (relaysToPost.isEmpty()) {
relaysToPost.addAll(relays)
}
relaysToPost.forEach { item ->
item.connectAndRun { relay ->
relay.send(signedEvent)
}
}
// mainActivity.finish()
}
) {
Text("Post")
}
}
}
}
if (showRelaysDialog) {
RelaySelectionDialog(
list = relays,
selectRelays = relaysToPost,
onClose = {
showRelaysDialog = false
},
onPost = {
relaysToPost.clear()
relaysToPost.addAll(it)
}
)
}
}
}
}
@@ -17,7 +17,7 @@ import androidx.compose.ui.window.Dialog
import androidx.compose.ui.window.DialogProperties
import com.greenart7c3.nostrsigner.models.Account
import com.greenart7c3.nostrsigner.service.toNpub
import com.greenart7c3.nostrsigner.ui.actions.CloseButton
import com.greenart7c3.nostrsigner.ui.components.CloseButton
import com.greenart7c3.nostrsigner.ui.theme.Size35dp
@Composable
@@ -41,6 +41,8 @@ import com.greenart7c3.nostrsigner.models.Account
import com.greenart7c3.nostrsigner.service.Biometrics
import com.greenart7c3.nostrsigner.service.toNpub
import com.greenart7c3.nostrsigner.service.toNsec
import com.greenart7c3.nostrsigner.ui.components.CloseButton
import com.greenart7c3.nostrsigner.ui.theme.ButtonBorder
import com.halilibo.richtext.markdown.Markdown
import com.halilibo.richtext.ui.RichTextStyle
import com.halilibo.richtext.ui.material.MaterialRichText
@@ -1,234 +0,0 @@
package com.greenart7c3.nostrsigner.ui.actions
import android.widget.Toast
import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.combinedClickable
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxHeight
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.itemsIndexed
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.Button
import androidx.compose.material.ButtonDefaults
import androidx.compose.material.Icon
import androidx.compose.material.MaterialTheme
import androidx.compose.material.Surface
import androidx.compose.material.Switch
import androidx.compose.material.Text
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Cancel
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.unit.dp
import androidx.compose.ui.window.Dialog
import androidx.compose.ui.window.DialogProperties
import com.greenart7c3.nostrsigner.service.relays.Relay
import kotlinx.coroutines.launch
data class RelayList(
val relay: Relay,
val isSelected: Boolean
)
val ButtonBorder = RoundedCornerShape(20.dp)
val Size20Modifier = Modifier.size(20.dp)
@Composable
fun CloseButton(onCancel: () -> Unit) {
Button(
onClick = {
onCancel()
},
shape = ButtonBorder,
colors = ButtonDefaults
.buttonColors(
backgroundColor = Color.Gray
)
) {
CloseIcon()
}
}
@Composable
fun PostButton(modifier: Modifier = Modifier, onPost: () -> Unit = {}, isActive: Boolean) {
Button(
modifier = modifier,
onClick = {
if (isActive) {
onPost()
}
},
shape = ButtonBorder,
colors = ButtonDefaults
.buttonColors(
backgroundColor = if (isActive) MaterialTheme.colors.primary else Color.Gray
),
contentPadding = PaddingValues(0.dp)
) {
Text(text = "Post", color = Color.White)
}
}
@Composable
fun CloseIcon() {
Icon(
Icons.Default.Cancel,
contentDescription = "Cancel",
modifier = Size20Modifier,
tint = Color.White
)
}
@Composable
fun RelaySelectionDialog(
list: List<Relay>,
selectRelays: List<Relay>,
onClose: () -> Unit,
onPost: (list: List<Relay>) -> Unit
) {
val scope = rememberCoroutineScope()
val context = LocalContext.current
var relays by remember {
mutableStateOf(
list.map {
RelayList(
it,
if (selectRelays.isNotEmpty()) selectRelays.any { relay -> it.url == relay.url } else true
)
}
)
}
var selected by remember {
mutableStateOf(true)
}
Dialog(
onDismissRequest = { onClose() },
properties = DialogProperties(
usePlatformDefaultWidth = false,
dismissOnClickOutside = false,
decorFitsSystemWindows = false
)
) {
Surface(
modifier = Modifier
.fillMaxWidth()
.fillMaxHeight()
) {
Column(
modifier = Modifier
.fillMaxWidth()
.fillMaxHeight()
.padding(start = 10.dp, end = 10.dp, top = 10.dp)
) {
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically
) {
CloseButton(
onCancel = {
onClose()
}
)
PostButton(
onPost = {
val selectedRelays = relays.filter { it.isSelected }
if (selectedRelays.isEmpty()) {
scope.launch {
Toast.makeText(context, "Select a relay to continue", Toast.LENGTH_SHORT).show()
}
return@PostButton
}
onPost(selectedRelays.map { it.relay })
onClose()
},
isActive = true
)
}
RelaySwitch(
text = "Select/Deselect all",
checked = selected,
onClick = {
selected = !selected
relays = relays.mapIndexed { _, item ->
item.copy(isSelected = selected)
}
}
)
LazyColumn(
contentPadding = PaddingValues(
top = 10.dp,
bottom = 10.dp
)
) {
itemsIndexed(
relays,
key = { _, item -> item.relay.url }
) { index, item ->
RelaySwitch(
text = item.relay.url
.removePrefix("ws://")
.removePrefix("wss://")
.removeSuffix("/"),
checked = item.isSelected,
onClick = {
relays = relays.mapIndexed { j, item ->
if (index == j) {
item.copy(isSelected = !item.isSelected)
} else {
item
}
}
}
)
}
}
}
}
}
}
@OptIn(ExperimentalFoundationApi::class)
@Composable
fun RelaySwitch(text: String, checked: Boolean, onClick: () -> Unit, onLongPress: () -> Unit = { }) {
Row(
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier
.combinedClickable(
onClick = onClick,
onLongClick = onLongPress
)
) {
Text(
modifier = Modifier.weight(1f),
text = text
)
Switch(
checked = checked,
onCheckedChange = {
onClick()
}
)
}
}
@@ -1,49 +0,0 @@
package com.greenart7c3.nostrsigner.ui.components
import androidx.compose.runtime.Stable
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.NonCancellable
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import java.util.concurrent.atomic.AtomicBoolean
/**
* This class is designed to have a waiting time between two calls of invalidate
*/
@Stable
class BundledUpdate(
val delay: Long,
val dispatcher: CoroutineDispatcher = Dispatchers.Default
) {
private var onlyOneInBlock = AtomicBoolean()
private var invalidatesAgain = false
fun invalidate(ignoreIfDoing: Boolean = false, onUpdate: suspend () -> Unit) {
if (onlyOneInBlock.getAndSet(true)) {
if (!ignoreIfDoing) {
invalidatesAgain = true
}
return
}
val scope = CoroutineScope(Job() + dispatcher)
scope.launch {
try {
onUpdate()
delay(delay)
if (invalidatesAgain) {
onUpdate()
}
} finally {
withContext(NonCancellable) {
invalidatesAgain = false
onlyOneInBlock.set(false)
}
}
}
}
}
@@ -0,0 +1,23 @@
package com.greenart7c3.nostrsigner.ui.components
import androidx.compose.material.Button
import androidx.compose.material.ButtonDefaults
import androidx.compose.runtime.Composable
import androidx.compose.ui.graphics.Color
import com.greenart7c3.nostrsigner.ui.theme.ButtonBorder
@Composable
fun CloseButton(onCancel: () -> Unit) {
Button(
onClick = {
onCancel()
},
shape = ButtonBorder,
colors = ButtonDefaults
.buttonColors(
backgroundColor = Color.Gray
)
) {
CloseIcon()
}
}
@@ -0,0 +1,18 @@
package com.greenart7c3.nostrsigner.ui.components
import androidx.compose.material.Icon
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Cancel
import androidx.compose.runtime.Composable
import androidx.compose.ui.graphics.Color
import com.greenart7c3.nostrsigner.ui.theme.Size20Modifier
@Composable
fun CloseIcon() {
Icon(
Icons.Default.Cancel,
contentDescription = "Cancel",
modifier = Size20Modifier,
tint = Color.White
)
}
@@ -0,0 +1,32 @@
package com.greenart7c3.nostrsigner.ui.components
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.material.Button
import androidx.compose.material.ButtonDefaults
import androidx.compose.material.MaterialTheme
import androidx.compose.material.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.unit.dp
import com.greenart7c3.nostrsigner.ui.theme.ButtonBorder
@Composable
fun PostButton(modifier: Modifier = Modifier, onPost: () -> Unit = {}, isActive: Boolean) {
Button(
modifier = modifier,
onClick = {
if (isActive) {
onPost()
}
},
shape = ButtonBorder,
colors = ButtonDefaults
.buttonColors(
backgroundColor = if (isActive) MaterialTheme.colors.primary else Color.Gray
),
contentPadding = PaddingValues(0.dp)
) {
Text(text = "Post", color = Color.White)
}
}
@@ -2,6 +2,7 @@ package com.greenart7c3.nostrsigner.ui.theme
import android.app.Activity
import androidx.compose.foundation.isSystemInDarkTheme
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.Colors
import androidx.compose.material.MaterialTheme
@@ -10,6 +11,7 @@ import androidx.compose.material.darkColors
import androidx.compose.material.lightColors
import androidx.compose.runtime.Composable
import androidx.compose.runtime.SideEffect
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.toArgb
import androidx.compose.ui.platform.LocalView
@@ -31,6 +33,9 @@ val Purple500 = Color(0xFF6200EE)
val Purple700 = Color(0xFF3700B3)
val Teal200 = Color(0xFF03DAC5)
val ButtonBorder = RoundedCornerShape(20.dp)
val Size20Modifier = Modifier.size(20.dp)
private val DarkColorPalette = darkColors(
primary = Purple200,
primaryVariant = Purple700,