Activating NIP-95 support
This commit is contained in:
@@ -375,6 +375,34 @@ class Account(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun sendNip95(data: ByteArray, headerInfo: FileHeader): Note? {
|
||||||
|
if (!isWriteable()) return null
|
||||||
|
|
||||||
|
val data = FileStorageEvent.create(
|
||||||
|
mimeType = headerInfo.mimeType ?: "",
|
||||||
|
data = data,
|
||||||
|
privateKey = loggedIn.privKey!!
|
||||||
|
)
|
||||||
|
|
||||||
|
val signedEvent = FileStorageHeaderEvent.create(
|
||||||
|
data,
|
||||||
|
mimeType = headerInfo.mimeType,
|
||||||
|
hash = headerInfo.hash,
|
||||||
|
size = headerInfo.size.toString(),
|
||||||
|
blurhash = headerInfo.blurHash,
|
||||||
|
description = headerInfo.description,
|
||||||
|
privateKey = loggedIn.privKey!!
|
||||||
|
)
|
||||||
|
|
||||||
|
Client.send(data)
|
||||||
|
LocalCache.consume(data)
|
||||||
|
|
||||||
|
Client.send(signedEvent)
|
||||||
|
LocalCache.consume(signedEvent)
|
||||||
|
|
||||||
|
return LocalCache.notes[signedEvent.id]
|
||||||
|
}
|
||||||
|
|
||||||
fun sendHeader(headerInfo: FileHeader): Note? {
|
fun sendHeader(headerInfo: FileHeader): Note? {
|
||||||
if (!isWriteable()) return null
|
if (!isWriteable()) return null
|
||||||
|
|
||||||
|
|||||||
@@ -666,6 +666,32 @@ object LocalCache {
|
|||||||
refreshObservers(note)
|
refreshObservers(note)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun consume(event: FileStorageHeaderEvent) {
|
||||||
|
val note = getOrCreateNote(event.id)
|
||||||
|
|
||||||
|
// Already processed this event.
|
||||||
|
if (note.event != null) return
|
||||||
|
|
||||||
|
val author = getOrCreateUser(event.pubKey)
|
||||||
|
|
||||||
|
note.loadEvent(event, author, emptyList())
|
||||||
|
|
||||||
|
refreshObservers(note)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun consume(event: FileStorageEvent) {
|
||||||
|
val note = getOrCreateNote(event.id)
|
||||||
|
|
||||||
|
// Already processed this event.
|
||||||
|
if (note.event != null) return
|
||||||
|
|
||||||
|
val author = getOrCreateUser(event.pubKey)
|
||||||
|
|
||||||
|
note.loadEvent(event, author, emptyList())
|
||||||
|
|
||||||
|
refreshObservers(note)
|
||||||
|
}
|
||||||
|
|
||||||
fun consume(event: LnZapPaymentRequestEvent) {
|
fun consume(event: LnZapPaymentRequestEvent) {
|
||||||
// Does nothing without a response callback.
|
// Does nothing without a response callback.
|
||||||
}
|
}
|
||||||
@@ -723,6 +749,10 @@ object LocalCache {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun pruneFileStorageEvents(account: Account) {
|
||||||
|
notes.filter { it.value.event is FileStorageEvent }
|
||||||
|
}
|
||||||
|
|
||||||
fun pruneOldAndHiddenMessages(account: Account) {
|
fun pruneOldAndHiddenMessages(account: Account) {
|
||||||
channels.forEach { it ->
|
channels.forEach { it ->
|
||||||
val toBeRemoved = it.value.pruneOldAndHiddenMessages(account)
|
val toBeRemoved = it.value.pruneOldAndHiddenMessages(account)
|
||||||
|
|||||||
@@ -19,17 +19,22 @@ class FileHeader(
|
|||||||
) {
|
) {
|
||||||
companion object {
|
companion object {
|
||||||
fun prepare(fileUrl: String, mimeType: String?, description: String?, onReady: (FileHeader) -> Unit, onError: () -> Unit) {
|
fun prepare(fileUrl: String, mimeType: String?, description: String?, onReady: (FileHeader) -> Unit, onError: () -> Unit) {
|
||||||
try {
|
|
||||||
val imageData = URL(fileUrl).readBytes()
|
val imageData = URL(fileUrl).readBytes()
|
||||||
|
|
||||||
|
prepare(imageData, fileUrl, mimeType, description, onReady, onError)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun prepare(data: ByteArray, fileUrl: String, mimeType: String?, description: String?, onReady: (FileHeader) -> Unit, onError: () -> Unit) {
|
||||||
|
try {
|
||||||
val sha256 = MessageDigest.getInstance("SHA-256")
|
val sha256 = MessageDigest.getInstance("SHA-256")
|
||||||
|
|
||||||
val hash = sha256.digest(imageData).toHexKey()
|
val hash = sha256.digest(data).toHexKey()
|
||||||
val size = imageData.size
|
val size = data.size
|
||||||
|
|
||||||
val blurHash = if (mimeType?.startsWith("image/") == true) {
|
val blurHash = if (mimeType?.startsWith("image/") == true) {
|
||||||
val opt = BitmapFactory.Options()
|
val opt = BitmapFactory.Options()
|
||||||
opt.inPreferredConfig = Bitmap.Config.ARGB_8888
|
opt.inPreferredConfig = Bitmap.Config.ARGB_8888
|
||||||
val mBitmap = BitmapFactory.decodeByteArray(imageData, 0, imageData.size, opt)
|
val mBitmap = BitmapFactory.decodeByteArray(data, 0, data.size, opt)
|
||||||
|
|
||||||
val intArray = IntArray(mBitmap.width * mBitmap.height)
|
val intArray = IntArray(mBitmap.width * mBitmap.height)
|
||||||
mBitmap.getPixels(
|
mBitmap.getPixels(
|
||||||
|
|||||||
@@ -77,6 +77,8 @@ abstract class NostrDataSource(val debugName: String) {
|
|||||||
is DeletionEvent -> LocalCache.consume(event)
|
is DeletionEvent -> LocalCache.consume(event)
|
||||||
|
|
||||||
is FileHeaderEvent -> LocalCache.consume(event)
|
is FileHeaderEvent -> LocalCache.consume(event)
|
||||||
|
is FileStorageEvent -> LocalCache.consume(event)
|
||||||
|
is FileStorageHeaderEvent -> LocalCache.consume(event)
|
||||||
is LnZapEvent -> {
|
is LnZapEvent -> {
|
||||||
event.zapRequest?.let { onEvent(it, subscriptionId, relay) }
|
event.zapRequest?.let { onEvent(it, subscriptionId, relay) }
|
||||||
LocalCache.consume(event)
|
LocalCache.consume(event)
|
||||||
|
|||||||
@@ -223,6 +223,8 @@ open class Event(
|
|||||||
DeletionEvent.kind -> DeletionEvent(id, pubKey, createdAt, tags, content, sig)
|
DeletionEvent.kind -> DeletionEvent(id, pubKey, createdAt, tags, content, sig)
|
||||||
|
|
||||||
FileHeaderEvent.kind -> FileHeaderEvent(id, pubKey, createdAt, tags, content, sig)
|
FileHeaderEvent.kind -> FileHeaderEvent(id, pubKey, createdAt, tags, content, sig)
|
||||||
|
FileStorageEvent.kind -> FileStorageEvent(id, pubKey, createdAt, tags, content, sig)
|
||||||
|
FileStorageHeaderEvent.kind -> FileStorageHeaderEvent(id, pubKey, createdAt, tags, content, sig)
|
||||||
LnZapEvent.kind -> LnZapEvent(id, pubKey, createdAt, tags, content, sig)
|
LnZapEvent.kind -> LnZapEvent(id, pubKey, createdAt, tags, content, sig)
|
||||||
LnZapPaymentRequestEvent.kind -> LnZapPaymentRequestEvent(id, pubKey, createdAt, tags, content, sig)
|
LnZapPaymentRequestEvent.kind -> LnZapPaymentRequestEvent(id, pubKey, createdAt, tags, content, sig)
|
||||||
LnZapPaymentResponseEvent.kind -> LnZapPaymentResponseEvent(id, pubKey, createdAt, tags, content, sig)
|
LnZapPaymentResponseEvent.kind -> LnZapPaymentResponseEvent(id, pubKey, createdAt, tags, content, sig)
|
||||||
|
|||||||
@@ -14,6 +14,8 @@ class FileStorageHeaderEvent(
|
|||||||
sig: HexKey
|
sig: HexKey
|
||||||
) : Event(id, pubKey, createdAt, kind, tags, content, sig) {
|
) : Event(id, pubKey, createdAt, kind, tags, content, sig) {
|
||||||
|
|
||||||
|
fun dataEventId() = tags.firstOrNull { it.size > 1 && it[0] == "e" }?.get(1)
|
||||||
|
|
||||||
fun encryptionKey() = tags.firstOrNull { it.size > 2 && it[0] == ENCRYPTION_KEY }?.let { AESGCM(it[1], it[2]) }
|
fun encryptionKey() = tags.firstOrNull { it.size > 2 && it[0] == ENCRYPTION_KEY }?.let { AESGCM(it[1], it[2]) }
|
||||||
fun mimeType() = tags.firstOrNull { it.size > 1 && it[0] == MIME_TYPE }?.get(1)
|
fun mimeType() = tags.firstOrNull { it.size > 1 && it[0] == MIME_TYPE }?.get(1)
|
||||||
fun hash() = tags.firstOrNull { it.size > 1 && it[0] == HASH }?.get(1)
|
fun hash() = tags.firstOrNull { it.size > 1 && it[0] == HASH }?.get(1)
|
||||||
|
|||||||
@@ -96,8 +96,6 @@ class Relay(
|
|||||||
val type = msg[0].asString
|
val type = msg[0].asString
|
||||||
val channel = msg[1].asString
|
val channel = msg[1].asString
|
||||||
|
|
||||||
// Log.w("Relay", "New Message $type, $url, $channel, ${msg[2]}")
|
|
||||||
|
|
||||||
when (type) {
|
when (type) {
|
||||||
"EVENT" -> {
|
"EVENT" -> {
|
||||||
val event = Event.fromJson(msg[2], Client.lenient)
|
val event = Event.fromJson(msg[2], Client.lenient)
|
||||||
@@ -116,15 +114,15 @@ class Relay(
|
|||||||
it.onRelayStateChange(this@Relay, Type.EOSE, channel)
|
it.onRelayStateChange(this@Relay, Type.EOSE, channel)
|
||||||
}
|
}
|
||||||
"NOTICE" -> listeners.forEach {
|
"NOTICE" -> listeners.forEach {
|
||||||
// Log.w("Relay", "Relay onNotice $url, $channel")
|
Log.w("Relay", "Relay onNotice $url, $channel")
|
||||||
it.onError(this@Relay, channel, Error("Relay sent notice: " + channel))
|
it.onError(this@Relay, channel, Error("Relay sent notice: " + channel))
|
||||||
}
|
}
|
||||||
"OK" -> listeners.forEach {
|
"OK" -> listeners.forEach {
|
||||||
// Log.w("Relay", "AUTHSENT Relay on OK $url, ${msg[1].asString}, ${msg[2].asBoolean}, ${msg[3].asString}")
|
Log.w("Relay", "Relay on OK $url, ${msg[1].asString}, ${msg[2].asBoolean}, ${msg[3].asString}")
|
||||||
it.onSendResponse(this@Relay, msg[1].asString, msg[2].asBoolean, msg[3].asString)
|
it.onSendResponse(this@Relay, msg[1].asString, msg[2].asBoolean, msg[3].asString)
|
||||||
}
|
}
|
||||||
"AUTH" -> listeners.forEach {
|
"AUTH" -> listeners.forEach {
|
||||||
// Log.w("Relay", "Relay AUTHSENT $url, ${msg[1].asString}")
|
// Log.w("Relay", "Relay$url, ${msg[1].asString}")
|
||||||
it.onAuth(this@Relay, msg[1].asString)
|
it.onAuth(this@Relay, msg[1].asString)
|
||||||
}
|
}
|
||||||
else -> listeners.forEach {
|
else -> listeners.forEach {
|
||||||
@@ -207,7 +205,7 @@ class Relay(
|
|||||||
val filters = Client.getSubscriptionFilters(requestId).filter { activeTypes.intersect(it.types).isNotEmpty() }
|
val filters = Client.getSubscriptionFilters(requestId).filter { activeTypes.intersect(it.types).isNotEmpty() }
|
||||||
if (filters.isNotEmpty()) {
|
if (filters.isNotEmpty()) {
|
||||||
val request =
|
val request =
|
||||||
"""["REQ","$requestId",${filters.take(40).joinToString(",") { it.filter.toJson(url) }}]"""
|
"""["REQ","$requestId",${filters.take(12).joinToString(",") { it.filter.toJson(url) }}]"""
|
||||||
//println("FILTERSSENT $url $request")
|
//println("FILTERSSENT $url $request")
|
||||||
socket?.send(request)
|
socket?.send(request)
|
||||||
eventUploadCounterInBytes += request.bytesUsedInMemory()
|
eventUploadCounterInBytes += request.bytesUsedInMemory()
|
||||||
|
|||||||
@@ -489,7 +489,7 @@ fun SearchButton(onPost: () -> Unit = {}, isActive: Boolean, modifier: Modifier
|
|||||||
}
|
}
|
||||||
|
|
||||||
enum class ServersAvailable {
|
enum class ServersAvailable {
|
||||||
IMGUR, NOSTR_BUILD, NOSTR_IMG
|
IMGUR, NOSTR_BUILD, NOSTR_IMG, NIP95
|
||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
@@ -508,7 +508,8 @@ fun ImageVideoDescription(
|
|||||||
|
|
||||||
val fileServers = listOf(
|
val fileServers = listOf(
|
||||||
Pair(ServersAvailable.IMGUR, "imgur.com"),
|
Pair(ServersAvailable.IMGUR, "imgur.com"),
|
||||||
Pair(ServersAvailable.NOSTR_IMG, "nostrimg.com")
|
Pair(ServersAvailable.NOSTR_IMG, "nostrimg.com"),
|
||||||
|
Pair(ServersAvailable.NIP95, "your relays (NIP-95)")
|
||||||
)
|
)
|
||||||
|
|
||||||
val fileServerOptions = fileServers.map { it.second }
|
val fileServerOptions = fileServers.map { it.second }
|
||||||
|
|||||||
@@ -114,12 +114,20 @@ open class NewPostViewModel : ViewModel() {
|
|||||||
isUploadingImage = true
|
isUploadingImage = true
|
||||||
contentToAddUrl = null
|
contentToAddUrl = null
|
||||||
|
|
||||||
|
val contentResolver = context.contentResolver
|
||||||
|
|
||||||
|
if (server == ServersAvailable.NIP95) {
|
||||||
|
val contentType = contentResolver.getType(it)
|
||||||
|
contentResolver.openInputStream(it)?.use {
|
||||||
|
createNIP95Record(it.readBytes(), contentType, description)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
ImageUploader.uploadImage(
|
ImageUploader.uploadImage(
|
||||||
uri = it,
|
uri = it,
|
||||||
server = server,
|
server = server,
|
||||||
contentResolver = context.contentResolver,
|
contentResolver = contentResolver,
|
||||||
onSuccess = { imageUrl, mimeType ->
|
onSuccess = { imageUrl, mimeType ->
|
||||||
createNIP97Record(imageUrl, mimeType, description)
|
createNIP94Record(imageUrl, mimeType, description)
|
||||||
},
|
},
|
||||||
onError = {
|
onError = {
|
||||||
isUploadingImage = false
|
isUploadingImage = false
|
||||||
@@ -129,6 +137,7 @@ open class NewPostViewModel : ViewModel() {
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
open fun cancel() {
|
open fun cancel() {
|
||||||
message = TextFieldValue("")
|
message = TextFieldValue("")
|
||||||
@@ -212,7 +221,7 @@ open class NewPostViewModel : ViewModel() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun createNIP97Record(imageUrl: String, mimeType: String?, description: String) {
|
fun createNIP94Record(imageUrl: String, mimeType: String?, description: String) {
|
||||||
viewModelScope.launch(Dispatchers.IO) {
|
viewModelScope.launch(Dispatchers.IO) {
|
||||||
// Images don't seem to be ready immediately after upload
|
// Images don't seem to be ready immediately after upload
|
||||||
|
|
||||||
@@ -249,6 +258,34 @@ open class NewPostViewModel : ViewModel() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun createNIP95Record(bytes: ByteArray, mimeType: String?, description: String) {
|
||||||
|
viewModelScope.launch(Dispatchers.IO) {
|
||||||
|
FileHeader.prepare(
|
||||||
|
bytes,
|
||||||
|
"",
|
||||||
|
mimeType,
|
||||||
|
description,
|
||||||
|
onReady = {
|
||||||
|
val note = account?.sendNip95(bytes, headerInfo = it)
|
||||||
|
|
||||||
|
isUploadingImage = false
|
||||||
|
|
||||||
|
note?.let {
|
||||||
|
message = TextFieldValue(message.text + "\n\nnostr:" + it.toNEvent())
|
||||||
|
}
|
||||||
|
|
||||||
|
urlPreview = findUrlInMessage()
|
||||||
|
},
|
||||||
|
onError = {
|
||||||
|
isUploadingImage = false
|
||||||
|
viewModelScope.launch {
|
||||||
|
imageUploadingError.emit("Failed to upload the image / video")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fun selectImage(uri: Uri) {
|
fun selectImage(uri: Uri) {
|
||||||
contentToAddUrl = uri
|
contentToAddUrl = uri
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -810,17 +810,18 @@ fun FileHeaderDisplay(note: Note) {
|
|||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun FileStorageHeaderDisplay(baseNote: Note) {
|
fun FileStorageHeaderDisplay(baseNote: Note) {
|
||||||
val fileNote = baseNote.replyTo?.firstOrNull() ?: return
|
val eventHeader = (baseNote.event as? FileStorageHeaderEvent) ?: return
|
||||||
|
|
||||||
|
val fileNote = eventHeader.dataEventId()?.let { LocalCache.checkGetOrCreateNote(it) } ?: return
|
||||||
|
|
||||||
val noteState by fileNote.live().metadata.observeAsState()
|
val noteState by fileNote.live().metadata.observeAsState()
|
||||||
val note = noteState?.note
|
val note = noteState?.note
|
||||||
|
|
||||||
val eventBytes = (note?.event as? FileStorageEvent)
|
val eventBytes = (note?.event as? FileStorageEvent)
|
||||||
val eventHeader = (baseNote.event as? FileStorageHeaderEvent) ?: return
|
|
||||||
|
|
||||||
var content by remember { mutableStateOf<ZoomableContent?>(null) }
|
var content by remember { mutableStateOf<ZoomableContent?>(null) }
|
||||||
|
|
||||||
LaunchedEffect(key1 = eventHeader.id) {
|
LaunchedEffect(key1 = eventHeader.id, key2 = noteState) {
|
||||||
withContext(Dispatchers.IO) {
|
withContext(Dispatchers.IO) {
|
||||||
val bytes = eventBytes?.decode()
|
val bytes = eventBytes?.decode()
|
||||||
val blurHash = eventHeader.blurhash()
|
val blurHash = eventHeader.blurhash()
|
||||||
|
|||||||
Reference in New Issue
Block a user