Compare commits

..
72 Commits
Author SHA1 Message Date
Vitor Pamplona 3e7bbc03c1 v0.71.0 2023-07-25 18:54:15 -04:00
Vitor Pamplona 5ba091de0d Allows following of Geohashes 2023-07-25 18:45:28 -04:00
Vitor Pamplona 1098c31787 - Adds support for GeoHash
- Refactors New Post Buttons to make them more similar to one another.
2023-07-25 16:52:32 -04:00
Vitor Pamplona c20277a754 Improvements on Translation Indexes 2023-07-25 12:06:17 -04:00
Vitor Pamplona a5e40d5ac9 Reviews closing of response.body calls 2023-07-25 11:20:27 -04:00
Vitor Pamplona aab42bb341 Refactoring of unused elements in VideoView 2023-07-25 11:13:17 -04:00
Vitor Pamplona 748349244e Fixes memory not closing the response body 2023-07-25 11:13:05 -04:00
Vitor Pamplona 9e9c8befe0 Fixes memory leak when services are destroyed 2023-07-25 11:12:43 -04:00
Vitor Pamplona b9fb11bdf3 Initializes the VideoCache when creating the app so that the PlaybackService doesn't suffer with delays in accessing the disk to create the cache. 2023-07-25 11:12:25 -04:00
Vitor Pamplona cd6f9576cc Refactors Relay class to remove the Synchronized block 2023-07-25 10:23:20 -04:00
Vitor Pamplona b919b165d6 Improves reachability of the relay screen when keyboard is visible 2023-07-25 10:22:52 -04:00
Vitor Pamplona a2d9a3f770 Runs the DisposableEffect before moving to load the video. 2023-07-25 09:03:56 -04:00
Vitor Pamplona 39fccd5442 Only asks for notification permissions on Tiramisu 2023-07-25 08:46:25 -04:00
Vitor Pamplona cfeaccb26e Reverts the startup redirection because navController might not be ready in the onCreate 2023-07-25 08:31:50 -04:00
Vitor Pamplona 08a2a2b428 No need for the secp256k1 instance anymore 2023-07-25 08:14:32 -04:00
Vitor Pamplona 297bcf2762 v0.70.8 2023-07-24 18:29:49 -04:00
Vitor Pamplona 092d333378 Fixes Notification Request for Samsung phones 2023-07-24 18:19:08 -04:00
Vitor Pamplona eb58e9ea39 Fixes a crash when video playback service isn't ready 2023-07-24 18:18:36 -04:00
Vitor Pamplona 63c498da87 v0.70.7 2023-07-24 16:31:33 -04:00
Vitor Pamplona b40dcb4bd8 Maybe this solves the back button issue in some phones 2023-07-24 16:30:56 -04:00
Vitor Pamplona 49fe1d0743 Fixes bug with Base64 encoding 2023-07-24 16:28:41 -04:00
Vitor Pamplona 25c40b89d7 v0.70.6 2023-07-24 15:39:52 -04:00
Vitor Pamplona bfe1e78e57 Moves the starting navigation to outside the compose element. 2023-07-24 15:36:40 -04:00
Vitor Pamplona fa79ddc573 Merge branch 'main' of https://github.com/vitorpamplona/amethyst 2023-07-24 15:22:13 -04:00
Vitor Pamplona 653e8200ca Fixes a crash when onNewIntent is called before onCreate 2023-07-24 15:10:33 -04:00
Vitor PamplonaandGitHub 8806ca6534 Merge pull request #522 from greenart7c3/main
add a button to return to default relay list
2023-07-24 13:21:11 -04:00
greenart7c3 897306e16e show paid relays after resetting relay list to default 2023-07-24 14:03:42 -03:00
greenart7c3 1e25688f9d add a button to return to default relay list 2023-07-24 13:55:55 -03:00
Vitor PamplonaandGitHub 2b6abcbfb9 Merge pull request #521 from greenart7c3/main
add missing OptIn when using GlobalScope
2023-07-24 12:19:44 -04:00
greenart7c3 ce33190177 add missing OptIn when using GlobalScope 2023-07-24 13:13:55 -03:00
Vitor Pamplona d68b14532d Fixing Test cases 2023-07-24 11:54:09 -04:00
Vitor Pamplona e83cfe4f07 Removes dirty word parser from HexUtils and puts into the New Message Processor. 2023-07-24 11:47:25 -04:00
Vitor Pamplona d3e9cc0788 Moves Bech32 encoders to Bech32 utils 2023-07-24 11:46:56 -04:00
Vitor Pamplona 87990026ea Refactoring dependency path to center in CryptoUtils 2023-07-24 11:34:16 -04:00
Vitor Pamplona 7923f5d854 Moves Bech Utility methods from CryptoUtils to BechUtils 2023-07-24 11:18:57 -04:00
Vitor Pamplona 69eea8824a Removes the reuse of the SHA256 digest due to buffer sharing issues in a multithreaded environment. 2023-07-24 11:17:01 -04:00
Vitor Pamplona a1517a67f5 Renames Persona to KeyPair and Utils to CryptoUtils 2023-07-24 11:13:33 -04:00
Vitor Pamplona 480ae479af Imports NostrPostr utility classes directly via source to avoid dependency issues on unnecessary libraries. 2023-07-24 11:09:43 -04:00
Vitor Pamplona 94213ebf83 Quick test for LnInvoice calculation amounts 2023-07-24 10:19:13 -04:00
Vitor PamplonaandGitHub eedce96453 Merge pull request #520 from greenart7c3/select_all
Fixes for relay selection dialog
2023-07-24 08:20:19 -04:00
greenart7c3 d6886f51f7 removed unused annotation 2023-07-24 07:56:21 -03:00
greenart7c3 0497f1e625 add select all switch, fix switch not clickable when relay name is too long 2023-07-24 07:46:32 -03:00
Vitor Pamplona 95ac046a09 Simplifying relay connection status 2023-07-23 18:37:37 -04:00
Vitor Pamplona 3e6938bf51 Fixes the use of OkHttpClient 2023-07-23 18:37:05 -04:00
Vitor Pamplona 786b8436f8 Refactoring of the Connectivity Settings 2023-07-23 16:33:18 -04:00
Vitor Pamplona b055e520c2 Avoids the animations when loading NIP94 and NIP95 content 2023-07-23 16:32:36 -04:00
Vitor Pamplona 9d75fb7074 Simplifies Annotations 2023-07-22 18:31:23 -04:00
Vitor Pamplona 5434f0fef7 Moves prepare controller to a try catch block 2023-07-22 18:29:41 -04:00
Vitor Pamplona ed68ca5f26 Moves playback service startup to the IO Thread 2023-07-22 18:27:25 -04:00
Vitor Pamplona 4eea36678e Pauses the service only after the debug messages 2023-07-22 18:27:02 -04:00
Vitor Pamplona 15ce0b3e05 Uses OKHttp instead of Jsoup to download UrlPreview cards 2023-07-22 18:26:43 -04:00
Vitor Pamplona eb73da1993 Activates Strict mode in debug 2023-07-22 18:26:12 -04:00
Vitor Pamplona 01ba21d303 Updates firebase version 2023-07-22 18:25:37 -04:00
Vitor PamplonaandGitHub 442eecf916 Merge pull request #518 from ZsZolee/main
Adding additional Hungarian translation
2023-07-22 14:07:27 -04:00
ZoltanandGitHub 231d02ac0d Adding additional Hungarian translation 2023-07-22 18:46:13 +02:00
Vitor Pamplona 43b011d3da v0.70.5 2023-07-21 21:00:32 -04:00
Vitor Pamplona 8a12cc3cce Releases the video player on Mutex when destroying the app. 2023-07-21 20:57:07 -04:00
Vitor Pamplona 7e03870c0b Releases the Mutex when stopping the feed. 2023-07-21 20:49:57 -04:00
Vitor Pamplona a6f56416e3 Fixes lightning colors 2023-07-21 20:41:40 -04:00
Vitor Pamplona efe7772135 Fixes color of the NIP05 address in the profile 2023-07-21 20:39:47 -04:00
Vitor Pamplona 15c96ce007 Removes the space in the video of a live activity mention 2023-07-21 17:48:09 -04:00
Vitor Pamplona 78d2a712c5 v0.70.4 2023-07-21 17:29:51 -04:00
Vitor Pamplona 2664292993 Moves wake lock to activate only when the video is actually playing 2023-07-21 17:08:56 -04:00
Vitor Pamplona b9813c2c8f v0.70.3 2023-07-21 11:48:03 -04:00
Vitor Pamplona 0c00e6512f Modifier refactoring 2023-07-21 11:47:28 -04:00
Vitor Pamplona cf31bfe5ea Adds a space when rendering inline images 2023-07-21 11:47:08 -04:00
Vitor Pamplona 5cdceb5194 Moves the loading of an Accounts backup contacts to the IO Thread 2023-07-21 11:46:31 -04:00
Vitor Pamplona 2c82e6c447 Adjustments to Modifiers in the Chatroom screen 2023-07-21 11:21:07 -04:00
Vitor Pamplona f56f3b6105 Merge branch 'main' of https://github.com/vitorpamplona/amethyst 2023-07-21 11:16:55 -04:00
Vitor PamplonaandGitHub 9420ddc5c7 Merge pull request #517 from greenart7c3/main
fix KeepPlayingButton color in light theme
2023-07-21 11:16:41 -04:00
greenart7c3 ade1d83817 fix KeepPlayingButton color in light theme 2023-07-21 12:11:23 -03:00
Vitor Pamplona 9a517380a0 Refactors ChatroomHeader compose 2023-07-21 11:02:16 -04:00
142 changed files with 2860 additions and 1302 deletions
+6 -10
View File
@@ -13,8 +13,8 @@ android {
applicationId "com.vitorpamplona.amethyst"
minSdk 26
targetSdk 33
versionCode 259
versionName "0.70.2"
versionCode 266
versionName "0.71.0"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
vectorDrawables {
@@ -118,13 +118,6 @@ dependencies {
// Bitcoin secp256k1 bindings to Android
implementation 'fr.acinq.secp256k1:secp256k1-kmp-jni-android:0.10.1'
// Nostr Base Protocol
implementation('com.github.vitorpamplona.NostrPostr:nostrpostrlib:master-SNAPSHOT') {
exclude group:'fr.acinq.secp256k1'
exclude module: 'guava'
exclude module: 'guava-testlib'
}
// Websockets API
implementation 'com.squareup.okhttp3:okhttp:5.0.0-alpha.11'
@@ -186,7 +179,7 @@ dependencies {
playImplementation 'com.google.mlkit:translate:17.0.1'
// PushNotifications
playImplementation platform('com.google.firebase:firebase-bom:32.1.1')
playImplementation platform('com.google.firebase:firebase-bom:32.2.0')
playImplementation 'com.google.firebase:firebase-messaging-ktx'
// Charts
@@ -198,6 +191,9 @@ dependencies {
// immutable collections to avoid recomposition
implementation("org.jetbrains.kotlinx:kotlinx-collections-immutable:0.3.5")
// GeoHash
implementation 'com.github.drfonfon:android-kotlin-geohash:1.0'
// Video compression lib
implementation 'com.github.AbedElazizShe:LightCompressor:1.3.1'
// Image compression lib
@@ -0,0 +1,24 @@
package com.vitorpamplona.amethyst
import androidx.test.ext.junit.runners.AndroidJUnit4
import com.vitorpamplona.amethyst.service.lnurl.LnInvoiceUtil
import org.junit.Assert
import org.junit.Test
import org.junit.runner.RunWith
@RunWith(AndroidJUnit4::class)
class LnInvoiceUtilTest {
@Test
fun test100KAmountCalculation() {
val bolt11 = "lnbc1m1pjt9u0qsp553q90pj5mafzv20w45eqavned9tgwhl4q99n9s5ppcw24nzw3zeqpp5002kd3ktym67du86kj665fgaev7ka8ys7j5yz5fg686lr5e2gfkshp5dkk27nnuax05az3pk2r6ytxtvwn5j4xzsq9ajprhc7crjkmgvr3qxqyjw5qcqpjrzjqtzxvfsuxe4l92pf97tt4rcgpy2xalkmlwexh899wqxf83l8nwv4xzh0gvqq89qqqqqqqqlgqqqqq0gqvs9qxpqysgqx5mz04wd7kqu5zhhel9enr036hjrp4gga0nz084p2asjl36a0zmrk6mhqa249zsgqref2rlvhffm73u7rxgr47gden6rugup4ksvpzsqvds4pz"
// Context of the app under test.
Assert.assertEquals(100000, LnInvoiceUtil.getAmountInSats(bolt11).toLong())
}
@Test
fun test100GAmountCalculation() {
val bolt11 = "lnbc1000000000000000p1pjtxqf0pp5myqxhcufqy56elfsg9dd4dthnqptusnnpwnul7u86l95xzjgqd8shp5gueg34sgm3u3nxqjqyunvvqdu0pr6jz6mwh4ew4886f2lpf4cmrqxqztgsp5w0cdfd45dfnqwex5gn85x7fru3jcrxhlcx3enx835477m3gdfcuq9qyyssqelrcmm7p9qazgjuxtdg7sd8nq5cscl2tratjlclt5rk5mc7uq2lphq3r2a43j5ua4leakc4emq8yp2yxdnzvzszpw6u2afac0kgl7hspfj67ta"
// Context of the app under test.
Assert.assertEquals(100000000000, LnInvoiceUtil.getAmountInSats(bolt11).toLong())
}
}
@@ -99,4 +99,13 @@ class TranslationsTest {
"en"
)
}
@Test
fun testEmoji() {
assertTranslateContains(
"https://cdn.nostr.build/i/df3783dcdf7dd289ba02ba538dc039c8fe1d4db055e580b81604ed88c6af4ee0.jpg",
"\uD83E\uDD23 https://cdn.nostr.build/i/df3783dcdf7dd289ba02ba538dc039c8fe1d4db055e580b81604ed88c6af4ee0.jpg ",
"pt"
)
}
}
+6
View File
@@ -34,6 +34,12 @@
<!-- Keeps screen on while playing videos -->
<uses-permission android:name="android.permission.WAKE_LOCK" />
<!-- This notification permission is needed for some phones -->
<uses-permission android:name="android.permission.POST_NOTIFICATIONS"/>
<!-- Adds Geohash to posts if active -->
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
<!-- Old permission to access media -->
<uses-permission
android:name="android.permission.WRITE_EXTERNAL_STORAGE"
@@ -1,11 +1,38 @@
package com.vitorpamplona.amethyst
import android.app.Application
import android.os.StrictMode
import android.os.StrictMode.ThreadPolicy
import android.os.StrictMode.VmPolicy
class Amethyst : Application() {
@androidx.annotation.OptIn(androidx.media3.common.util.UnstableApi::class)
override fun onCreate() {
super.onCreate()
instance = this
VideoCache.initFileCache(instance)
if (BuildConfig.DEBUG) {
StrictMode.setThreadPolicy(
ThreadPolicy.Builder()
.detectAll()
.penaltyLog()
.build()
)
StrictMode.setVmPolicy(
VmPolicy.Builder()
.detectActivityLeaks()
.detectFileUriExposure()
.detectContentUriWithoutPermission()
.detectLeakedClosableObjects()
.detectLeakedRegistrationObjects()
.detectLeakedSqlLiteObjects()
.penaltyLog()
// .penaltyDeath()
.build()
)
}
}
companion object {
@@ -7,22 +7,24 @@ import androidx.compose.runtime.Immutable
import com.google.gson.GsonBuilder
import com.google.gson.reflect.TypeToken
import com.vitorpamplona.amethyst.model.Account
import com.vitorpamplona.amethyst.model.ConnectivityType
import com.vitorpamplona.amethyst.model.GLOBAL_FOLLOWS
import com.vitorpamplona.amethyst.model.KIND3_FOLLOWS
import com.vitorpamplona.amethyst.model.RelaySetupInfo
import com.vitorpamplona.amethyst.model.Settings
import com.vitorpamplona.amethyst.model.hexToByteArray
import com.vitorpamplona.amethyst.model.parseConnectivityType
import com.vitorpamplona.amethyst.model.toHexKey
import com.vitorpamplona.amethyst.service.HttpClient
import com.vitorpamplona.amethyst.service.KeyPair
import com.vitorpamplona.amethyst.service.model.ContactListEvent
import com.vitorpamplona.amethyst.service.model.Event
import com.vitorpamplona.amethyst.service.model.Event.Companion.getRefinedEvent
import com.vitorpamplona.amethyst.service.model.LnZapEvent
import com.vitorpamplona.amethyst.service.toNpub
import com.vitorpamplona.amethyst.ui.actions.ServersAvailable
import com.vitorpamplona.amethyst.ui.note.Nip47URI
import fr.acinq.secp256k1.Hex
import nostr.postr.Persona
import nostr.postr.toHex
import nostr.postr.toNpub
import java.io.File
import java.util.Locale
@@ -209,8 +211,8 @@ object LocalPreferences {
fun saveToEncryptedStorage(account: Account) {
val prefs = encryptedPreferences(account.userProfile().pubkeyNpub())
prefs.edit().apply {
account.loggedIn.privKey?.let { putString(PrefKeys.NOSTR_PRIVKEY, it.toHex()) }
account.loggedIn.pubKey.let { putString(PrefKeys.NOSTR_PUBKEY, it.toHex()) }
account.keyPair.privKey?.let { putString(PrefKeys.NOSTR_PRIVKEY, it.toHexKey()) }
account.keyPair.pubKey.let { putString(PrefKeys.NOSTR_PUBKEY, it.toHexKey()) }
putStringSet(PrefKeys.FOLLOWING_CHANNELS, account.followingChannels)
putStringSet(PrefKeys.FOLLOWING_COMMUNITIES, account.followingCommunities)
putStringSet(PrefKeys.HIDDEN_USERS, account.hiddenUsers)
@@ -245,21 +247,21 @@ object LocalPreferences {
val globalPrefs = encryptedPreferences()
globalPrefs.edit().apply {
if (account.settings.automaticallyShowImages == null) {
if (account.settings.automaticallyShowImages.prefCode == null) {
remove(PrefKeys.AUTOMATICALLY_SHOW_IMAGES)
} else {
putBoolean(PrefKeys.AUTOMATICALLY_SHOW_IMAGES, account.settings.automaticallyShowImages!!)
putBoolean(PrefKeys.AUTOMATICALLY_SHOW_IMAGES, account.settings.automaticallyShowImages.prefCode!!)
}
if (account.settings.automaticallyStartPlayback == null) {
if (account.settings.automaticallyStartPlayback.prefCode == null) {
remove(PrefKeys.AUTOMATICALLY_START_PLAYBACK)
} else {
putBoolean(PrefKeys.AUTOMATICALLY_START_PLAYBACK, account.settings.automaticallyStartPlayback!!)
putBoolean(PrefKeys.AUTOMATICALLY_START_PLAYBACK, account.settings.automaticallyStartPlayback.prefCode!!)
}
if (account.settings.automaticallyShowUrlPreview == null) {
if (account.settings.automaticallyShowUrlPreview.prefCode == null) {
remove(PrefKeys.AUTOMATICALLY_LOAD_URL_PREVIEW)
} else {
putBoolean(PrefKeys.AUTOMATICALLY_LOAD_URL_PREVIEW, account.settings.automaticallyShowUrlPreview!!)
putBoolean(PrefKeys.AUTOMATICALLY_LOAD_URL_PREVIEW, account.settings.automaticallyShowUrlPreview.prefCode!!)
}
putString(PrefKeys.PREFERRED_LANGUAGE, account.settings.preferredLanguage ?: "")
}.apply()
@@ -390,27 +392,27 @@ object LocalPreferences {
val settings = Settings()
encryptedPreferences().apply {
settings.automaticallyShowImages = if (contains(PrefKeys.AUTOMATICALLY_SHOW_IMAGES)) {
getBoolean(PrefKeys.AUTOMATICALLY_SHOW_IMAGES, false)
parseConnectivityType(getBoolean(PrefKeys.AUTOMATICALLY_SHOW_IMAGES, false))
} else {
null
ConnectivityType.ALWAYS
}
settings.automaticallyStartPlayback = if (contains(PrefKeys.AUTOMATICALLY_START_PLAYBACK)) {
getBoolean(PrefKeys.AUTOMATICALLY_START_PLAYBACK, false)
parseConnectivityType(getBoolean(PrefKeys.AUTOMATICALLY_START_PLAYBACK, false))
} else {
null
ConnectivityType.ALWAYS
}
settings.automaticallyShowUrlPreview = if (contains(PrefKeys.AUTOMATICALLY_LOAD_URL_PREVIEW)) {
getBoolean(PrefKeys.AUTOMATICALLY_LOAD_URL_PREVIEW, false)
parseConnectivityType(getBoolean(PrefKeys.AUTOMATICALLY_LOAD_URL_PREVIEW, false))
} else {
null
ConnectivityType.ALWAYS
}
settings.preferredLanguage = getString(PrefKeys.PREFERRED_LANGUAGE, "")
}
val a = Account(
loggedIn = Persona(privKey = privKey?.hexToByteArray(), pubKey = pubKey.hexToByteArray()),
keyPair = KeyPair(privKey = privKey?.hexToByteArray(), pubKey = pubKey.hexToByteArray()),
followingChannels = followingChannels,
followingCommunities = followingCommunities,
hiddenUsers = hiddenUsers,
@@ -12,6 +12,7 @@ import androidx.media3.common.Player.STATE_READY
import androidx.media3.exoplayer.ExoPlayer
import androidx.media3.session.MediaSession
import com.vitorpamplona.amethyst.ui.MainActivity
import kotlinx.coroutines.DelicateCoroutinesApi
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.launch
@@ -66,7 +67,6 @@ class MultiPlayerPlaybackManager(
repeatMode = Player.REPEAT_MODE_ALL
videoScalingMode = C.VIDEO_SCALING_MODE_SCALE_TO_FIT
volume = 0f
setWakeMode(C.WAKE_MODE_NETWORK)
}
val mediaSession = MediaSession.Builder(context, player).run {
@@ -80,8 +80,10 @@ class MultiPlayerPlaybackManager(
player.addListener(object : Player.Listener {
override fun onIsPlayingChanged(isPlaying: Boolean) {
if (isPlaying) {
player.setWakeMode(C.WAKE_MODE_NETWORK)
playingMap.put(id, mediaSession)
} else {
player.setWakeMode(C.WAKE_MODE_NONE)
cache.put(id, mediaSession)
playingMap.remove(id, mediaSession)
}
@@ -115,6 +117,7 @@ class MultiPlayerPlaybackManager(
return mediaSession
}
@OptIn(DelicateCoroutinesApi::class)
fun releaseAppPlayers() {
GlobalScope.launch(Dispatchers.Main) {
cache.evictAll()
@@ -3,6 +3,7 @@ package com.vitorpamplona.amethyst
import android.content.ComponentName
import android.content.Context
import android.os.Bundle
import android.util.Log
import androidx.media3.session.MediaController
import androidx.media3.session.SessionToken
import com.google.common.util.concurrent.MoreExecutors
@@ -16,25 +17,32 @@ object PlaybackClientController {
context: Context,
onReady: (MediaController) -> Unit
) {
// creating a bundle object
// creating a bundle object
val bundle = Bundle()
bundle.putString("id", controllerID)
bundle.putString("uri", videoUri)
bundle.putString("callbackUri", callbackUri)
try {
// creating a bundle object
// creating a bundle object
val bundle = Bundle()
bundle.putString("id", controllerID)
bundle.putString("uri", videoUri)
bundle.putString("callbackUri", callbackUri)
val sessionTokenLocal = SessionToken(context, ComponentName(context, PlaybackService::class.java))
val controllerFuture = MediaController
.Builder(context, sessionTokenLocal)
.setConnectionHints(bundle)
.buildAsync()
val sessionTokenLocal = SessionToken(context, ComponentName(context, PlaybackService::class.java))
val controllerFuture = MediaController
.Builder(context, sessionTokenLocal)
.setConnectionHints(bundle)
.buildAsync()
controllerFuture.addListener(
{
val controller = controllerFuture.get()
onReady(controller)
},
MoreExecutors.directExecutor()
)
controllerFuture.addListener(
{
try {
onReady(controllerFuture.get())
} catch (e: Exception) {
Log.e("Playback Client", "Failed to load Playback Client for $videoUri", e)
}
},
MoreExecutors.directExecutor()
)
} catch (e: Exception) {
Log.e("Playback Client", "Failed to load Playback Client for $videoUri", e)
}
}
}
@@ -6,7 +6,6 @@ import androidx.media3.datasource.okhttp.OkHttpDataSource
import androidx.media3.exoplayer.hls.HlsMediaSource
import androidx.media3.exoplayer.source.MediaSource
import androidx.media3.exoplayer.source.ProgressiveMediaSource
import androidx.media3.session.DefaultMediaNotificationProvider
import androidx.media3.session.MediaSession
import androidx.media3.session.MediaSessionService
import com.vitorpamplona.amethyst.service.HttpClient
@@ -24,7 +23,7 @@ class PlaybackService : MediaSessionService() {
}
fun newProgressiveDataSource(): MediaSource.Factory {
return ProgressiveMediaSource.Factory(VideoCache.get(applicationContext, HttpClient.getHttpClient()))
return ProgressiveMediaSource.Factory(VideoCache.get(Amethyst.instance, HttpClient.getHttpClient()))
}
// Create your Player and MediaSession in the onCreate lifecycle event
@@ -37,23 +36,23 @@ class PlaybackService : MediaSessionService() {
managerLocal = MultiPlayerPlaybackManager(cachedPositions = videoViewedPositionCache)
// Stop all videos and recreates all managers when the proxy changes.
HttpClient.proxyChangeListeners.add {
val toDestroyHls = managerHls
val toDestroyProgressive = managerProgressive
HttpClient.proxyChangeListeners.add(this@PlaybackService::onProxyUpdated)
}
managerHls = MultiPlayerPlaybackManager(newHslDataSource(), videoViewedPositionCache)
managerProgressive = MultiPlayerPlaybackManager(newProgressiveDataSource(), videoViewedPositionCache)
private fun onProxyUpdated() {
val toDestroyHls = managerHls
val toDestroyProgressive = managerProgressive
toDestroyHls?.releaseAppPlayers()
toDestroyProgressive?.releaseAppPlayers()
}
managerHls = MultiPlayerPlaybackManager(newHslDataSource(), videoViewedPositionCache)
managerProgressive = MultiPlayerPlaybackManager(newProgressiveDataSource(), videoViewedPositionCache)
setMediaNotificationProvider(
DefaultMediaNotificationProvider.Builder(applicationContext).build()
)
toDestroyHls?.releaseAppPlayers()
toDestroyProgressive?.releaseAppPlayers()
}
override fun onDestroy() {
HttpClient.proxyChangeListeners.remove(this@PlaybackService::onProxyUpdated)
managerHls?.releaseAppPlayers()
managerLocal?.releaseAppPlayers()
managerProgressive?.releaseAppPlayers()
@@ -17,6 +17,7 @@ import com.vitorpamplona.amethyst.service.NostrChatroomDataSource
import com.vitorpamplona.amethyst.service.NostrChatroomListDataSource
import com.vitorpamplona.amethyst.service.NostrCommunityDataSource
import com.vitorpamplona.amethyst.service.NostrDiscoveryDataSource
import com.vitorpamplona.amethyst.service.NostrGeohashDataSource
import com.vitorpamplona.amethyst.service.NostrHashtagDataSource
import com.vitorpamplona.amethyst.service.NostrHomeDataSource
import com.vitorpamplona.amethyst.service.NostrSearchEventOrUserDataSource
@@ -97,6 +98,7 @@ object ServiceManager {
NostrCommunityDataSource.stop()
NostrHashtagDataSource.stop()
NostrGeohashDataSource.stop()
NostrSearchEventOrUserDataSource.stop()
NostrSingleChannelDataSource.stop()
NostrSingleEventDataSource.stop()
@@ -21,8 +21,7 @@ import okhttp3.OkHttpClient
lateinit var cacheDataSourceFactory: CacheDataSource.Factory
@Synchronized
@androidx.annotation.OptIn(androidx.media3.common.util.UnstableApi::class)
private fun init(context: Context, client: OkHttpClient) {
fun initFileCache(context: Context) {
exoDatabaseProvider = StandaloneDatabaseProvider(context)
simpleCache = SimpleCache(
@@ -30,8 +29,6 @@ import okhttp3.OkHttpClient
leastRecentlyUsedCacheEvictor,
exoDatabaseProvider
)
renewCacheFactory(client)
}
// This method should be called when proxy setting changes.
@@ -46,9 +43,12 @@ import okhttp3.OkHttpClient
fun get(context: Context, client: OkHttpClient): CacheDataSource.Factory {
if (!this::simpleCache.isInitialized) {
init(context, client)
initFileCache(context)
}
// Renews the factory because OkHttpMight have changed.
renewCacheFactory(client)
return cacheDataSourceFactory
}
}
@@ -7,9 +7,10 @@ import androidx.compose.runtime.Stable
import androidx.core.os.ConfigurationCompat
import androidx.lifecycle.LiveData
import androidx.lifecycle.distinctUntilChanged
import androidx.lifecycle.map
import com.vitorpamplona.amethyst.OptOutFromFilters
import com.vitorpamplona.amethyst.service.CryptoUtils
import com.vitorpamplona.amethyst.service.FileHeader
import com.vitorpamplona.amethyst.service.KeyPair
import com.vitorpamplona.amethyst.service.NostrLnZapPaymentResponseDataSource
import com.vitorpamplona.amethyst.service.model.*
import com.vitorpamplona.amethyst.service.relays.Client
@@ -28,8 +29,6 @@ import kotlinx.coroutines.DelicateCoroutinesApi
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.launch
import nostr.postr.Persona
import nostr.postr.Utils
import java.math.BigDecimal
import java.net.Proxy
import java.util.Locale
@@ -54,7 +53,7 @@ val KIND3_FOLLOWS = " All Follows "
@OptIn(DelicateCoroutinesApi::class)
@Stable
class Account(
val loggedIn: Persona,
val keyPair: KeyPair,
var followingChannels: Set<String> = DefaultChannels, // deprecated
var followingCommunities: Set<String> = setOf(), // deprecated
@@ -100,7 +99,7 @@ class Account(
)
val liveHiddenUsers: LiveData<LiveHiddenUsers> = live.combineWith(getBlockListNote().live().metadata) { localLive, liveMuteListEvent ->
val liveBlockedUsers = (liveMuteListEvent?.note?.event as? PeopleListEvent)?.publicAndPrivateUsers(loggedIn.privKey)
val liveBlockedUsers = (liveMuteListEvent?.note?.event as? PeopleListEvent)?.publicAndPrivateUsers(keyPair.privKey)
LiveHiddenUsers(
hiddenUsers = liveBlockedUsers ?: persistentSetOf(),
spammers = localLive?.account?.transientHiddenUsers ?: persistentSetOf(),
@@ -111,7 +110,7 @@ class Account(
var userProfileCache: User? = null
fun updateAutomaticallyStartPlayback(
automaticallyStartPlayback: Boolean?
automaticallyStartPlayback: ConnectivityType
) {
settings.automaticallyStartPlayback = automaticallyStartPlayback
live.invalidateData()
@@ -119,7 +118,7 @@ class Account(
}
fun updateAutomaticallyShowUrlPreview(
automaticallyShowUrlPreview: Boolean?
automaticallyShowUrlPreview: ConnectivityType
) {
settings.automaticallyShowUrlPreview = automaticallyShowUrlPreview
live.invalidateData()
@@ -127,7 +126,7 @@ class Account(
}
fun updateAutomaticallyShowImages(
automaticallyShowImages: Boolean?
automaticallyShowImages: ConnectivityType
) {
settings.automaticallyShowImages = automaticallyShowImages
live.invalidateData()
@@ -147,14 +146,14 @@ class Account(
fun userProfile(): User {
return userProfileCache ?: run {
val myUser: User = LocalCache.getOrCreateUser(loggedIn.pubKey.toHexKey())
val myUser: User = LocalCache.getOrCreateUser(keyPair.pubKey.toHexKey())
userProfileCache = myUser
myUser
}
}
fun isWriteable(): Boolean {
return loggedIn.privKey != null
return keyPair.privKey != null
}
fun sendNewRelayList(relays: Map<String, ContactListEvent.ReadWrite>) {
@@ -166,7 +165,7 @@ class Account(
val event = ContactListEvent.updateRelayList(
earlierVersion = contactList,
relayUse = relays,
privateKey = loggedIn.privKey!!
privateKey = keyPair.privKey!!
)
Client.send(event)
@@ -175,10 +174,11 @@ class Account(
val event = ContactListEvent.createFromScratch(
followUsers = listOf(),
followTags = listOf(),
followGeohashes = listOf(),
followCommunities = listOf(),
followEvents = DefaultChannels.toList(),
relayUse = relays,
privateKey = loggedIn.privKey!!
privateKey = keyPair.privKey!!
)
// Keep this local to avoid erasing a good contact list.
@@ -190,8 +190,8 @@ class Account(
fun sendNewUserMetadata(toString: String, identities: List<IdentityClaim>) {
if (!isWriteable()) return
loggedIn.privKey?.let {
val event = MetadataEvent.create(toString, identities, loggedIn.privKey!!)
keyPair.privKey?.let {
val event = MetadataEvent.create(toString, identities, keyPair.privKey!!)
Client.send(event)
LocalCache.consume(event)
}
@@ -225,7 +225,7 @@ class Account(
val emojiUrl = EmojiUrl.decode(reaction)
if (emojiUrl != null) {
note.event?.let {
val event = ReactionEvent.create(emojiUrl, it, loggedIn.privKey!!)
val event = ReactionEvent.create(emojiUrl, it, keyPair.privKey!!)
Client.send(event)
LocalCache.consume(event)
}
@@ -235,7 +235,7 @@ class Account(
}
note.event?.let {
val event = ReactionEvent.create(reaction, it, loggedIn.privKey!!)
val event = ReactionEvent.create(reaction, it, keyPair.privKey!!)
Client.send(event)
LocalCache.consume(event)
}
@@ -249,7 +249,7 @@ class Account(
event,
userProfile().latestContactList?.relays()?.keys?.ifEmpty { null }
?: localRelays.map { it.url }.toSet(),
loggedIn.privKey!!,
keyPair.privKey!!,
pollOption,
message,
zapType
@@ -263,18 +263,18 @@ class Account(
}
fun isNIP47Author(pubkeyHex: String?): Boolean {
val privKey = zapPaymentRequest?.secret?.hexToByteArray() ?: loggedIn.privKey
val privKey = zapPaymentRequest?.secret?.hexToByteArray() ?: keyPair.privKey
if (privKey == null) return false
val pubKey = Utils.pubkeyCreate(privKey).toHexKey()
val pubKey = CryptoUtils.pubkeyCreate(privKey).toHexKey()
return (pubKey == pubkeyHex)
}
fun decryptZapPaymentResponseEvent(zapResponseEvent: LnZapPaymentResponseEvent): Response? {
val myNip47 = zapPaymentRequest ?: return null
val privKey = myNip47.secret?.hexToByteArray() ?: loggedIn.privKey
val privKey = myNip47.secret?.hexToByteArray() ?: keyPair.privKey
val pubKey = myNip47.pubKeyHex.hexToByteArray()
if (privKey == null) return null
@@ -287,7 +287,7 @@ class Account(
}
fun calculateZappedAmount(zappedNote: Note?): BigDecimal {
val privKey = zapPaymentRequest?.secret?.hexToByteArray() ?: loggedIn.privKey
val privKey = zapPaymentRequest?.secret?.hexToByteArray() ?: keyPair.privKey
val pubKey = zapPaymentRequest?.pubKeyHex?.hexToByteArray()
return zappedNote?.zappedAmount(privKey, pubKey) ?: BigDecimal.ZERO
}
@@ -296,13 +296,13 @@ class Account(
if (!isWriteable()) return
zapPaymentRequest?.let { nip47 ->
val event = LnZapPaymentRequestEvent.create(bolt11, nip47.pubKeyHex, nip47.secret?.hexToByteArray() ?: loggedIn.privKey!!)
val event = LnZapPaymentRequestEvent.create(bolt11, nip47.pubKeyHex, nip47.secret?.hexToByteArray() ?: keyPair.privKey!!)
val wcListener = NostrLnZapPaymentResponseDataSource(
fromServiceHex = nip47.pubKeyHex,
toUserHex = event.pubKey,
replyingToHex = event.id,
authSigningKey = nip47.secret?.hexToByteArray() ?: loggedIn.privKey!!
authSigningKey = nip47.secret?.hexToByteArray() ?: keyPair.privKey!!
)
wcListener.start()
@@ -330,7 +330,7 @@ class Account(
return LnZapRequestEvent.create(
userPubKeyHex,
userProfile().latestContactList?.relays()?.keys?.ifEmpty { null } ?: localRelays.map { it.url }.toSet(),
loggedIn.privKey!!,
keyPair.privKey!!,
message,
zapType
)
@@ -345,13 +345,13 @@ class Account(
}
note.event?.let {
val event = ReactionEvent.createWarning(it, loggedIn.privKey!!)
val event = ReactionEvent.createWarning(it, keyPair.privKey!!)
Client.send(event)
LocalCache.consume(event)
}
note.event?.let {
val event = ReportEvent.create(it, type, loggedIn.privKey!!, content = content)
val event = ReportEvent.create(it, type, keyPair.privKey!!, content = content)
Client.send(event)
LocalCache.consume(event, null)
}
@@ -365,7 +365,7 @@ class Account(
return
}
val event = ReportEvent.create(user.pubkeyHex, type, loggedIn.privKey!!)
val event = ReportEvent.create(user.pubkeyHex, type, keyPair.privKey!!)
Client.send(event)
LocalCache.consume(event, null)
}
@@ -380,7 +380,7 @@ class Account(
val myNotes = notes.filter { it.author == userProfile() }.map { it.idHex }
if (myNotes.isNotEmpty()) {
val event = DeletionEvent.create(myNotes, loggedIn.privKey!!)
val event = DeletionEvent.create(myNotes, keyPair.privKey!!)
Client.send(event)
LocalCache.consume(event)
}
@@ -389,7 +389,7 @@ class Account(
fun createHTTPAuthorization(url: String, method: String, body: String? = null): HTTPAuthorizationEvent? {
if (!isWriteable()) return null
return HTTPAuthorizationEvent.create(url, method, body, loggedIn.privKey!!)
return HTTPAuthorizationEvent.create(url, method, body, keyPair.privKey!!)
}
fun boost(note: Note) {
@@ -402,11 +402,11 @@ class Account(
note.event?.let {
if (it.kind() == 1) {
val event = RepostEvent.create(it, loggedIn.privKey!!)
val event = RepostEvent.create(it, keyPair.privKey!!)
Client.send(event)
LocalCache.consume(event)
} else {
val event = GenericRepostEvent.create(it, loggedIn.privKey!!)
val event = GenericRepostEvent.create(it, keyPair.privKey!!)
Client.send(event)
LocalCache.consume(event)
}
@@ -427,7 +427,7 @@ class Account(
if (followingCommunities.isNotEmpty()) {
followingCommunities.forEach {
ATag.parse(it, null)?.let {
returningContactList = ContactListEvent.followAddressableEvent(returningContactList, it, loggedIn.privKey!!)
returningContactList = ContactListEvent.followAddressableEvent(returningContactList, it, keyPair.privKey!!)
}
}
followingCommunities = emptySet()
@@ -435,7 +435,7 @@ class Account(
if (followingChannels.isNotEmpty()) {
followingChannels.forEach {
returningContactList = ContactListEvent.followEvent(returningContactList, it, loggedIn.privKey!!)
returningContactList = ContactListEvent.followEvent(returningContactList, it, keyPair.privKey!!)
}
followingChannels = emptySet()
}
@@ -449,15 +449,16 @@ class Account(
val contactList = migrateCommunitiesAndChannelsIfNeeded(userProfile().latestContactList)
val event = if (contactList != null) {
ContactListEvent.followUser(contactList, user.pubkeyHex, loggedIn.privKey!!)
ContactListEvent.followUser(contactList, user.pubkeyHex, keyPair.privKey!!)
} else {
ContactListEvent.createFromScratch(
followUsers = listOf(Contact(user.pubkeyHex, null)),
followTags = emptyList(),
followGeohashes = emptyList(),
followCommunities = emptyList(),
followEvents = DefaultChannels.toList(),
relayUse = Constants.defaultRelays.associate { it.url to ContactListEvent.ReadWrite(it.read, it.write) },
privateKey = loggedIn.privKey!!
privateKey = keyPair.privKey!!
)
}
@@ -471,15 +472,16 @@ class Account(
val contactList = migrateCommunitiesAndChannelsIfNeeded(userProfile().latestContactList)
val event = if (contactList != null) {
ContactListEvent.followEvent(contactList, channel.idHex, loggedIn.privKey!!)
ContactListEvent.followEvent(contactList, channel.idHex, keyPair.privKey!!)
} else {
ContactListEvent.createFromScratch(
followUsers = emptyList(),
followTags = emptyList(),
followGeohashes = emptyList(),
followCommunities = emptyList(),
followEvents = DefaultChannels.toList().plus(channel.idHex),
relayUse = Constants.defaultRelays.associate { it.url to ContactListEvent.ReadWrite(it.read, it.write) },
privateKey = loggedIn.privKey!!
privateKey = keyPair.privKey!!
)
}
@@ -493,16 +495,17 @@ class Account(
val contactList = migrateCommunitiesAndChannelsIfNeeded(userProfile().latestContactList)
val event = if (contactList != null) {
ContactListEvent.followAddressableEvent(contactList, community.address, loggedIn.privKey!!)
ContactListEvent.followAddressableEvent(contactList, community.address, keyPair.privKey!!)
} else {
val relays = Constants.defaultRelays.associate { it.url to ContactListEvent.ReadWrite(it.read, it.write) }
ContactListEvent.createFromScratch(
followUsers = emptyList(),
followTags = emptyList(),
followGeohashes = emptyList(),
followCommunities = listOf(community.address),
followEvents = DefaultChannels.toList(),
relayUse = relays,
privateKey = loggedIn.privKey!!
privateKey = keyPair.privKey!!
)
}
@@ -510,7 +513,7 @@ class Account(
LocalCache.consume(event)
}
fun follow(tag: String) {
fun followHashtag(tag: String) {
if (!isWriteable()) return
val contactList = migrateCommunitiesAndChannelsIfNeeded(userProfile().latestContactList)
@@ -519,16 +522,44 @@ class Account(
ContactListEvent.followHashtag(
contactList,
tag,
loggedIn.privKey!!
keyPair.privKey!!
)
} else {
ContactListEvent.createFromScratch(
followUsers = emptyList(),
followTags = listOf(tag),
followGeohashes = emptyList(),
followCommunities = emptyList(),
followEvents = DefaultChannels.toList(),
relayUse = Constants.defaultRelays.associate { it.url to ContactListEvent.ReadWrite(it.read, it.write) },
privateKey = loggedIn.privKey!!
privateKey = keyPair.privKey!!
)
}
Client.send(event)
LocalCache.consume(event)
}
fun followGeohash(geohash: String) {
if (!isWriteable()) return
val contactList = migrateCommunitiesAndChannelsIfNeeded(userProfile().latestContactList)
val event = if (contactList != null) {
ContactListEvent.followGeohash(
contactList,
geohash,
keyPair.privKey!!
)
} else {
ContactListEvent.createFromScratch(
followUsers = emptyList(),
followTags = emptyList(),
followGeohashes = listOf(geohash),
followCommunities = emptyList(),
followEvents = DefaultChannels.toList(),
relayUse = Constants.defaultRelays.associate { it.url to ContactListEvent.ReadWrite(it.read, it.write) },
privateKey = keyPair.privKey!!
)
}
@@ -545,7 +576,7 @@ class Account(
val event = ContactListEvent.unfollowUser(
contactList,
user.pubkeyHex,
loggedIn.privKey!!
keyPair.privKey!!
)
Client.send(event)
@@ -553,7 +584,7 @@ class Account(
}
}
fun unfollow(tag: String) {
fun unfollowHashtag(tag: String) {
if (!isWriteable()) return
val contactList = migrateCommunitiesAndChannelsIfNeeded(userProfile().latestContactList)
@@ -562,7 +593,24 @@ class Account(
val event = ContactListEvent.unfollowHashtag(
contactList,
tag,
loggedIn.privKey!!
keyPair.privKey!!
)
Client.send(event)
LocalCache.consume(event)
}
}
fun unfollowGeohash(geohash: String) {
if (!isWriteable()) return
val contactList = migrateCommunitiesAndChannelsIfNeeded(userProfile().latestContactList)
if (contactList != null && contactList.tags.isNotEmpty()) {
val event = ContactListEvent.unfollowGeohash(
contactList,
geohash,
keyPair.privKey!!
)
Client.send(event)
@@ -579,7 +627,7 @@ class Account(
val event = ContactListEvent.unfollowEvent(
contactList,
channel.idHex,
loggedIn.privKey!!
keyPair.privKey!!
)
Client.send(event)
@@ -596,7 +644,7 @@ class Account(
val event = ContactListEvent.unfollowAddressableEvent(
contactList,
community.address,
loggedIn.privKey!!
keyPair.privKey!!
)
Client.send(event)
@@ -610,7 +658,7 @@ class Account(
val data = FileStorageEvent.create(
mimeType = headerInfo.mimeType ?: "",
data = byteArray,
privateKey = loggedIn.privKey!!
privateKey = keyPair.privKey!!
)
val signedEvent = FileStorageHeaderEvent.create(
@@ -622,7 +670,7 @@ class Account(
blurhash = headerInfo.blurHash,
description = headerInfo.description,
sensitiveContent = headerInfo.sensitiveContent,
privateKey = loggedIn.privKey!!
privateKey = keyPair.privKey!!
)
return Pair(data, signedEvent)
@@ -652,7 +700,7 @@ class Account(
blurhash = headerInfo.blurHash,
description = headerInfo.description,
sensitiveContent = headerInfo.sensitiveContent,
privateKey = loggedIn.privKey!!
privateKey = keyPair.privKey!!
)
Client.send(signedEvent, relayList = relayList)
@@ -672,7 +720,8 @@ class Account(
replyingTo: String?,
root: String?,
directMentions: Set<HexKey>,
relayList: List<Relay>? = null
relayList: List<Relay>? = null,
geohash: String? = null
) {
if (!isWriteable()) return
@@ -692,7 +741,8 @@ class Account(
replyingTo = replyingTo,
root = root,
directMentions = directMentions,
privateKey = loggedIn.privKey!!
geohash = geohash,
privateKey = keyPair.privKey!!
)
Client.send(signedEvent, relayList = relayList)
@@ -711,7 +761,8 @@ class Account(
zapReceiver: String? = null,
wantsToMarkAsSensitive: Boolean,
zapRaiserAmount: Long? = null,
relayList: List<Relay>? = null
relayList: List<Relay>? = null,
geohash: String? = null
) {
if (!isWriteable()) return
@@ -724,7 +775,7 @@ class Account(
replyTos = repliesToHex,
mentions = mentionsHex,
addresses = addresses,
privateKey = loggedIn.privKey!!,
privateKey = keyPair.privKey!!,
pollOptions = pollOptions,
valueMaximum = valueMaximum,
valueMinimum = valueMinimum,
@@ -732,14 +783,15 @@ class Account(
closedAt = closedAt,
zapReceiver = zapReceiver,
markAsSensitive = wantsToMarkAsSensitive,
zapRaiserAmount = zapRaiserAmount
zapRaiserAmount = zapRaiserAmount,
geohash = geohash
)
// println("Sending new PollNoteEvent: %s".format(signedEvent.toJson()))
Client.send(signedEvent, relayList = relayList)
LocalCache.consume(signedEvent)
}
fun sendChannelMessage(message: String, toChannel: String, replyTo: List<Note>?, mentions: List<User>?, zapReceiver: String? = null, wantsToMarkAsSensitive: Boolean, zapRaiserAmount: Long? = null) {
fun sendChannelMessage(message: String, toChannel: String, replyTo: List<Note>?, mentions: List<User>?, zapReceiver: String? = null, wantsToMarkAsSensitive: Boolean, zapRaiserAmount: Long? = null, geohash: String? = null) {
if (!isWriteable()) return
// val repliesToHex = listOfNotNull(replyingTo?.idHex).ifEmpty { null }
@@ -754,13 +806,14 @@ class Account(
zapReceiver = zapReceiver,
markAsSensitive = wantsToMarkAsSensitive,
zapRaiserAmount = zapRaiserAmount,
privateKey = loggedIn.privKey!!
geohash = geohash,
privateKey = keyPair.privKey!!
)
Client.send(signedEvent)
LocalCache.consume(signedEvent, null)
}
fun sendLiveMessage(message: String, toChannel: ATag, replyTo: List<Note>?, mentions: List<User>?, zapReceiver: String? = null, wantsToMarkAsSensitive: Boolean, zapRaiserAmount: Long? = null) {
fun sendLiveMessage(message: String, toChannel: ATag, replyTo: List<Note>?, mentions: List<User>?, zapReceiver: String? = null, wantsToMarkAsSensitive: Boolean, zapRaiserAmount: Long? = null, geohash: String? = null) {
if (!isWriteable()) return
// val repliesToHex = listOfNotNull(replyingTo?.idHex).ifEmpty { null }
@@ -775,13 +828,14 @@ class Account(
zapReceiver = zapReceiver,
markAsSensitive = wantsToMarkAsSensitive,
zapRaiserAmount = zapRaiserAmount,
privateKey = loggedIn.privKey!!
geohash = geohash,
privateKey = keyPair.privKey!!
)
Client.send(signedEvent)
LocalCache.consume(signedEvent, null)
}
fun sendPrivateMessage(message: String, toUser: User, replyingTo: Note? = null, mentions: List<User>?, zapReceiver: String? = null, wantsToMarkAsSensitive: Boolean, zapRaiserAmount: Long? = null) {
fun sendPrivateMessage(message: String, toUser: User, replyingTo: Note? = null, mentions: List<User>?, zapReceiver: String? = null, wantsToMarkAsSensitive: Boolean, zapRaiserAmount: Long? = null, geohash: String? = null) {
if (!isWriteable()) return
val repliesToHex = listOfNotNull(replyingTo?.idHex).ifEmpty { null }
@@ -796,7 +850,8 @@ class Account(
zapReceiver = zapReceiver,
markAsSensitive = wantsToMarkAsSensitive,
zapRaiserAmount = zapRaiserAmount,
privateKey = loggedIn.privKey!!,
geohash = geohash,
privateKey = keyPair.privKey!!,
advertiseNip18 = false
)
Client.send(signedEvent)
@@ -814,7 +869,7 @@ class Account(
val event = ChannelCreateEvent.create(
channelInfo = metadata,
privateKey = loggedIn.privKey!!
privateKey = keyPair.privKey!!
)
Client.send(event)
@@ -835,7 +890,7 @@ class Account(
val event = EmojiPackSelectionEvent.create(
noteEvent.taggedAddresses().filter { it != emojiListEvent.address() },
loggedIn.privKey!!
keyPair.privKey!!
)
Client.send(event)
@@ -850,7 +905,7 @@ class Account(
val event = if (usersEmojiList.event == null) {
EmojiPackSelectionEvent.create(
listOf(emojiListEvent.address()),
loggedIn.privKey!!
keyPair.privKey!!
)
} else {
val noteEvent = usersEmojiList.event
@@ -862,7 +917,7 @@ class Account(
EmojiPackSelectionEvent.create(
noteEvent.taggedAddresses().plus(emojiListEvent.address()),
loggedIn.privKey!!
keyPair.privKey!!
)
}
@@ -882,11 +937,11 @@ class Account(
bookmarks?.taggedUsers() ?: emptyList(),
bookmarks?.taggedAddresses() ?: emptyList(),
bookmarks?.privateTaggedEvents(privKey = loggedIn.privKey!!) ?: emptyList(),
bookmarks?.privateTaggedUsers(privKey = loggedIn.privKey!!) ?: emptyList(),
bookmarks?.privateTaggedAddresses(privKey = loggedIn.privKey!!)?.plus(note.address) ?: listOf(note.address),
bookmarks?.privateTaggedEvents(privKey = keyPair.privKey!!) ?: emptyList(),
bookmarks?.privateTaggedUsers(privKey = keyPair.privKey!!) ?: emptyList(),
bookmarks?.privateTaggedAddresses(privKey = keyPair.privKey!!)?.plus(note.address) ?: listOf(note.address),
loggedIn.privKey!!
keyPair.privKey!!
)
} else {
BookmarkListEvent.create(
@@ -895,11 +950,11 @@ class Account(
bookmarks?.taggedUsers() ?: emptyList(),
bookmarks?.taggedAddresses() ?: emptyList(),
bookmarks?.privateTaggedEvents(privKey = loggedIn.privKey!!)?.plus(note.idHex) ?: listOf(note.idHex),
bookmarks?.privateTaggedUsers(privKey = loggedIn.privKey!!) ?: emptyList(),
bookmarks?.privateTaggedAddresses(privKey = loggedIn.privKey!!) ?: emptyList(),
bookmarks?.privateTaggedEvents(privKey = keyPair.privKey!!)?.plus(note.idHex) ?: listOf(note.idHex),
bookmarks?.privateTaggedUsers(privKey = keyPair.privKey!!) ?: emptyList(),
bookmarks?.privateTaggedAddresses(privKey = keyPair.privKey!!) ?: emptyList(),
loggedIn.privKey!!
keyPair.privKey!!
)
}
@@ -919,11 +974,11 @@ class Account(
bookmarks?.taggedUsers() ?: emptyList(),
bookmarks?.taggedAddresses()?.plus(note.address) ?: listOf(note.address),
bookmarks?.privateTaggedEvents(privKey = loggedIn.privKey!!) ?: emptyList(),
bookmarks?.privateTaggedUsers(privKey = loggedIn.privKey!!) ?: emptyList(),
bookmarks?.privateTaggedAddresses(privKey = loggedIn.privKey!!) ?: emptyList(),
bookmarks?.privateTaggedEvents(privKey = keyPair.privKey!!) ?: emptyList(),
bookmarks?.privateTaggedUsers(privKey = keyPair.privKey!!) ?: emptyList(),
bookmarks?.privateTaggedAddresses(privKey = keyPair.privKey!!) ?: emptyList(),
loggedIn.privKey!!
keyPair.privKey!!
)
} else {
BookmarkListEvent.create(
@@ -932,11 +987,11 @@ class Account(
bookmarks?.taggedUsers() ?: emptyList(),
bookmarks?.taggedAddresses() ?: emptyList(),
bookmarks?.privateTaggedEvents(privKey = loggedIn.privKey!!) ?: emptyList(),
bookmarks?.privateTaggedUsers(privKey = loggedIn.privKey!!) ?: emptyList(),
bookmarks?.privateTaggedAddresses(privKey = loggedIn.privKey!!) ?: emptyList(),
bookmarks?.privateTaggedEvents(privKey = keyPair.privKey!!) ?: emptyList(),
bookmarks?.privateTaggedUsers(privKey = keyPair.privKey!!) ?: emptyList(),
bookmarks?.privateTaggedAddresses(privKey = keyPair.privKey!!) ?: emptyList(),
loggedIn.privKey!!
keyPair.privKey!!
)
}
@@ -956,11 +1011,11 @@ class Account(
bookmarks?.taggedUsers() ?: emptyList(),
bookmarks?.taggedAddresses() ?: emptyList(),
bookmarks?.privateTaggedEvents(privKey = loggedIn.privKey!!) ?: emptyList(),
bookmarks?.privateTaggedUsers(privKey = loggedIn.privKey!!) ?: emptyList(),
bookmarks?.privateTaggedAddresses(privKey = loggedIn.privKey!!)?.minus(note.address) ?: listOf(),
bookmarks?.privateTaggedEvents(privKey = keyPair.privKey!!) ?: emptyList(),
bookmarks?.privateTaggedUsers(privKey = keyPair.privKey!!) ?: emptyList(),
bookmarks?.privateTaggedAddresses(privKey = keyPair.privKey!!)?.minus(note.address) ?: listOf(),
loggedIn.privKey!!
keyPair.privKey!!
)
} else {
BookmarkListEvent.create(
@@ -969,11 +1024,11 @@ class Account(
bookmarks?.taggedUsers() ?: emptyList(),
bookmarks?.taggedAddresses() ?: emptyList(),
bookmarks?.privateTaggedEvents(privKey = loggedIn.privKey!!)?.minus(note.idHex) ?: listOf(),
bookmarks?.privateTaggedUsers(privKey = loggedIn.privKey!!) ?: emptyList(),
bookmarks?.privateTaggedAddresses(privKey = loggedIn.privKey!!) ?: emptyList(),
bookmarks?.privateTaggedEvents(privKey = keyPair.privKey!!)?.minus(note.idHex) ?: listOf(),
bookmarks?.privateTaggedUsers(privKey = keyPair.privKey!!) ?: emptyList(),
bookmarks?.privateTaggedAddresses(privKey = keyPair.privKey!!) ?: emptyList(),
loggedIn.privKey!!
keyPair.privKey!!
)
}
@@ -984,7 +1039,7 @@ class Account(
fun createAuthEvent(relay: Relay, challenge: String): RelayAuthEvent? {
if (!isWriteable()) return null
return RelayAuthEvent.create(relay.url, challenge, loggedIn.privKey!!)
return RelayAuthEvent.create(relay.url, challenge, keyPair.privKey!!)
}
fun removePublicBookmark(note: Note) {
@@ -999,11 +1054,11 @@ class Account(
bookmarks?.taggedUsers() ?: emptyList(),
bookmarks?.taggedAddresses()?.minus(note.address),
bookmarks?.privateTaggedEvents(privKey = loggedIn.privKey!!) ?: emptyList(),
bookmarks?.privateTaggedUsers(privKey = loggedIn.privKey!!) ?: emptyList(),
bookmarks?.privateTaggedAddresses(privKey = loggedIn.privKey!!) ?: emptyList(),
bookmarks?.privateTaggedEvents(privKey = keyPair.privKey!!) ?: emptyList(),
bookmarks?.privateTaggedUsers(privKey = keyPair.privKey!!) ?: emptyList(),
bookmarks?.privateTaggedAddresses(privKey = keyPair.privKey!!) ?: emptyList(),
loggedIn.privKey!!
keyPair.privKey!!
)
} else {
BookmarkListEvent.create(
@@ -1012,11 +1067,11 @@ class Account(
bookmarks?.taggedUsers() ?: emptyList(),
bookmarks?.taggedAddresses() ?: emptyList(),
bookmarks?.privateTaggedEvents(privKey = loggedIn.privKey!!) ?: emptyList(),
bookmarks?.privateTaggedUsers(privKey = loggedIn.privKey!!) ?: emptyList(),
bookmarks?.privateTaggedAddresses(privKey = loggedIn.privKey!!) ?: emptyList(),
bookmarks?.privateTaggedEvents(privKey = keyPair.privKey!!) ?: emptyList(),
bookmarks?.privateTaggedUsers(privKey = keyPair.privKey!!) ?: emptyList(),
bookmarks?.privateTaggedAddresses(privKey = keyPair.privKey!!) ?: emptyList(),
loggedIn.privKey!!
keyPair.privKey!!
)
}
@@ -1028,10 +1083,10 @@ class Account(
if (!isWriteable()) return false
if (note is AddressableNote) {
return userProfile().latestBookmarkList?.privateTaggedAddresses(loggedIn.privKey!!)
return userProfile().latestBookmarkList?.privateTaggedAddresses(keyPair.privKey!!)
?.contains(note.address) == true
} else {
return userProfile().latestBookmarkList?.privateTaggedEvents(loggedIn.privKey!!)
return userProfile().latestBookmarkList?.privateTaggedEvents(keyPair.privKey!!)
?.contains(note.idHex) == true
}
}
@@ -1061,7 +1116,7 @@ class Account(
var returningList: PeopleListEvent = latestList
if (hiddenUsers.isNotEmpty()) {
returningList = PeopleListEvent.addUsers(returningList, hiddenUsers.toList(), true, loggedIn.privKey!!)
returningList = PeopleListEvent.addUsers(returningList, hiddenUsers.toList(), true, keyPair.privKey!!)
hiddenUsers = emptySet()
}
@@ -1076,14 +1131,14 @@ class Account(
earlierVersion = blockList,
pubKeyHex = pubkeyHex,
isPrivate = true,
privateKey = loggedIn.privKey!!
privateKey = keyPair.privKey!!
)
} else {
PeopleListEvent.createListWithUser(
name = PeopleListEvent.blockList,
pubKeyHex = pubkeyHex,
isPrivate = true,
privateKey = loggedIn.privKey!!
privateKey = keyPair.privKey!!
)
}
@@ -1102,7 +1157,7 @@ class Account(
earlierVersion = blockList,
pubKeyHex = pubkeyHex,
isPrivate = true,
privateKey = loggedIn.privKey!!
privateKey = keyPair.privKey!!
)
Client.send(event)
@@ -1172,7 +1227,7 @@ class Account(
if (listName == GLOBAL_FOLLOWS) return null
if (listName == KIND3_FOLLOWS) return userProfile().cachedFollowingKeySet()
val privKey = loggedIn.privKey
val privKey = keyPair.privKey
return if (listName != null) {
val aTag = ATag(PeopleListEvent.kind, userProfile().pubkeyHex, listName, null).toTag()
@@ -1196,7 +1251,7 @@ class Account(
if (listName == GLOBAL_FOLLOWS) return null
if (listName == KIND3_FOLLOWS) return userProfile().cachedFollowingTagSet()
val privKey = loggedIn.privKey
val privKey = keyPair.privKey
return if (listName != null) {
val aTag = ATag(PeopleListEvent.kind, userProfile().pubkeyHex, listName, null).toTag()
@@ -1216,11 +1271,35 @@ class Account(
}
}
fun selectedGeohashesFollowList(listName: String?): Set<String>? {
if (listName == GLOBAL_FOLLOWS) return null
if (listName == KIND3_FOLLOWS) return userProfile().cachedFollowingGeohashSet()
val privKey = keyPair.privKey
return if (listName != null) {
val aTag = ATag(PeopleListEvent.kind, userProfile().pubkeyHex, listName, null).toTag()
val list = LocalCache.addressables[aTag]
if (list != null) {
val publicAddresses = list.event?.geohashes() ?: emptySet()
val privateAddresses = privKey?.let {
(list.event as? GeneralListEvent)?.privateGeohashes(it)
} ?: emptySet()
(publicAddresses + privateAddresses).toSet()
} else {
emptySet()
}
} else {
emptySet()
}
}
fun selectedCommunitiesFollowList(listName: String?): Set<String>? {
if (listName == GLOBAL_FOLLOWS) return null
if (listName == KIND3_FOLLOWS) return userProfile().cachedFollowingCommunitiesSet()
val privKey = loggedIn.privKey
val privKey = keyPair.privKey
return if (listName != null) {
val aTag = ATag(PeopleListEvent.kind, userProfile().pubkeyHex, listName, null).toTag()
@@ -1257,7 +1336,7 @@ class Account(
val event = ChannelMetadataEvent.create(
newChannelInfo = metadata,
originalChannelIdHex = channel.idHex,
privateKey = loggedIn.privKey!!
privateKey = keyPair.privKey!!
)
Client.send(event)
@@ -1268,9 +1347,9 @@ class Account(
fun decryptContent(note: Note): String? {
val event = note.event
return if (event is PrivateDmEvent && loggedIn.privKey != null) {
event.plainContent(loggedIn.privKey!!, event.talkingWith(userProfile().pubkeyHex).hexToByteArray())
} else if (event is LnZapRequestEvent && loggedIn.privKey != null) {
return if (event is PrivateDmEvent && keyPair.privKey != null) {
event.plainContent(keyPair.privKey!!, event.talkingWith(userProfile().pubkeyHex).hexToByteArray())
} else if (event is LnZapRequestEvent && keyPair.privKey != null) {
decryptZapContentAuthor(note)?.content()
} else {
event?.content()
@@ -1279,7 +1358,7 @@ class Account(
fun decryptZapContentAuthor(note: Note): Event? {
val event = note.event
val loggedInPrivateKey = loggedIn.privKey
val loggedInPrivateKey = keyPair.privKey
return if (event is LnZapRequestEvent && loggedInPrivateKey != null && event.isPrivateZap()) {
val recipientPK = event.zappedAuthor().firstOrNull()
@@ -1312,7 +1391,7 @@ class Account(
try {
if (altPrivateKeyToUse != null && altPubkeyToUse != null) {
val altPubKeyFromPrivate = Utils.pubkeyCreate(altPrivateKeyToUse).toHexKey()
val altPubKeyFromPrivate = CryptoUtils.pubkeyCreate(altPrivateKeyToUse).toHexKey()
if (altPubKeyFromPrivate == event.pubKey) {
val result = event.getPrivateZapEvent(altPrivateKeyToUse, altPubkeyToUse)
@@ -1414,7 +1493,7 @@ class Account(
fun isHidden(userHex: String): Boolean {
val blockList = getBlockList()
return (blockList?.publicAndPrivateUsers(loggedIn.privKey)?.contains(userHex) ?: false) || userHex in transientHiddenUsers
return (blockList?.publicAndPrivateUsers(keyPair.privKey)?.contains(userHex) ?: false) || userHex in transientHiddenUsers
}
fun followingKeySet(): Set<HexKey> {
@@ -1549,7 +1628,9 @@ class Account(
println("Loading saved contacts ${it.toJson()}")
if (userProfile().latestContactList == null) {
LocalCache.consume(it)
GlobalScope.launch(Dispatchers.IO) {
LocalCache.consume(it)
}
}
}
}
@@ -7,6 +7,7 @@ import com.vitorpamplona.amethyst.service.checkNotInMainThread
import com.vitorpamplona.amethyst.service.model.ATag
import com.vitorpamplona.amethyst.service.model.ChannelCreateEvent
import com.vitorpamplona.amethyst.service.model.LiveActivitiesEvent
import com.vitorpamplona.amethyst.service.toNote
import com.vitorpamplona.amethyst.ui.components.BundledUpdate
import com.vitorpamplona.amethyst.ui.note.toShortenHex
import fr.acinq.secp256k1.Hex
@@ -1,119 +0,0 @@
package com.vitorpamplona.amethyst.model
import com.vitorpamplona.amethyst.service.nip19.Nip19
import com.vitorpamplona.amethyst.ui.note.toShortenHex
import fr.acinq.secp256k1.Hex
import nostr.postr.Bech32
import nostr.postr.Persona
import nostr.postr.bechToBytes
import nostr.postr.toHex
import nostr.postr.toNpub
/** Makes the distinction between String and Hex **/
typealias HexKey = String
typealias NPubKey = String
typealias NoteId = String
fun NPubKey.toDisplayKey(): String {
return this.toShortenHex()
}
fun NoteId.toDisplayId(): String {
return this.toShortenHex()
}
fun ByteArray.toNote() = Bech32.encodeBytes(hrp = "note", this, Bech32.Encoding.Bech32)
fun ByteArray.toHexKey(): HexKey {
return toHex()
}
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")) {
Persona(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")) {
Persona(privKey = key.bechToBytes()).pubKey.toHexKey()
} else if (pubKeyParsed != null) {
pubKeyParsed
} else {
Hex.decode(key).toHexKey()
}
} catch (e: Exception) {
null
}
}
data class DirtyKeyInfo(val key: Nip19.Return, val restOfWord: String)
fun parseDirtyWordForKey(mightBeAKey: String): DirtyKeyInfo? {
var key = mightBeAKey
if (key.startsWith("nostr:", true)) {
key = key.substring("nostr:".length)
}
key = key.removePrefix("@")
if (key.length < 63) {
return null
}
try {
val keyB32 = key.substring(0, 63)
val restOfWord = key.substring(63)
if (key.startsWith("nsec1", true)) {
// Converts to npub
val pubkey = Nip19.uriToRoute(Persona(privKey = keyB32.bechToBytes()).pubKey.toNpub()) ?: return null
return DirtyKeyInfo(pubkey, restOfWord)
} else if (key.startsWith("npub1", true)) {
val pubkey = Nip19.uriToRoute(keyB32) ?: return null
return DirtyKeyInfo(pubkey, restOfWord)
} else if (key.startsWith("note1", true)) {
val noteId = Nip19.uriToRoute(keyB32) ?: return null
return DirtyKeyInfo(noteId, restOfWord)
} else if (key.startsWith("nprofile", true)) {
val pubkeyRelay = Nip19.uriToRoute(keyB32 + restOfWord) ?: return null
return DirtyKeyInfo(pubkeyRelay, pubkeyRelay.additionalChars)
} else if (key.startsWith("nevent1", true)) {
val noteRelayId = Nip19.uriToRoute(keyB32 + restOfWord) ?: return null
return DirtyKeyInfo(noteRelayId, noteRelayId.additionalChars)
} else if (key.startsWith("naddr1", true)) {
val address = Nip19.uriToRoute(keyB32 + restOfWord) ?: return null
return DirtyKeyInfo(address, address.additionalChars) // no way to know when they address ends and dirt begins
}
} catch (e: Exception) {
e.printStackTrace()
}
return null
}
@@ -0,0 +1,52 @@
package com.vitorpamplona.amethyst.model
import com.vitorpamplona.amethyst.service.KeyPair
import com.vitorpamplona.amethyst.service.bechToBytes
import com.vitorpamplona.amethyst.service.nip19.Nip19
import com.vitorpamplona.amethyst.ui.note.toShortenHex
import fr.acinq.secp256k1.Hex
/** Makes the distinction between String and Hex **/
typealias HexKey = String
fun ByteArray.toHexKey(): HexKey {
return Hex.encode(this)
}
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
}
}
@@ -11,6 +11,7 @@ import com.vitorpamplona.amethyst.service.model.*
import com.vitorpamplona.amethyst.service.nip19.Nip19
import com.vitorpamplona.amethyst.service.relays.EOSETime
import com.vitorpamplona.amethyst.service.relays.Relay
import com.vitorpamplona.amethyst.service.toNote
import com.vitorpamplona.amethyst.ui.actions.ImmutableListOfLists
import com.vitorpamplona.amethyst.ui.actions.updated
import com.vitorpamplona.amethyst.ui.components.BundledUpdate
@@ -1,11 +1,40 @@
package com.vitorpamplona.amethyst.model
import androidx.compose.runtime.Stable
import com.vitorpamplona.amethyst.R
@Stable
class Settings(
var automaticallyShowImages: Boolean? = null,
var automaticallyStartPlayback: Boolean? = null,
var preferredLanguage: String? = null,
var automaticallyShowUrlPreview: Boolean? = null
var automaticallyShowImages: ConnectivityType = ConnectivityType.ALWAYS,
var automaticallyStartPlayback: ConnectivityType = ConnectivityType.ALWAYS,
var automaticallyShowUrlPreview: ConnectivityType = ConnectivityType.ALWAYS
)
enum class ConnectivityType(val prefCode: Boolean?, val screenCode: Int, val reourceId: Int) {
ALWAYS(null, 0, R.string.connectivity_type_always),
WIFI_ONLY(true, 1, R.string.connectivity_type_wifi_only),
NEVER(false, 2, R.string.connectivity_type_never)
}
fun parseConnectivityType(code: Boolean?): ConnectivityType {
return when (code) {
ConnectivityType.ALWAYS.prefCode -> ConnectivityType.ALWAYS
ConnectivityType.WIFI_ONLY.prefCode -> ConnectivityType.WIFI_ONLY
ConnectivityType.NEVER.prefCode -> ConnectivityType.NEVER
else -> {
ConnectivityType.ALWAYS
}
}
}
fun parseConnectivityType(screenCode: Int): ConnectivityType {
return when (screenCode) {
ConnectivityType.ALWAYS.screenCode -> ConnectivityType.ALWAYS
ConnectivityType.WIFI_ONLY.screenCode -> ConnectivityType.WIFI_ONLY
ConnectivityType.NEVER.screenCode -> ConnectivityType.NEVER
else -> {
ConnectivityType.ALWAYS
}
}
}
@@ -3,6 +3,7 @@ package com.vitorpamplona.amethyst.model
import androidx.compose.runtime.Immutable
import androidx.compose.runtime.Stable
import androidx.lifecycle.LiveData
import com.vitorpamplona.amethyst.service.Bech32
import com.vitorpamplona.amethyst.service.NostrSingleUserDataSource
import com.vitorpamplona.amethyst.service.checkNotInMainThread
import com.vitorpamplona.amethyst.service.model.BookmarkListEvent
@@ -12,14 +13,13 @@ import com.vitorpamplona.amethyst.service.model.MetadataEvent
import com.vitorpamplona.amethyst.service.model.ReportEvent
import com.vitorpamplona.amethyst.service.relays.EOSETime
import com.vitorpamplona.amethyst.service.relays.Relay
import com.vitorpamplona.amethyst.service.toNpub
import com.vitorpamplona.amethyst.ui.actions.ImmutableListOfLists
import com.vitorpamplona.amethyst.ui.actions.toImmutableListOfLists
import com.vitorpamplona.amethyst.ui.components.BundledUpdate
import com.vitorpamplona.amethyst.ui.note.toShortenHex
import fr.acinq.secp256k1.Hex
import kotlinx.coroutines.Dispatchers
import nostr.postr.Bech32
import nostr.postr.toNpub
import java.math.BigDecimal
import java.util.regex.Pattern
@@ -249,6 +249,12 @@ class User(val pubkeyHex: String) {
} ?: false
}
fun isFollowingGeohashCached(geoTag: String): Boolean {
return latestContactList?.verifiedFollowGeohashSet?.let {
return geoTag.lowercase() in it
} ?: false
}
fun isFollowingCached(user: User): Boolean {
return latestContactList?.verifiedFollowKeySet?.let {
return user.pubkeyHex in it
@@ -277,6 +283,10 @@ class User(val pubkeyHex: String) {
return latestContactList?.verifiedFollowTagSet ?: emptySet()
}
fun cachedFollowingGeohashSet(): Set<HexKey> {
return latestContactList?.verifiedFollowGeohashSet ?: emptySet()
}
fun cachedFollowingCommunitiesSet(): Set<HexKey> {
return latestContactList?.verifiedFollowCommunitySet ?: emptySet()
}
@@ -0,0 +1,217 @@
package com.vitorpamplona.amethyst.service
/*
* Copyright 2020 ACINQ SAS
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import kotlin.jvm.JvmStatic
/**
* Bech32 works with 5 bits values, we use this type to make it explicit: whenever you see Int5 it means 5 bits values,
* and whenever you see Byte it means 8 bits values.
*/
private typealias Int5 = Byte
/**
* Bech32 and Bech32m address formats.
* See https://github.com/bitcoin/bips/blob/master/bip-0173.mediawiki and https://github.com/bitcoin/bips/blob/master/bip-0350.mediawiki.
*/
object Bech32 {
const val alphabet: String = "qpzry9x8gf2tvdw0s3jn54khce6mua7l"
enum class Encoding(public val constant: Int) {
Bech32(1),
Bech32m(0x2bc830a3),
Beck32WithoutChecksum(0)
}
// char -> 5 bits value
private val map = Array<Int5>(255) { -1 }
init {
for (i in 0..alphabet.lastIndex) {
map[alphabet[i].code] = i.toByte()
}
}
private fun expand(hrp: String): Array<Int5> {
val result = Array<Int5>(hrp.length + 1 + hrp.length) { 0 }
for (i in hrp.indices) {
result[i] = hrp[i].code.shr(5).toByte()
result[hrp.length + 1 + i] = (hrp[i].code and 31).toByte()
}
result[hrp.length] = 0
return result
}
private fun polymod(values: Array<Int5>, values1: Array<Int5>): Int {
val GEN = arrayOf(0x3b6a57b2, 0x26508e6d, 0x1ea119fa, 0x3d4233dd, 0x2a1462b3)
var chk = 1
values.forEach { v ->
val b = chk shr 25
chk = ((chk and 0x1ffffff) shl 5) xor v.toInt()
for (i in 0..5) {
if (((b shr i) and 1) != 0) chk = chk xor GEN[i]
}
}
values1.forEach { v ->
val b = chk shr 25
chk = ((chk and 0x1ffffff) shl 5) xor v.toInt()
for (i in 0..5) {
if (((b shr i) and 1) != 0) chk = chk xor GEN[i]
}
}
return chk
}
/**
* @param hrp human readable prefix
* @param int5s 5-bit data
* @param encoding encoding to use (bech32 or bech32m)
* @return hrp + data encoded as a Bech32 string
*/
@JvmStatic
public fun encode(hrp: String, int5s: Array<Int5>, encoding: Encoding): String {
require(hrp.lowercase() == hrp || hrp.uppercase() == hrp) { "mixed case strings are not valid bech32 prefixes" }
val data = int5s.toByteArray().toTypedArray()
val checksum = when (encoding) {
Encoding.Beck32WithoutChecksum -> arrayOf()
else -> checksum(hrp, data, encoding)
}
return hrp + "1" + (data + checksum).map { i -> alphabet[i.toInt()] }.toCharArray().concatToString()
}
/**
* @param hrp human readable prefix
* @param data data to encode
* @param encoding encoding to use (bech32 or bech32m)
* @return hrp + data encoded as a Bech32 string
*/
@JvmStatic
public fun encodeBytes(hrp: String, data: ByteArray, encoding: Encoding): String = encode(hrp, eight2five(data), encoding)
/**
* decodes a bech32 string
* @param bech32 bech32 string
* @param noChecksum if true, the bech32 string doesn't have a checksum
* @return a (hrp, data, encoding) tuple
*/
@JvmStatic
public fun decode(bech32: String, noChecksum: Boolean = false): Triple<String, Array<Int5>, Encoding> {
require(bech32.lowercase() == bech32 || bech32.uppercase() == bech32) { "mixed case strings are not valid bech32" }
bech32.forEach { require(it.code in 33..126) { "invalid character " } }
val input = bech32.lowercase()
val pos = input.lastIndexOf('1')
val hrp = input.take(pos)
require(hrp.length in 1..83) { "hrp must contain 1 to 83 characters" }
val data = Array<Int5>(input.length - pos - 1) { 0 }
for (i in 0..data.lastIndex) data[i] = map[input[pos + 1 + i].code]
return if (noChecksum) {
Triple(hrp, data, Encoding.Beck32WithoutChecksum)
} else {
val encoding = when (polymod(expand(hrp), data)) {
Encoding.Bech32.constant -> Encoding.Bech32
Encoding.Bech32m.constant -> Encoding.Bech32m
else -> throw IllegalArgumentException("invalid checksum for $bech32")
}
Triple(hrp, data.dropLast(6).toTypedArray(), encoding)
}
}
/**
* decodes a bech32 string
* @param bech32 bech32 string
* @param noChecksum if true, the bech32 string doesn't have a checksum
* @return a (hrp, data, encoding) tuple
*/
@JvmStatic
public fun decodeBytes(bech32: String, noChecksum: Boolean = false): Triple<String, ByteArray, Encoding> {
val (hrp, int5s, encoding) = decode(bech32, noChecksum)
return Triple(hrp, five2eight(int5s, 0), encoding)
}
/**
* @param hrp Human Readable Part
* @param data data (a sequence of 5 bits integers)
* @param encoding encoding to use (bech32 or bech32m)
* @return a checksum computed over hrp and data
*/
private fun checksum(hrp: String, data: Array<Int5>, encoding: Encoding): Array<Int5> {
val values = expand(hrp) + data
val poly = polymod(values, arrayOf(0.toByte(), 0.toByte(), 0.toByte(), 0.toByte(), 0.toByte(), 0.toByte())) xor encoding.constant
return Array(6) { i -> (poly.shr(5 * (5 - i)) and 31).toByte() }
}
/**
* @param input a sequence of 8 bits integers
* @return a sequence of 5 bits integers
*/
@JvmStatic
public fun eight2five(input: ByteArray): Array<Int5> {
var buffer = 0L
val output = ArrayList<Int5>()
var count = 0
input.forEach { b ->
buffer = (buffer shl 8) or (b.toLong() and 0xff)
count += 8
while (count >= 5) {
output.add(((buffer shr (count - 5)) and 31).toByte())
count -= 5
}
}
if (count > 0) output.add(((buffer shl (5 - count)) and 31).toByte())
return output.toTypedArray()
}
/**
* @param input a sequence of 5 bits integers
* @return a sequence of 8 bits integers
*/
@JvmStatic
public fun five2eight(input: Array<Int5>, offset: Int): ByteArray {
var buffer = 0L
val output = ArrayList<Byte>()
var count = 0
for (i in offset..input.lastIndex) {
val b = input[i]
buffer = (buffer shl 5) or (b.toLong() and 31)
count += 5
while (count >= 8) {
output.add(((buffer shr (count - 8)) and 0xff).toByte())
count -= 8
}
}
require(count <= 4) { "Zero-padding of more than 4 bits" }
require((buffer and ((1L shl count) - 1L)) == 0L) { "Non-zero padding in 8-to-5 conversion" }
return output.toByteArray()
}
}
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)
hrp?.also {
if (it != decodedForm.first) {
throw IllegalArgumentException("Expected $it but obtained ${decodedForm.first}")
}
}
return decodedForm.second
}
@@ -80,16 +80,17 @@ class CashuProcessor {
.post(requestBody)
.build()
val response = client.newCall(request).execute()
val body = response.body.string()
val tree = jacksonObjectMapper().readTree(body)
client.newCall(request).execute().use {
val body = it.body.string()
val tree = jacksonObjectMapper().readTree(body)
val successful = tree?.get("paid")?.asText() == "true"
val successful = tree?.get("paid")?.asText() == "true"
if (successful) {
onSuccess("Redeemed ${token.totalAmount} Sats" + " (Fees: ${token.fees} Sats)")
} else {
onError(tree?.get("detail")?.asText()?.split('.')?.getOrNull(0) ?: "Cashu: Tokens already spent.")
if (successful) {
onSuccess("Redeemed ${token.totalAmount} Sats" + " (Fees: ${token.fees} Sats)")
} else {
onError(tree?.get("detail")?.asText()?.split('.')?.getOrNull(0) ?: "Cashu: Tokens already spent.")
}
}
} catch (e: Exception) {
onError("Token melt failure: " + e.message)
@@ -0,0 +1,87 @@
package com.vitorpamplona.amethyst.service
import fr.acinq.secp256k1.Hex
import fr.acinq.secp256k1.Secp256k1
import java.security.MessageDigest
import java.security.SecureRandom
import java.util.Base64
import javax.crypto.Cipher
import javax.crypto.spec.IvParameterSpec
import javax.crypto.spec.SecretKeySpec
object CryptoUtils {
private val secp256k1 = Secp256k1.get()
private val random = SecureRandom()
/**
* Provides a 32B "private key" aka random number
*/
fun privkeyCreate(): ByteArray {
val bytes = ByteArray(32)
random.nextBytes(bytes)
return bytes
}
fun pubkeyCreate(privKey: ByteArray) =
secp256k1.pubKeyCompress(secp256k1.pubkeyCreate(privKey)).copyOfRange(1, 33)
fun sign(data: ByteArray, privKey: ByteArray): ByteArray =
secp256k1.signSchnorr(data, privKey, null)
fun encrypt(msg: String, privateKey: ByteArray, pubKey: ByteArray): String {
val sharedSecret = getSharedSecret(privateKey, pubKey)
return encrypt(msg, sharedSecret)
}
fun encrypt(msg: String, sharedSecret: ByteArray): String {
val iv = ByteArray(16)
random.nextBytes(iv)
val cipher = Cipher.getInstance("AES/CBC/PKCS5Padding")
cipher.init(Cipher.ENCRYPT_MODE, SecretKeySpec(sharedSecret, "AES"), IvParameterSpec(iv))
val ivBase64 = Base64.getEncoder().encodeToString(iv)
val encryptedMsg = cipher.doFinal(msg.toByteArray())
val encryptedMsgBase64 = Base64.getEncoder().encodeToString(encryptedMsg)
return "$encryptedMsgBase64?iv=$ivBase64"
}
fun decrypt(msg: String, privateKey: ByteArray, pubKey: ByteArray): String {
val sharedSecret = getSharedSecret(privateKey, pubKey)
return decrypt(msg, sharedSecret)
}
fun decrypt(msg: String, sharedSecret: ByteArray): String {
val parts = msg.split("?iv=")
val iv = parts[1].run { Base64.getDecoder().decode(this) }
val encryptedMsg = parts.first().run { Base64.getDecoder().decode(this) }
val cipher = Cipher.getInstance("AES/CBC/PKCS5Padding")
cipher.init(Cipher.DECRYPT_MODE, SecretKeySpec(sharedSecret, "AES"), IvParameterSpec(iv))
return String(cipher.doFinal(encryptedMsg))
}
fun verifySignature(
signature: ByteArray,
hash: ByteArray,
pubKey: ByteArray
): Boolean {
return secp256k1.verifySchnorr(signature, hash, pubKey)
}
fun sha256(data: ByteArray): ByteArray {
// Creates a new buffer every time
return MessageDigest.getInstance("SHA-256").digest(data)
}
/**
* @return 32B shared secret
*/
fun getSharedSecret(privateKey: ByteArray, pubKey: ByteArray): ByteArray =
secp256k1.pubKeyTweakMul(Hex.decode("02") + pubKey, privateKey).copyOfRange(1, 33)
}
fun Int.toByteArray(): ByteArray {
val bytes = ByteArray(4)
(0..3).forEach {
bytes[3 - it] = ((this ushr (8 * it)) and 0xFFFF).toByte()
}
return bytes
}
@@ -6,7 +6,6 @@ import android.util.Log
import com.vitorpamplona.amethyst.model.toHexKey
import com.vitorpamplona.amethyst.ui.actions.ImageDownloader
import io.trbl.blurhash.BlurHash
import java.security.MessageDigest
import kotlin.math.roundToInt
class FileHeader(
@@ -45,9 +44,7 @@ class FileHeader(
onError: () -> Unit
) {
try {
val sha256 = MessageDigest.getInstance("SHA-256")
val hash = sha256.digest(data).toHexKey()
val hash = CryptoUtils.sha256(data).toHexKey()
val size = data.size
val (blurHash, dim) = if (mimeType?.startsWith("image/") == true) {
@@ -0,0 +1,34 @@
package com.vitorpamplona.amethyst.service
import com.vitorpamplona.amethyst.model.toHexKey
class KeyPair(
privKey: ByteArray? = null,
pubKey: ByteArray? = null
) {
val privKey: ByteArray?
val pubKey: ByteArray
init {
if (privKey == null) {
if (pubKey == null) {
// create new, random keys
this.privKey = CryptoUtils.privkeyCreate()
this.pubKey = CryptoUtils.pubkeyCreate(this.privKey)
} else {
// this is a read-only account
check(pubKey.size == 32)
this.privKey = null
this.pubKey = pubKey
}
} else {
// as private key is provided, ignore the public key and set keys according to private key
this.privKey = privKey
this.pubKey = CryptoUtils.pubkeyCreate(privKey)
}
}
override fun toString(): String {
return "KeyPair(privateKey=${privKey?.toHexKey()}, publicKey=${pubKey.toHexKey()}"
}
}
@@ -0,0 +1,83 @@
package com.vitorpamplona.amethyst.service
import android.annotation.SuppressLint
import android.content.Context
import android.location.Geocoder
import android.location.Location
import android.location.LocationListener
import android.location.LocationManager
import android.os.Bundle
import android.os.HandlerThread
import androidx.compose.runtime.MutableState
import androidx.compose.runtime.mutableStateOf
import kotlinx.coroutines.flow.MutableStateFlow
class LocationUtil(context: Context) {
companion object {
const val MIN_TIME: Long = 1000L
const val MIN_DISTANCE: Float = 0.0f
}
private val locationManager = context.getSystemService(Context.LOCATION_SERVICE) as LocationManager
private var locationListener: LocationListener? = null
val locationStateFlow = MutableStateFlow<Location>(Location(LocationManager.NETWORK_PROVIDER))
val providerState = mutableStateOf(false)
val isStart: MutableState<Boolean> = mutableStateOf(false)
private val locHandlerThread = HandlerThread("LocationUtil Thread")
init {
locHandlerThread.start()
}
@SuppressLint("MissingPermission")
fun start(minTimeMs: Long = MIN_TIME, minDistanceM: Float = MIN_DISTANCE) {
locationListener().let {
locationListener = it
locationManager.requestLocationUpdates(LocationManager.NETWORK_PROVIDER, minTimeMs, minDistanceM, it, locHandlerThread.looper)
}
providerState.value = locationManager.isProviderEnabled(LocationManager.NETWORK_PROVIDER)
isStart.value = true
}
fun stop() {
locationListener?.let {
locationManager.removeUpdates(it)
}
isStart.value = false
}
private fun locationListener() = object : LocationListener {
override fun onLocationChanged(location: Location) {
locationStateFlow.value = location
}
override fun onStatusChanged(provider: String?, status: Int, extras: Bundle?) {
}
override fun onProviderEnabled(provider: String) {
providerState.value = true
}
override fun onProviderDisabled(provider: String) {
providerState.value = false
}
}
}
class ReverseGeoLocationUtil {
fun execute(
location: Location,
context: Context
): String? {
return try {
Geocoder(context).getFromLocation(location.latitude, location.longitude, 1)?.firstOrNull()?.let { address ->
listOfNotNull(address.locality ?: address.subAdminArea, address.countryCode).joinToString(", ")
}
} catch (e: Exception) {
e.printStackTrace()
return null
}
}
}
@@ -92,6 +92,26 @@ object NostrDiscoveryDataSource : NostrDataSource("DiscoveryFeed") {
)
}
fun createLiveStreamGeohashesFilter(): TypedFilter? {
val hashToLoad = account.selectedGeohashesFollowList(account.defaultDiscoveryFollowList)
if (hashToLoad.isNullOrEmpty()) return null
return TypedFilter(
types = setOf(FeedType.GLOBAL),
filter = JsonFilter(
kinds = listOf(LiveActivitiesChatMessageEvent.kind, LiveActivitiesEvent.kind),
tags = mapOf(
"g" to hashToLoad.map {
listOf(it, it.lowercase(), it.uppercase(), it.capitalize())
}.flatten()
),
limit = 300,
since = latestEOSEs.users[account.userProfile()]?.followList?.get(account.defaultDiscoveryFollowList)?.relayList
)
)
}
fun createPublicChatsTagsFilter(): TypedFilter? {
val hashToLoad = account.selectedTagsFollowList(account.defaultDiscoveryFollowList)
@@ -112,6 +132,26 @@ object NostrDiscoveryDataSource : NostrDataSource("DiscoveryFeed") {
)
}
fun createPublicChatsGeohashesFilter(): TypedFilter? {
val hashToLoad = account.selectedGeohashesFollowList(account.defaultDiscoveryFollowList)
if (hashToLoad.isNullOrEmpty()) return null
return TypedFilter(
types = setOf(FeedType.PUBLIC_CHATS),
filter = JsonFilter(
kinds = listOf(ChannelCreateEvent.kind, ChannelMetadataEvent.kind, ChannelMessageEvent.kind),
tags = mapOf(
"g" to hashToLoad.map {
listOf(it, it.lowercase(), it.uppercase(), it.capitalize())
}.flatten()
),
limit = 300,
since = latestEOSEs.users[account.userProfile()]?.followList?.get(account.defaultDiscoveryFollowList)?.relayList
)
)
}
fun createCommunitiesTagsFilter(): TypedFilter? {
val hashToLoad = account.selectedTagsFollowList(account.defaultDiscoveryFollowList)
@@ -132,6 +172,26 @@ object NostrDiscoveryDataSource : NostrDataSource("DiscoveryFeed") {
)
}
fun createCommunitiesGeohashesFilter(): TypedFilter? {
val hashToLoad = account.selectedGeohashesFollowList(account.defaultDiscoveryFollowList)
if (hashToLoad.isNullOrEmpty()) return null
return TypedFilter(
types = setOf(FeedType.GLOBAL),
filter = JsonFilter(
kinds = listOf(CommunityDefinitionEvent.kind, CommunityPostApprovalEvent.kind),
tags = mapOf(
"g" to hashToLoad.map {
listOf(it, it.lowercase(), it.uppercase(), it.capitalize())
}.flatten()
),
limit = 300,
since = latestEOSEs.users[account.userProfile()]?.followList?.get(account.defaultDiscoveryFollowList)?.relayList
)
)
}
val discoveryFeedChannel = requestNewChannel() { time, relayUrl ->
latestEOSEs.addOrUpdate(account.userProfile(), account.defaultDiscoveryFollowList, relayUrl, time)
}
@@ -143,7 +203,10 @@ object NostrDiscoveryDataSource : NostrDataSource("DiscoveryFeed") {
createCommunitiesFilter(),
createLiveStreamTagsFilter(),
createPublicChatsTagsFilter(),
createCommunitiesTagsFilter()
createCommunitiesTagsFilter(),
createCommunitiesGeohashesFilter(),
createPublicChatsGeohashesFilter(),
createLiveStreamGeohashesFilter()
).ifEmpty { null }
}
}
@@ -0,0 +1,42 @@
package com.vitorpamplona.amethyst.service
import com.vitorpamplona.amethyst.service.model.ChannelMessageEvent
import com.vitorpamplona.amethyst.service.model.LongTextNoteEvent
import com.vitorpamplona.amethyst.service.model.PollNoteEvent
import com.vitorpamplona.amethyst.service.model.TextNoteEvent
import com.vitorpamplona.amethyst.service.relays.COMMON_FEED_TYPES
import com.vitorpamplona.amethyst.service.relays.JsonFilter
import com.vitorpamplona.amethyst.service.relays.TypedFilter
object NostrGeohashDataSource : NostrDataSource("SingleGeoHashFeed") {
private var geohashToWatch: String? = null
fun createLoadHashtagFilter(): TypedFilter? {
val hashToLoad = geohashToWatch ?: return null
return TypedFilter(
types = COMMON_FEED_TYPES,
filter = JsonFilter(
tags = mapOf(
"g" to listOf(
hashToLoad
)
),
kinds = listOf(TextNoteEvent.kind, ChannelMessageEvent.kind, LongTextNoteEvent.kind, LongTextNoteEvent.kind, PollNoteEvent.kind),
limit = 200
)
)
}
val loadGeohashChannel = requestNewChannel()
override fun updateChannelFilters() {
loadGeohashChannel.typedFilters = listOfNotNull(createLoadHashtagFilter()).ifEmpty { null }
}
fun loadHashtag(tag: String?) {
geohashToWatch = tag
invalidateFilters()
}
}
@@ -104,6 +104,26 @@ object NostrHomeDataSource : NostrDataSource("HomeFeed") {
)
}
fun createFollowGeohashesFilter(): TypedFilter? {
val hashToLoad = account.selectedGeohashesFollowList(account.defaultHomeFollowList) ?: emptySet()
if (hashToLoad.isEmpty()) return null
return TypedFilter(
types = setOf(FeedType.FOLLOWS),
filter = JsonFilter(
kinds = listOf(TextNoteEvent.kind, LongTextNoteEvent.kind, ClassifiedsEvent.kind, HighlightEvent.kind, AudioTrackEvent.kind, PinListEvent.kind),
tags = mapOf(
"g" to hashToLoad.map {
listOf(it, it.lowercase(), it.uppercase(), it.capitalize())
}.flatten()
),
limit = 100,
since = latestEOSEs.users[account.userProfile()]?.followList?.get(account.defaultHomeFollowList)?.relayList
)
)
}
fun createFollowCommunitiesFilter(): TypedFilter? {
val communitiesToLoad = account.selectedCommunitiesFollowList(account.defaultHomeFollowList) ?: emptySet()
@@ -135,6 +155,11 @@ object NostrHomeDataSource : NostrDataSource("HomeFeed") {
}
override fun updateChannelFilters() {
followAccountChannel.typedFilters = listOfNotNull(createFollowAccountsFilter(), createFollowCommunitiesFilter(), createFollowTagsFilter()).ifEmpty { null }
followAccountChannel.typedFilters = listOfNotNull(
createFollowAccountsFilter(),
createFollowCommunitiesFilter(),
createFollowTagsFilter(),
createFollowGeohashesFilter()
).ifEmpty { null }
}
}
@@ -51,11 +51,35 @@ object NostrVideoDataSource : NostrDataSource("VideoFeed") {
)
}
fun createFollowGeohashesFilter(): TypedFilter? {
val hashToLoad = account.selectedGeohashesFollowList(account.defaultStoriesFollowList)
if (hashToLoad.isNullOrEmpty()) return null
return TypedFilter(
types = setOf(FeedType.GLOBAL),
filter = JsonFilter(
kinds = listOf(FileHeaderEvent.kind, FileStorageHeaderEvent.kind),
tags = mapOf(
"g" to hashToLoad.map {
listOf(it, it.lowercase(), it.uppercase(), it.capitalize())
}.flatten()
),
limit = 100,
since = latestEOSEs.users[account.userProfile()]?.followList?.get(account.defaultStoriesFollowList)?.relayList
)
)
}
val videoFeedChannel = requestNewChannel() { time, relayUrl ->
latestEOSEs.addOrUpdate(account.userProfile(), account.defaultStoriesFollowList, relayUrl, time)
}
override fun updateChannelFilters() {
videoFeedChannel.typedFilters = listOfNotNull(createContextualFilter(), createFollowTagsFilter()).ifEmpty { null }
videoFeedChannel.typedFilters = listOfNotNull(
createContextualFilter(),
createFollowTagsFilter(),
createFollowGeohashesFilter()
).ifEmpty { null }
}
}
@@ -28,7 +28,9 @@ object OnlineChecker {
.get()
.build()
val result = HttpClient.getHttpClient().newCall(request).execute().code == 200
val result = HttpClient.getHttpClient().newCall(request).execute().use {
it.isSuccessful
}
checkOnlineCache.put(url, OnlineCheckResult(System.currentTimeMillis(), result))
result
} catch (e: Exception) {
@@ -7,16 +7,9 @@ object ConnectivityStatus {
private val onMobileData = mutableStateOf(false)
val isOnMobileData: MutableState<Boolean> = onMobileData
private val onWifi = mutableStateOf(false)
val isOnWifi: MutableState<Boolean> = onWifi
fun updateConnectivityStatus(isOnMobileData: Boolean, isOnWifi: Boolean) {
if (onMobileData.value != isOnMobileData) {
onMobileData.value = isOnMobileData
}
if (onWifi.value != isOnWifi) {
onWifi.value = isOnWifi
}
}
}
@@ -2,14 +2,15 @@ package com.vitorpamplona.amethyst.service.lnurl
import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper
import com.vitorpamplona.amethyst.BuildConfig
import com.vitorpamplona.amethyst.service.Bech32
import com.vitorpamplona.amethyst.service.HttpClient
import com.vitorpamplona.amethyst.service.checkNotInMainThread
import com.vitorpamplona.amethyst.service.toLnUrl
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import nostr.postr.Bech32
import okhttp3.Call
import okhttp3.Callback
import okhttp3.Request
@@ -131,7 +132,7 @@ class LightningAddressResolver() {
fetchLightningAddressJson(
lnaddress,
onSuccess = {
onSuccess(Bech32.encodeBytes("lnurl", it.toByteArray(), Bech32.Encoding.Bech32))
onSuccess(it.toByteArray().toLnUrl())
},
onError = onError
)
@@ -4,11 +4,11 @@ import android.util.Log
import androidx.compose.runtime.Immutable
import com.vitorpamplona.amethyst.model.hexToByteArray
import com.vitorpamplona.amethyst.model.toHexKey
import com.vitorpamplona.amethyst.service.bechToBytes
import com.vitorpamplona.amethyst.service.nip19.Tlv
import com.vitorpamplona.amethyst.service.toByteArray
import com.vitorpamplona.amethyst.service.toNAddress
import fr.acinq.secp256k1.Hex
import nostr.postr.Bech32
import nostr.postr.bechToBytes
import nostr.postr.toByteArray
@Immutable
data class ATag(val kind: Int, val pubKeyHex: String, val dTag: String, val relay: String?) {
@@ -30,7 +30,7 @@ data class ATag(val kind: Int, val pubKeyHex: String, val dTag: String, val rela
byteArrayOf(Tlv.Type.AUTHOR.id, author.size.toByte()) + author +
byteArrayOf(Tlv.Type.KIND.id, kind.size.toByte()) + kind
return Bech32.encodeBytes(hrp = "naddr", fullArray, Bech32.Encoding.Bech32)
return fullArray.toNAddress()
}
companion object {
@@ -4,7 +4,7 @@ import androidx.compose.runtime.Immutable
import com.vitorpamplona.amethyst.model.HexKey
import com.vitorpamplona.amethyst.model.TimeUtils
import com.vitorpamplona.amethyst.model.toHexKey
import nostr.postr.Utils
import com.vitorpamplona.amethyst.service.CryptoUtils
@Immutable
class AudioTrackEvent(
@@ -52,9 +52,9 @@ class AudioTrackEvent(
subject?.let { listOf(SUBJECT, it) }
)
val pubKey = Utils.pubkeyCreate(privateKey).toHexKey()
val pubKey = CryptoUtils.pubkeyCreate(privateKey).toHexKey()
val id = generateId(pubKey, createdAt, kind, tags, "")
val sig = Utils.sign(id, privateKey)
val sig = CryptoUtils.sign(id, privateKey)
return AudioTrackEvent(id.toHexKey(), pubKey, createdAt, tags, "", sig.toHexKey())
}
}
@@ -4,7 +4,7 @@ import androidx.compose.runtime.Immutable
import com.vitorpamplona.amethyst.model.HexKey
import com.vitorpamplona.amethyst.model.TimeUtils
import com.vitorpamplona.amethyst.model.toHexKey
import nostr.postr.Utils
import com.vitorpamplona.amethyst.service.CryptoUtils
@Immutable
class BookmarkListEvent(
@@ -32,7 +32,7 @@ class BookmarkListEvent(
privateKey: ByteArray,
createdAt: Long = TimeUtils.now()
): BookmarkListEvent {
val pubKey = Utils.pubkeyCreate(privateKey)
val pubKey = CryptoUtils.pubkeyCreate(privateKey)
val content = createPrivateTags(privEvents, privUsers, privAddresses, privateKey, pubKey)
val tags = mutableListOf<List<String>>()
@@ -49,7 +49,7 @@ class BookmarkListEvent(
}
val id = generateId(pubKey.toHexKey(), createdAt, kind, tags, content)
val sig = Utils.sign(id, privateKey)
val sig = CryptoUtils.sign(id, privateKey)
return BookmarkListEvent(id.toHexKey(), pubKey.toHexKey(), createdAt, tags, content, sig.toHexKey())
}
}
@@ -5,7 +5,7 @@ import androidx.compose.runtime.Immutable
import com.vitorpamplona.amethyst.model.HexKey
import com.vitorpamplona.amethyst.model.TimeUtils
import com.vitorpamplona.amethyst.model.toHexKey
import nostr.postr.Utils
import com.vitorpamplona.amethyst.service.CryptoUtils
@Immutable
class ChannelCreateEvent(
@@ -38,10 +38,10 @@ class ChannelCreateEvent(
""
}
val pubKey = Utils.pubkeyCreate(privateKey).toHexKey()
val pubKey = CryptoUtils.pubkeyCreate(privateKey).toHexKey()
val tags = emptyList<List<String>>()
val id = generateId(pubKey, createdAt, kind, tags, content)
val sig = Utils.sign(id, privateKey)
val sig = CryptoUtils.sign(id, privateKey)
return ChannelCreateEvent(id.toHexKey(), pubKey, createdAt, tags, content, sig.toHexKey())
}
}
@@ -4,7 +4,7 @@ import androidx.compose.runtime.Immutable
import com.vitorpamplona.amethyst.model.HexKey
import com.vitorpamplona.amethyst.model.TimeUtils
import com.vitorpamplona.amethyst.model.toHexKey
import nostr.postr.Utils
import com.vitorpamplona.amethyst.service.CryptoUtils
@Immutable
class ChannelHideMessageEvent(
@@ -27,14 +27,14 @@ class ChannelHideMessageEvent(
const val kind = 43
fun create(reason: String, messagesToHide: List<String>?, privateKey: ByteArray, createdAt: Long = TimeUtils.now()): ChannelHideMessageEvent {
val pubKey = Utils.pubkeyCreate(privateKey).toHexKey()
val pubKey = CryptoUtils.pubkeyCreate(privateKey).toHexKey()
val tags =
messagesToHide?.map {
listOf("e", it)
} ?: emptyList()
val id = generateId(pubKey, createdAt, kind, tags, reason)
val sig = Utils.sign(id, privateKey)
val sig = CryptoUtils.sign(id, privateKey)
return ChannelHideMessageEvent(id.toHexKey(), pubKey, createdAt, tags, reason, sig.toHexKey())
}
}
@@ -4,7 +4,7 @@ import androidx.compose.runtime.Immutable
import com.vitorpamplona.amethyst.model.HexKey
import com.vitorpamplona.amethyst.model.TimeUtils
import com.vitorpamplona.amethyst.model.toHexKey
import nostr.postr.Utils
import com.vitorpamplona.amethyst.service.CryptoUtils
@Immutable
class ChannelMessageEvent(
@@ -36,10 +36,11 @@ class ChannelMessageEvent(
privateKey: ByteArray,
createdAt: Long = TimeUtils.now(),
markAsSensitive: Boolean,
zapRaiserAmount: Long?
zapRaiserAmount: Long?,
geohash: String? = null
): ChannelMessageEvent {
val content = message
val pubKey = Utils.pubkeyCreate(privateKey).toHexKey()
val pubKey = CryptoUtils.pubkeyCreate(privateKey).toHexKey()
val tags = mutableListOf(
listOf("e", channel, "", "root")
)
@@ -58,9 +59,12 @@ class ChannelMessageEvent(
zapRaiserAmount?.let {
tags.add(listOf("zapraiser", "$it"))
}
geohash?.let {
tags.add(listOf("g", it))
}
val id = generateId(pubKey, createdAt, kind, tags, content)
val sig = Utils.sign(id, privateKey)
val sig = CryptoUtils.sign(id, privateKey)
return ChannelMessageEvent(id.toHexKey(), pubKey, createdAt, tags, content, sig.toHexKey())
}
}
@@ -5,7 +5,7 @@ import androidx.compose.runtime.Immutable
import com.vitorpamplona.amethyst.model.HexKey
import com.vitorpamplona.amethyst.model.TimeUtils
import com.vitorpamplona.amethyst.model.toHexKey
import nostr.postr.Utils
import com.vitorpamplona.amethyst.service.CryptoUtils
@Immutable
class ChannelMetadataEvent(
@@ -37,10 +37,10 @@ class ChannelMetadataEvent(
""
}
val pubKey = Utils.pubkeyCreate(privateKey).toHexKey()
val pubKey = CryptoUtils.pubkeyCreate(privateKey).toHexKey()
val tags = listOf(listOf("e", originalChannelIdHex, "", "root"))
val id = generateId(pubKey, createdAt, kind, tags, content)
val sig = Utils.sign(id, privateKey)
val sig = CryptoUtils.sign(id, privateKey)
return ChannelMetadataEvent(id.toHexKey(), pubKey, createdAt, tags, content, sig.toHexKey())
}
}
@@ -4,7 +4,7 @@ import androidx.compose.runtime.Immutable
import com.vitorpamplona.amethyst.model.HexKey
import com.vitorpamplona.amethyst.model.TimeUtils
import com.vitorpamplona.amethyst.model.toHexKey
import nostr.postr.Utils
import com.vitorpamplona.amethyst.service.CryptoUtils
@Immutable
class ChannelMuteUserEvent(
@@ -28,14 +28,14 @@ class ChannelMuteUserEvent(
fun create(reason: String, usersToMute: List<String>?, privateKey: ByteArray, createdAt: Long = TimeUtils.now()): ChannelMuteUserEvent {
val content = reason
val pubKey = Utils.pubkeyCreate(privateKey).toHexKey()
val pubKey = CryptoUtils.pubkeyCreate(privateKey).toHexKey()
val tags =
usersToMute?.map {
listOf("p", it)
} ?: emptyList()
val id = generateId(pubKey, createdAt, kind, tags, content)
val sig = Utils.sign(id, privateKey)
val sig = CryptoUtils.sign(id, privateKey)
return ChannelMuteUserEvent(id.toHexKey(), pubKey, createdAt, tags, content, sig.toHexKey())
}
}
@@ -4,7 +4,7 @@ import androidx.compose.runtime.Immutable
import com.vitorpamplona.amethyst.model.HexKey
import com.vitorpamplona.amethyst.model.TimeUtils
import com.vitorpamplona.amethyst.model.toHexKey
import nostr.postr.Utils
import com.vitorpamplona.amethyst.service.CryptoUtils
@Immutable
class ClassifiedsEvent(
@@ -66,9 +66,9 @@ class ClassifiedsEvent(
publishedAt?.let { tags.add(listOf("publishedAt", it.toString())) }
title?.let { tags.add(listOf("title", it)) }
val pubKey = Utils.pubkeyCreate(privateKey).toHexKey()
val pubKey = CryptoUtils.pubkeyCreate(privateKey).toHexKey()
val id = generateId(pubKey, createdAt, kind, tags, "")
val sig = Utils.sign(id, privateKey)
val sig = CryptoUtils.sign(id, privateKey)
return ClassifiedsEvent(id.toHexKey(), pubKey, createdAt, tags, "", sig.toHexKey())
}
}
@@ -4,7 +4,7 @@ import androidx.compose.runtime.Immutable
import com.vitorpamplona.amethyst.model.HexKey
import com.vitorpamplona.amethyst.model.TimeUtils
import com.vitorpamplona.amethyst.model.toHexKey
import nostr.postr.Utils
import com.vitorpamplona.amethyst.service.CryptoUtils
@Immutable
class CommunityDefinitionEvent(
@@ -33,9 +33,9 @@ class CommunityDefinitionEvent(
createdAt: Long = TimeUtils.now()
): CommunityDefinitionEvent {
val tags = mutableListOf<List<String>>()
val pubKey = Utils.pubkeyCreate(privateKey).toHexKey()
val pubKey = CryptoUtils.pubkeyCreate(privateKey).toHexKey()
val id = generateId(pubKey, createdAt, kind, tags, "")
val sig = Utils.sign(id, privateKey)
val sig = CryptoUtils.sign(id, privateKey)
return CommunityDefinitionEvent(id.toHexKey(), pubKey, createdAt, tags, "", sig.toHexKey())
}
}
@@ -5,8 +5,8 @@ import androidx.compose.runtime.Immutable
import com.vitorpamplona.amethyst.model.HexKey
import com.vitorpamplona.amethyst.model.TimeUtils
import com.vitorpamplona.amethyst.model.toHexKey
import com.vitorpamplona.amethyst.service.CryptoUtils
import com.vitorpamplona.amethyst.service.relays.Client
import nostr.postr.Utils
@Immutable
class CommunityPostApprovalEvent(
@@ -53,10 +53,10 @@ class CommunityPostApprovalEvent(
val replyToAuthor = listOf("p", approvedPost.pubKey())
val kind = listOf("k", "${approvedPost.kind()}")
val pubKey = Utils.pubkeyCreate(privateKey).toHexKey()
val pubKey = CryptoUtils.pubkeyCreate(privateKey).toHexKey()
val tags: List<List<String>> = listOf(communities, replyToPost, replyToAuthor, kind)
val id = generateId(pubKey, createdAt, GenericRepostEvent.kind, tags, content)
val sig = Utils.sign(id, privateKey)
val sig = CryptoUtils.sign(id, privateKey)
return GenericRepostEvent(id.toHexKey(), pubKey, createdAt, tags, content, sig.toHexKey())
}
}
@@ -8,7 +8,7 @@ import com.vitorpamplona.amethyst.model.HexKey
import com.vitorpamplona.amethyst.model.TimeUtils
import com.vitorpamplona.amethyst.model.decodePublicKey
import com.vitorpamplona.amethyst.model.toHexKey
import nostr.postr.Utils
import com.vitorpamplona.amethyst.service.CryptoUtils
@Immutable
data class Contact(val pubKeyHex: String, val relayUri: String?)
@@ -39,6 +39,10 @@ class ContactListEvent(
unverifiedFollowTagSet().map { it.lowercase() }.toSet()
}
val verifiedFollowGeohashSet: Set<String> by lazy {
unverifiedFollowGeohashSet().map { it.lowercase() }.toSet()
}
val verifiedFollowCommunitySet: Set<String> by lazy {
unverifiedFollowAddressSet().toSet()
}
@@ -49,6 +53,7 @@ class ContactListEvent(
fun unverifiedFollowKeySet() = tags.filter { it[0] == "p" }.mapNotNull { it.getOrNull(1) }
fun unverifiedFollowTagSet() = tags.filter { it[0] == "t" }.mapNotNull { it.getOrNull(1) }
fun unverifiedFollowGeohashSet() = tags.filter { it[0] == "g" }.mapNotNull { it.getOrNull(1) }
fun unverifiedFollowAddressSet() = tags.filter { it[0] == "a" }.mapNotNull { it.getOrNull(1) }
@@ -82,6 +87,7 @@ class ContactListEvent(
fun createFromScratch(
followUsers: List<Contact>,
followTags: List<String>,
followGeohashes: List<String>,
followCommunities: List<ATag>,
followEvents: List<String>,
relayUse: Map<String, ReadWrite>?,
@@ -110,6 +116,8 @@ class ContactListEvent(
} else {
listOf("a", it.toTag())
}
} + followGeohashes.map {
listOf("g", it)
}
return create(
@@ -164,6 +172,28 @@ class ContactListEvent(
)
}
fun followGeohash(earlierVersion: ContactListEvent, hashtag: String, privateKey: ByteArray, createdAt: Long = TimeUtils.now()): ContactListEvent {
if (earlierVersion.isTaggedGeoHash(hashtag)) return earlierVersion
return create(
content = earlierVersion.content,
tags = earlierVersion.tags.plus(element = listOf("g", hashtag)),
privateKey = privateKey,
createdAt = createdAt
)
}
fun unfollowGeohash(earlierVersion: ContactListEvent, hashtag: String, privateKey: ByteArray, createdAt: Long = TimeUtils.now()): ContactListEvent {
if (!earlierVersion.isTaggedGeoHash(hashtag)) return earlierVersion
return create(
content = earlierVersion.content,
tags = earlierVersion.tags.filter { it.size > 1 && it[1] != hashtag },
privateKey = privateKey,
createdAt = createdAt
)
}
fun followEvent(earlierVersion: ContactListEvent, idHex: String, privateKey: ByteArray, createdAt: Long = TimeUtils.now()): ContactListEvent {
if (earlierVersion.isTaggedEvent(idHex)) return earlierVersion
@@ -224,9 +254,9 @@ class ContactListEvent(
}
fun create(content: String, tags: List<List<String>>, privateKey: ByteArray, createdAt: Long = TimeUtils.now()): ContactListEvent {
val pubKey = Utils.pubkeyCreate(privateKey).toHexKey()
val pubKey = CryptoUtils.pubkeyCreate(privateKey).toHexKey()
val id = generateId(pubKey, createdAt, kind, tags, content)
val sig = Utils.sign(id, privateKey)
val sig = CryptoUtils.sign(id, privateKey)
return ContactListEvent(id.toHexKey(), pubKey, createdAt, tags, content, sig.toHexKey())
}
}
@@ -4,7 +4,7 @@ import androidx.compose.runtime.Immutable
import com.vitorpamplona.amethyst.model.HexKey
import com.vitorpamplona.amethyst.model.TimeUtils
import com.vitorpamplona.amethyst.model.toHexKey
import nostr.postr.Utils
import com.vitorpamplona.amethyst.service.CryptoUtils
@Immutable
class DeletionEvent(
@@ -22,10 +22,10 @@ class DeletionEvent(
fun create(deleteEvents: List<String>, privateKey: ByteArray, createdAt: Long = TimeUtils.now()): DeletionEvent {
val content = ""
val pubKey = Utils.pubkeyCreate(privateKey).toHexKey()
val pubKey = CryptoUtils.pubkeyCreate(privateKey).toHexKey()
val tags = deleteEvents.map { listOf("e", it) }
val id = generateId(pubKey, createdAt, kind, tags, content)
val sig = Utils.sign(id, privateKey)
val sig = CryptoUtils.sign(id, privateKey)
return DeletionEvent(id.toHexKey(), pubKey, createdAt, tags, content, sig.toHexKey())
}
}
@@ -4,7 +4,7 @@ import androidx.compose.runtime.Immutable
import com.vitorpamplona.amethyst.model.HexKey
import com.vitorpamplona.amethyst.model.TimeUtils
import com.vitorpamplona.amethyst.model.toHexKey
import nostr.postr.Utils
import com.vitorpamplona.amethyst.service.CryptoUtils
@Immutable
class EmojiPackEvent(
@@ -25,13 +25,13 @@ class EmojiPackEvent(
createdAt: Long = TimeUtils.now()
): EmojiPackEvent {
val content = ""
val pubKey = Utils.pubkeyCreate(privateKey)
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 = Utils.sign(id, privateKey)
val sig = CryptoUtils.sign(id, privateKey)
return EmojiPackEvent(id.toHexKey(), pubKey.toHexKey(), createdAt, tags, content, sig.toHexKey())
}
}
@@ -4,7 +4,7 @@ import androidx.compose.runtime.Immutable
import com.vitorpamplona.amethyst.model.HexKey
import com.vitorpamplona.amethyst.model.TimeUtils
import com.vitorpamplona.amethyst.model.toHexKey
import nostr.postr.Utils
import com.vitorpamplona.amethyst.service.CryptoUtils
@Immutable
class EmojiPackSelectionEvent(
@@ -28,7 +28,7 @@ class EmojiPackSelectionEvent(
createdAt: Long = TimeUtils.now()
): EmojiPackSelectionEvent {
val msg = ""
val pubKey = Utils.pubkeyCreate(privateKey).toHexKey()
val pubKey = CryptoUtils.pubkeyCreate(privateKey).toHexKey()
val tags = mutableListOf<List<String>>()
listOfEmojiPacks?.forEach {
@@ -36,7 +36,7 @@ class EmojiPackSelectionEvent(
}
val id = generateId(pubKey, createdAt, kind, tags, msg)
val sig = Utils.sign(id, privateKey)
val sig = CryptoUtils.sign(id, privateKey)
return EmojiPackSelectionEvent(id.toHexKey(), pubKey, createdAt, tags, msg, sig.toHexKey())
}
}
@@ -7,14 +7,11 @@ import com.google.gson.annotations.SerializedName
import com.vitorpamplona.amethyst.model.HexKey
import com.vitorpamplona.amethyst.model.TimeUtils
import com.vitorpamplona.amethyst.model.toHexKey
import com.vitorpamplona.amethyst.service.CryptoUtils
import com.vitorpamplona.amethyst.service.nip19.Nip19
import fr.acinq.secp256k1.Hex
import fr.acinq.secp256k1.Secp256k1
import nostr.postr.Utils
import nostr.postr.toHex
import java.lang.reflect.Type
import java.math.BigDecimal
import java.security.MessageDigest
import java.util.*
@Immutable
@@ -72,6 +69,7 @@ open class Event(
}
override fun hashtags() = tags.filter { it.size > 1 && it[0] == "t" }.map { it[1] }
override fun geohashes() = tags.filter { it.size > 1 && it[0] == "g" }.map { it[1] }
override fun matchTag1With(text: String) = tags.any { it.size > 1 && it[1].contains(text, true) }
@@ -84,7 +82,10 @@ open class Event(
override fun isTaggedAddressableNotes(idHexes: Set<String>) = tags.any { it.size > 1 && it[0] == "a" && it[1] in idHexes }
override fun isTaggedHash(hashtag: String) = tags.any { it.size > 1 && it[0] == "t" && it[1].equals(hashtag, true) }
override fun isTaggedGeoHash(hashtag: String) = tags.any { it.size > 1 && it[0] == "g" && it[1].startsWith(hashtag, true) }
override fun isTaggedHashes(hashtags: Set<String>) = tags.any { it.size > 1 && it[0] == "t" && it[1].lowercase() in hashtags }
override fun isTaggedGeoHashes(hashtags: Set<String>) = tags.any { it.size > 1 && it[0] == "g" && it[1].lowercase() in hashtags }
override fun firstIsTaggedHashes(hashtags: Set<String>) = tags.firstOrNull { it.size > 1 && it[0] == "t" && it[1].lowercase() in hashtags }?.getOrNull(1)
override fun firstIsTaggedAddressableNote(addressableNotes: Set<String>) = tags.firstOrNull { it.size > 1 && it[0] == "a" && it[1] in addressableNotes }?.getOrNull(1)
@@ -125,6 +126,10 @@ open class Event(
return rank
}
override fun getGeoHash(): String? {
return tags.firstOrNull { it.size > 1 && it[0] == "g" }?.get(1)?.ifBlank { null }
}
override fun getReward(): BigDecimal? {
return try {
tags.firstOrNull { it.size > 1 && it[0] == "reward" }?.get(1)?.let { BigDecimal(it) }
@@ -158,14 +163,14 @@ open class Event(
""".trimIndent()
)
}
if (!secp256k1.verifySchnorr(Hex.decode(sig), Hex.decode(id), Hex.decode(pubKey))) {
if (!CryptoUtils.verifySignature(Hex.decode(sig), Hex.decode(id), Hex.decode(pubKey))) {
throw Exception("""Bad signature!""")
}
}
override fun hasValidSignature(): Boolean {
return try {
id.contentEquals(generateId()) && secp256k1.verifySchnorr(Hex.decode(sig), Hex.decode(id), Hex.decode(pubKey))
id.contentEquals(generateId()) && CryptoUtils.verifySignature(Hex.decode(sig), Hex.decode(id), Hex.decode(pubKey))
} catch (e: Exception) {
Log.e("Event", "Fail checking if event $id has a valid signature", e)
false
@@ -183,7 +188,7 @@ open class Event(
.replace("\\u2028", "\u2028")
.replace("\\u2029", "\u2029")
return MessageDigest.getInstance("SHA-256").digest(rawEventJson.toByteArray()).toHexKey()
return CryptoUtils.sha256(rawEventJson.toByteArray()).toHexKey()
}
private class EventDeserializer : JsonDeserializer<Event> {
@@ -251,12 +256,10 @@ open class Event(
src: ByteArray,
typeOfSrc: Type?,
context: JsonSerializationContext?
) = JsonPrimitive(src.toHex())
) = JsonPrimitive(src.toHexKey())
}
companion object {
private val secp256k1 = Secp256k1.get()
val gson: Gson = GsonBuilder()
.disableHtmlEscaping()
.registerTypeAdapter(Event::class.java, EventSerializer())
@@ -337,13 +340,13 @@ open class Event(
.replace("\\u2028", "\u2028")
.replace("\\u2029", "\u2029")
return MessageDigest.getInstance("SHA-256").digest(rawEventJson.toByteArray())
return CryptoUtils.sha256(rawEventJson.toByteArray())
}
fun create(privateKey: ByteArray, kind: Int, tags: List<List<String>> = emptyList(), content: String = "", createdAt: Long = TimeUtils.now()): Event {
val pubKey = Utils.pubkeyCreate(privateKey).toHexKey()
val pubKey = CryptoUtils.pubkeyCreate(privateKey).toHexKey()
val id = Companion.generateId(pubKey, createdAt, kind, tags, content)
val sig = Utils.sign(id, privateKey).toHexKey()
val sig = CryptoUtils.sign(id, privateKey).toHexKey()
return Event(id.toHexKey(), pubKey, createdAt, kind, tags, content, sig)
}
}
@@ -34,7 +34,11 @@ interface EventInterface {
fun isTaggedAddressableNotes(idHexes: Set<String>): Boolean
fun isTaggedHash(hashtag: String): Boolean
fun isTaggedGeoHash(hashtag: String): Boolean
fun isTaggedHashes(hashtags: Set<String>): Boolean
fun isTaggedGeoHashes(hashtags: Set<String>): Boolean
fun firstIsTaggedHashes(hashtags: Set<String>): String?
fun firstIsTaggedAddressableNote(addressableNotes: Set<String>): String?
@@ -42,9 +46,11 @@ interface EventInterface {
fun getTagOfAddressableKind(kind: Int): ATag?
fun hashtags(): List<String>
fun geohashes(): List<String>
fun getReward(): BigDecimal?
fun getPoWRank(): Int
fun getGeoHash(): String?
fun zapAddress(): String?
fun isSensitive(): Boolean
@@ -4,7 +4,7 @@ import androidx.compose.runtime.Immutable
import com.vitorpamplona.amethyst.model.HexKey
import com.vitorpamplona.amethyst.model.TimeUtils
import com.vitorpamplona.amethyst.model.toHexKey
import nostr.postr.Utils
import com.vitorpamplona.amethyst.service.CryptoUtils
@Immutable
class FileHeaderEvent(
@@ -74,9 +74,9 @@ class FileHeaderEvent(
)
val content = description ?: ""
val pubKey = Utils.pubkeyCreate(privateKey).toHexKey()
val pubKey = CryptoUtils.pubkeyCreate(privateKey).toHexKey()
val id = generateId(pubKey, createdAt, kind, tags, content)
val sig = Utils.sign(id, privateKey)
val sig = CryptoUtils.sign(id, privateKey)
return FileHeaderEvent(id.toHexKey(), pubKey, createdAt, tags, content, sig.toHexKey())
}
}
@@ -5,7 +5,7 @@ import androidx.compose.runtime.Immutable
import com.vitorpamplona.amethyst.model.HexKey
import com.vitorpamplona.amethyst.model.TimeUtils
import com.vitorpamplona.amethyst.model.toHexKey
import nostr.postr.Utils
import com.vitorpamplona.amethyst.service.CryptoUtils
import java.util.Base64
@Immutable
@@ -51,9 +51,9 @@ class FileStorageEvent(
)
val content = encode(data)
val pubKey = Utils.pubkeyCreate(privateKey).toHexKey()
val pubKey = CryptoUtils.pubkeyCreate(privateKey).toHexKey()
val id = generateId(pubKey, createdAt, kind, tags, content)
val sig = Utils.sign(id, privateKey)
val sig = CryptoUtils.sign(id, privateKey)
return FileStorageEvent(id.toHexKey(), pubKey, createdAt, tags, content, sig.toHexKey())
}
}
@@ -4,7 +4,7 @@ import androidx.compose.runtime.Immutable
import com.vitorpamplona.amethyst.model.HexKey
import com.vitorpamplona.amethyst.model.TimeUtils
import com.vitorpamplona.amethyst.model.toHexKey
import nostr.postr.Utils
import com.vitorpamplona.amethyst.service.CryptoUtils
@Immutable
class FileStorageHeaderEvent(
@@ -74,9 +74,9 @@ class FileStorageHeaderEvent(
)
val content = description ?: ""
val pubKey = Utils.pubkeyCreate(privateKey).toHexKey()
val pubKey = CryptoUtils.pubkeyCreate(privateKey).toHexKey()
val id = generateId(pubKey, createdAt, kind, tags, content)
val sig = Utils.sign(id, privateKey)
val sig = CryptoUtils.sign(id, privateKey)
return FileStorageHeaderEvent(id.toHexKey(), pubKey, createdAt, tags, content, sig.toHexKey())
}
}
@@ -5,7 +5,7 @@ import androidx.compose.runtime.Immutable
import com.google.gson.reflect.TypeToken
import com.vitorpamplona.amethyst.model.HexKey
import com.vitorpamplona.amethyst.model.hexToByteArray
import nostr.postr.Utils
import com.vitorpamplona.amethyst.service.CryptoUtils
@Immutable
abstract class GeneralListEvent(
@@ -28,9 +28,9 @@ abstract class GeneralListEvent(
if (content.isBlank()) return null
return try {
val sharedSecret = Utils.getSharedSecret(privKey, pubKey.hexToByteArray())
val sharedSecret = CryptoUtils.getSharedSecret(privKey, pubKey.hexToByteArray())
return Utils.decrypt(content, sharedSecret)
return CryptoUtils.decrypt(content, sharedSecret)
} catch (e: Exception) {
Log.w("GeneralList", "Error decrypting the message ${e.message} for ${dTag()}")
null
@@ -61,6 +61,7 @@ abstract class GeneralListEvent(
fun privateTaggedUsers(privKey: ByteArray) = privateTags(privKey)?.filter { it.size > 1 && it[0] == "p" }?.map { it[1] }
fun privateHashtags(privKey: ByteArray) = privateTags(privKey)?.filter { it.size > 1 && it[0] == "t" }?.map { it[1] }
fun privateGeohashes(privKey: ByteArray) = privateTags(privKey)?.filter { it.size > 1 && it[0] == "g" }?.map { it[1] }
fun privateTaggedEvents(privKey: ByteArray) = privateTags(privKey)?.filter { it.size > 1 && it[0] == "e" }?.map { it[1] }
fun privateTaggedAddresses(privKey: ByteArray) = privateTags(privKey)?.filter { it.firstOrNull() == "a" }?.mapNotNull {
val aTagValue = it.getOrNull(1)
@@ -90,7 +91,7 @@ abstract class GeneralListEvent(
}
val msg = gson.toJson(privTags)
return Utils.encrypt(
return CryptoUtils.encrypt(
msg,
privateKey,
pubKey
@@ -101,10 +102,10 @@ abstract class GeneralListEvent(
privateTags: List<List<String>>? = null,
privateKey: ByteArray
): String {
return Utils.encrypt(
return CryptoUtils.encrypt(
msg = gson.toJson(privateTags),
privateKey = privateKey,
pubKey = Utils.pubkeyCreate(privateKey)
pubKey = CryptoUtils.pubkeyCreate(privateKey)
)
}
}
@@ -4,8 +4,8 @@ import androidx.compose.runtime.Immutable
import com.vitorpamplona.amethyst.model.HexKey
import com.vitorpamplona.amethyst.model.TimeUtils
import com.vitorpamplona.amethyst.model.toHexKey
import com.vitorpamplona.amethyst.service.CryptoUtils
import com.vitorpamplona.amethyst.service.relays.Client
import nostr.postr.Utils
@Immutable
class GenericRepostEvent(
@@ -35,7 +35,7 @@ class GenericRepostEvent(
val replyToPost = listOf("e", boostedPost.id())
val replyToAuthor = listOf("p", boostedPost.pubKey())
val pubKey = Utils.pubkeyCreate(privateKey).toHexKey()
val pubKey = CryptoUtils.pubkeyCreate(privateKey).toHexKey()
var tags: List<List<String>> = listOf(replyToPost, replyToAuthor)
if (boostedPost is AddressableEvent) {
@@ -45,7 +45,7 @@ class GenericRepostEvent(
tags = tags + listOf(listOf("k", "${boostedPost.kind()}"))
val id = generateId(pubKey, createdAt, kind, tags, content)
val sig = Utils.sign(id, privateKey)
val sig = CryptoUtils.sign(id, privateKey)
return GenericRepostEvent(id.toHexKey(), pubKey, createdAt, tags, content, sig.toHexKey())
}
}
@@ -4,8 +4,7 @@ import androidx.compose.runtime.Immutable
import com.vitorpamplona.amethyst.model.HexKey
import com.vitorpamplona.amethyst.model.TimeUtils
import com.vitorpamplona.amethyst.model.toHexKey
import nostr.postr.Utils
import java.security.MessageDigest
import com.vitorpamplona.amethyst.service.CryptoUtils
@Immutable
class HTTPAuthorizationEvent(
@@ -27,11 +26,9 @@ class HTTPAuthorizationEvent(
privateKey: ByteArray,
createdAt: Long = TimeUtils.now()
): HTTPAuthorizationEvent {
val sha256 = MessageDigest.getInstance("SHA-256")
var hash = ""
body?.let {
hash = sha256.digest(it.toByteArray()).toHexKey()
hash = CryptoUtils.sha256(it.toByteArray()).toHexKey()
}
val tags = listOfNotNull(
@@ -40,9 +37,9 @@ class HTTPAuthorizationEvent(
listOf("payload", hash)
)
val pubKey = Utils.pubkeyCreate(privateKey).toHexKey()
val pubKey = CryptoUtils.pubkeyCreate(privateKey).toHexKey()
val id = generateId(pubKey, createdAt, kind, tags, "")
val sig = Utils.sign(id, privateKey)
val sig = CryptoUtils.sign(id, privateKey)
return HTTPAuthorizationEvent(id.toHexKey(), pubKey, createdAt, tags, "", sig.toHexKey())
}
}
@@ -4,7 +4,7 @@ import androidx.compose.runtime.Immutable
import com.vitorpamplona.amethyst.model.HexKey
import com.vitorpamplona.amethyst.model.TimeUtils
import com.vitorpamplona.amethyst.model.toHexKey
import nostr.postr.Utils
import com.vitorpamplona.amethyst.service.CryptoUtils
@Immutable
class HighlightEvent(
@@ -30,10 +30,10 @@ class HighlightEvent(
privateKey: ByteArray,
createdAt: Long = TimeUtils.now()
): PollNoteEvent {
val pubKey = Utils.pubkeyCreate(privateKey).toHexKey()
val pubKey = CryptoUtils.pubkeyCreate(privateKey).toHexKey()
val tags = mutableListOf<List<String>>()
val id = generateId(pubKey, createdAt, kind, tags, msg)
val sig = Utils.sign(id, privateKey)
val sig = CryptoUtils.sign(id, privateKey)
return PollNoteEvent(id.toHexKey(), pubKey, createdAt, tags, msg, sig.toHexKey())
}
}
@@ -4,7 +4,7 @@ import androidx.compose.runtime.Immutable
import com.vitorpamplona.amethyst.model.HexKey
import com.vitorpamplona.amethyst.model.TimeUtils
import com.vitorpamplona.amethyst.model.toHexKey
import nostr.postr.Utils
import com.vitorpamplona.amethyst.service.CryptoUtils
@Immutable
class LiveActivitiesChatMessageEvent(
@@ -51,10 +51,11 @@ class LiveActivitiesChatMessageEvent(
privateKey: ByteArray,
createdAt: Long = TimeUtils.now(),
markAsSensitive: Boolean,
zapRaiserAmount: Long?
zapRaiserAmount: Long?,
geohash: String? = null
): LiveActivitiesChatMessageEvent {
val content = message
val pubKey = Utils.pubkeyCreate(privateKey).toHexKey()
val pubKey = CryptoUtils.pubkeyCreate(privateKey).toHexKey()
val tags = mutableListOf(
listOf("a", activity.toTag(), "", "root")
)
@@ -73,9 +74,12 @@ class LiveActivitiesChatMessageEvent(
zapRaiserAmount?.let {
tags.add(listOf("zapraiser", "$it"))
}
geohash?.let {
tags.add(listOf("g", it))
}
val id = generateId(pubKey, createdAt, kind, tags, content)
val sig = Utils.sign(id, privateKey)
val sig = CryptoUtils.sign(id, privateKey)
return LiveActivitiesChatMessageEvent(id.toHexKey(), pubKey, createdAt, tags, content, sig.toHexKey())
}
}
@@ -4,7 +4,7 @@ import androidx.compose.runtime.Immutable
import com.vitorpamplona.amethyst.model.HexKey
import com.vitorpamplona.amethyst.model.TimeUtils
import com.vitorpamplona.amethyst.model.toHexKey
import nostr.postr.Utils
import com.vitorpamplona.amethyst.service.CryptoUtils
@Immutable
class LiveActivitiesEvent(
@@ -51,9 +51,9 @@ class LiveActivitiesEvent(
createdAt: Long = TimeUtils.now()
): LiveActivitiesEvent {
val tags = mutableListOf<List<String>>()
val pubKey = Utils.pubkeyCreate(privateKey).toHexKey()
val pubKey = CryptoUtils.pubkeyCreate(privateKey).toHexKey()
val id = generateId(pubKey, createdAt, kind, tags, "")
val sig = Utils.sign(id, privateKey)
val sig = CryptoUtils.sign(id, privateKey)
return LiveActivitiesEvent(id.toHexKey(), pubKey, createdAt, tags, "", sig.toHexKey())
}
}
@@ -10,7 +10,7 @@ import com.vitorpamplona.amethyst.model.HexKey
import com.vitorpamplona.amethyst.model.TimeUtils
import com.vitorpamplona.amethyst.model.hexToByteArray
import com.vitorpamplona.amethyst.model.toHexKey
import nostr.postr.Utils
import com.vitorpamplona.amethyst.service.CryptoUtils
import java.lang.reflect.Type
@Immutable
@@ -35,9 +35,9 @@ class LnZapPaymentRequestEvent(
}
return try {
val sharedSecret = Utils.getSharedSecret(privKey, pubkey)
val sharedSecret = CryptoUtils.getSharedSecret(privKey, pubkey)
val jsonText = Utils.decrypt(content, sharedSecret)
val jsonText = CryptoUtils.decrypt(content, sharedSecret)
val payInvoiceMethod = gson.fromJson(jsonText, Request::class.java)
@@ -59,10 +59,10 @@ class LnZapPaymentRequestEvent(
privateKey: ByteArray,
createdAt: Long = TimeUtils.now()
): LnZapPaymentRequestEvent {
val pubKey = Utils.pubkeyCreate(privateKey)
val pubKey = CryptoUtils.pubkeyCreate(privateKey)
val serializedRequest = gson.toJson(PayInvoiceMethod.create(lnInvoice))
val content = Utils.encrypt(
val content = CryptoUtils.encrypt(
serializedRequest,
privateKey,
walletServicePubkey.hexToByteArray()
@@ -72,7 +72,7 @@ class LnZapPaymentRequestEvent(
tags.add(listOf("p", walletServicePubkey))
val id = generateId(pubKey.toHexKey(), createdAt, kind, tags, content)
val sig = Utils.sign(id, privateKey)
val sig = CryptoUtils.sign(id, privateKey)
return LnZapPaymentRequestEvent(id.toHexKey(), pubKey.toHexKey(), createdAt, tags, content, sig.toHexKey())
}
}
@@ -8,7 +8,7 @@ import com.google.gson.JsonElement
import com.google.gson.JsonParseException
import com.google.gson.annotations.SerializedName
import com.vitorpamplona.amethyst.model.HexKey
import nostr.postr.Utils
import com.vitorpamplona.amethyst.service.CryptoUtils
import java.lang.reflect.Type
@Immutable
@@ -30,9 +30,9 @@ class LnZapPaymentResponseEvent(
private fun decrypt(privKey: ByteArray, pubKey: ByteArray): String? {
return try {
val sharedSecret = Utils.getSharedSecret(privKey, pubKey)
val sharedSecret = CryptoUtils.getSharedSecret(privKey, pubKey)
val retVal = Utils.decrypt(content, sharedSecret)
val retVal = CryptoUtils.decrypt(content, sharedSecret)
if (retVal.startsWith(PrivateDmEvent.nip18Advertisement)) {
retVal.substring(16)
@@ -2,12 +2,10 @@ package com.vitorpamplona.amethyst.service.model
import androidx.compose.runtime.Immutable
import com.vitorpamplona.amethyst.model.*
import nostr.postr.Bech32
import nostr.postr.Utils
import com.vitorpamplona.amethyst.service.Bech32
import com.vitorpamplona.amethyst.service.CryptoUtils
import java.nio.charset.Charset
import java.security.MessageDigest
import java.security.SecureRandom
import java.util.*
import javax.crypto.BadPaddingException
import javax.crypto.Cipher
import javax.crypto.spec.IvParameterSpec
@@ -68,7 +66,7 @@ class LnZapRequestEvent(
): LnZapRequestEvent {
var content = message
var privkey = privateKey
var pubKey = Utils.pubkeyCreate(privateKey).toHexKey()
var pubKey = CryptoUtils.pubkeyCreate(privateKey).toHexKey()
var tags = listOf(
listOf("e", originalNote.id()),
listOf("p", originalNote.pubKey()),
@@ -82,8 +80,8 @@ class LnZapRequestEvent(
}
if (zapType == LnZapEvent.ZapType.ANONYMOUS) {
tags = tags + listOf(listOf("anon", ""))
privkey = Utils.privkeyCreate()
pubKey = Utils.pubkeyCreate(privkey).toHexKey()
privkey = CryptoUtils.privkeyCreate()
pubKey = CryptoUtils.pubkeyCreate(privkey).toHexKey()
} else if (zapType == LnZapEvent.ZapType.PRIVATE) {
var encryptionPrivateKey = createEncryptionPrivateKey(privateKey.toHexKey(), originalNote.id(), createdAt)
var noteJson = (create(privkey, 9733, listOf(tags[0], tags[1]), message)).toJson()
@@ -91,10 +89,10 @@ class LnZapRequestEvent(
tags = tags + listOf(listOf("anon", encryptedContent))
content = "" // make sure public content is empty, as the content is encrypted
privkey = encryptionPrivateKey // sign event with generated privkey
pubKey = Utils.pubkeyCreate(encryptionPrivateKey).toHexKey() // updated event with according pubkey
pubKey = CryptoUtils.pubkeyCreate(encryptionPrivateKey).toHexKey() // updated event with according pubkey
}
val id = generateId(pubKey, createdAt, kind, tags, content)
val sig = Utils.sign(id, privkey)
val sig = CryptoUtils.sign(id, privkey)
return LnZapRequestEvent(id.toHexKey(), pubKey, createdAt, tags, content, sig.toHexKey())
}
@@ -108,14 +106,14 @@ class LnZapRequestEvent(
): LnZapRequestEvent {
var content = message
var privkey = privateKey
var pubKey = Utils.pubkeyCreate(privateKey).toHexKey()
var pubKey = CryptoUtils.pubkeyCreate(privateKey).toHexKey()
var tags = listOf(
listOf("p", userHex),
listOf("relays") + relays
)
if (zapType == LnZapEvent.ZapType.ANONYMOUS) {
privkey = Utils.privkeyCreate()
pubKey = Utils.pubkeyCreate(privkey).toHexKey()
privkey = CryptoUtils.privkeyCreate()
pubKey = CryptoUtils.pubkeyCreate(privkey).toHexKey()
tags = tags + listOf(listOf("anon", ""))
} else if (zapType == LnZapEvent.ZapType.PRIVATE) {
var encryptionPrivateKey = createEncryptionPrivateKey(privateKey.toHexKey(), userHex, createdAt)
@@ -124,21 +122,21 @@ class LnZapRequestEvent(
tags = tags + listOf(listOf("anon", encryptedContent))
content = ""
privkey = encryptionPrivateKey
pubKey = Utils.pubkeyCreate(encryptionPrivateKey).toHexKey()
pubKey = CryptoUtils.pubkeyCreate(encryptionPrivateKey).toHexKey()
}
val id = generateId(pubKey, createdAt, kind, tags, content)
val sig = Utils.sign(id, privkey)
val sig = CryptoUtils.sign(id, privkey)
return LnZapRequestEvent(id.toHexKey(), pubKey, createdAt, tags, content, sig.toHexKey())
}
fun createEncryptionPrivateKey(privkey: String, id: String, createdAt: Long): ByteArray {
var str = privkey + id + createdAt.toString()
var strbyte = str.toByteArray(Charset.forName("utf-8"))
return MessageDigest.getInstance("SHA-256").digest(strbyte)
return CryptoUtils.sha256(strbyte)
}
private fun encryptPrivateZapMessage(msg: String, privkey: ByteArray, pubkey: ByteArray): String {
var sharedSecret = Utils.getSharedSecret(privkey, pubkey)
var sharedSecret = CryptoUtils.getSharedSecret(privkey, pubkey)
val iv = ByteArray(16)
SecureRandom().nextBytes(iv)
@@ -157,7 +155,7 @@ class LnZapRequestEvent(
}
private fun decryptPrivateZapMessage(msg: String, privkey: ByteArray, pubkey: ByteArray): String {
var sharedSecret = Utils.getSharedSecret(privkey, pubkey)
var sharedSecret = CryptoUtils.getSharedSecret(privkey, pubkey)
if (sharedSecret.size != 16 && sharedSecret.size != 32) {
throw IllegalArgumentException("Invalid shared secret size")
}
@@ -4,7 +4,7 @@ import androidx.compose.runtime.Immutable
import com.vitorpamplona.amethyst.model.HexKey
import com.vitorpamplona.amethyst.model.TimeUtils
import com.vitorpamplona.amethyst.model.toHexKey
import nostr.postr.Utils
import com.vitorpamplona.amethyst.service.CryptoUtils
@Immutable
class LongTextNoteEvent(
@@ -33,7 +33,7 @@ class LongTextNoteEvent(
const val kind = 30023
fun create(msg: String, replyTos: List<String>?, mentions: List<String>?, privateKey: ByteArray, createdAt: Long = TimeUtils.now()): LongTextNoteEvent {
val pubKey = Utils.pubkeyCreate(privateKey).toHexKey()
val pubKey = CryptoUtils.pubkeyCreate(privateKey).toHexKey()
val tags = mutableListOf<List<String>>()
replyTos?.forEach {
tags.add(listOf("e", it))
@@ -42,7 +42,7 @@ class LongTextNoteEvent(
tags.add(listOf("p", it))
}
val id = generateId(pubKey, createdAt, kind, tags, msg)
val sig = Utils.sign(id, privateKey)
val sig = CryptoUtils.sign(id, privateKey)
return LongTextNoteEvent(id.toHexKey(), pubKey, createdAt, tags, msg, sig.toHexKey())
}
}
@@ -10,7 +10,7 @@ import com.vitorpamplona.amethyst.model.HexKey
import com.vitorpamplona.amethyst.model.TimeUtils
import com.vitorpamplona.amethyst.model.UserMetadata
import com.vitorpamplona.amethyst.model.toHexKey
import nostr.postr.Utils
import com.vitorpamplona.amethyst.service.CryptoUtils
import java.io.ByteArrayInputStream
@Stable
@@ -172,7 +172,7 @@ class MetadataEvent(
}
fun create(contactMetaData: String, identities: List<IdentityClaim>, privateKey: ByteArray, createdAt: Long = TimeUtils.now()): MetadataEvent {
val pubKey = Utils.pubkeyCreate(privateKey).toHexKey()
val pubKey = CryptoUtils.pubkeyCreate(privateKey).toHexKey()
val tags = mutableListOf<List<String>>()
identities.forEach {
@@ -180,7 +180,7 @@ class MetadataEvent(
}
val id = generateId(pubKey, createdAt, kind, tags, contactMetaData)
val sig = Utils.sign(id, privateKey)
val sig = CryptoUtils.sign(id, privateKey)
return MetadataEvent(id.toHexKey(), pubKey, createdAt, tags, contactMetaData, sig.toHexKey())
}
}
@@ -7,7 +7,7 @@ import com.vitorpamplona.amethyst.model.HexKey
import com.vitorpamplona.amethyst.model.TimeUtils
import com.vitorpamplona.amethyst.model.hexToByteArray
import com.vitorpamplona.amethyst.model.toHexKey
import nostr.postr.Utils
import com.vitorpamplona.amethyst.service.CryptoUtils
@Immutable
class MuteListEvent(
@@ -23,9 +23,9 @@ class MuteListEvent(
fun plainContent(privKey: ByteArray): String? {
return try {
val sharedSecret = Utils.getSharedSecret(privKey, pubKey.hexToByteArray())
val sharedSecret = CryptoUtils.getSharedSecret(privKey, pubKey.hexToByteArray())
return Utils.decrypt(content, sharedSecret)
return CryptoUtils.decrypt(content, sharedSecret)
} catch (e: Exception) {
Log.w("BookmarkList", "Error decrypting the message ${e.message}")
null
@@ -73,7 +73,7 @@ class MuteListEvent(
privateKey: ByteArray,
createdAt: Long = TimeUtils.now()
): MuteListEvent {
val pubKey = Utils.pubkeyCreate(privateKey)
val pubKey = CryptoUtils.pubkeyCreate(privateKey)
val privTags = mutableListOf<List<String>>()
privEvents?.forEach {
@@ -87,7 +87,7 @@ class MuteListEvent(
}
val msg = gson.toJson(privTags)
val content = Utils.encrypt(
val content = CryptoUtils.encrypt(
msg,
privateKey,
pubKey
@@ -105,7 +105,7 @@ class MuteListEvent(
}
val id = generateId(pubKey.toHexKey(), createdAt, kind, tags, content)
val sig = Utils.sign(id, privateKey)
val sig = CryptoUtils.sign(id, privateKey)
return MuteListEvent(id.toHexKey(), pubKey.toHexKey(), createdAt, tags, content, sig.toHexKey())
}
}
@@ -4,7 +4,7 @@ import androidx.compose.runtime.Immutable
import com.vitorpamplona.amethyst.model.HexKey
import com.vitorpamplona.amethyst.model.TimeUtils
import com.vitorpamplona.amethyst.model.toHexKey
import nostr.postr.Utils
import com.vitorpamplona.amethyst.service.CryptoUtils
@Immutable
class NNSEvent(
@@ -31,9 +31,9 @@ class NNSEvent(
createdAt: Long = TimeUtils.now()
): NNSEvent {
val tags = mutableListOf<List<String>>()
val pubKey = Utils.pubkeyCreate(privateKey).toHexKey()
val pubKey = CryptoUtils.pubkeyCreate(privateKey).toHexKey()
val id = generateId(pubKey, createdAt, kind, tags, "")
val sig = Utils.sign(id, privateKey)
val sig = CryptoUtils.sign(id, privateKey)
return NNSEvent(id.toHexKey(), pubKey, createdAt, tags, "", sig.toHexKey())
}
}
@@ -4,10 +4,10 @@ import androidx.compose.runtime.Immutable
import com.vitorpamplona.amethyst.model.HexKey
import com.vitorpamplona.amethyst.model.TimeUtils
import com.vitorpamplona.amethyst.model.toHexKey
import com.vitorpamplona.amethyst.service.CryptoUtils
import kotlinx.collections.immutable.ImmutableSet
import kotlinx.collections.immutable.persistentSetOf
import kotlinx.collections.immutable.toImmutableSet
import nostr.postr.Utils
@Immutable
class PeopleListEvent(
@@ -141,9 +141,9 @@ class PeopleListEvent(
}
fun create(content: String, tags: List<List<String>>, privateKey: ByteArray, createdAt: Long = TimeUtils.now()): PeopleListEvent {
val pubKey = Utils.pubkeyCreate(privateKey).toHexKey()
val pubKey = CryptoUtils.pubkeyCreate(privateKey).toHexKey()
val id = generateId(pubKey, createdAt, kind, tags, content)
val sig = Utils.sign(id, privateKey)
val sig = CryptoUtils.sign(id, privateKey)
return PeopleListEvent(id.toHexKey(), pubKey, createdAt, tags, content, sig.toHexKey())
}
}
@@ -4,7 +4,7 @@ import androidx.compose.runtime.Immutable
import com.vitorpamplona.amethyst.model.HexKey
import com.vitorpamplona.amethyst.model.TimeUtils
import com.vitorpamplona.amethyst.model.toHexKey
import nostr.postr.Utils
import com.vitorpamplona.amethyst.service.CryptoUtils
@Immutable
class PinListEvent(
@@ -34,9 +34,9 @@ class PinListEvent(
tags.add(listOf("pin", it))
}
val pubKey = Utils.pubkeyCreate(privateKey).toHexKey()
val pubKey = CryptoUtils.pubkeyCreate(privateKey).toHexKey()
val id = generateId(pubKey, createdAt, kind, tags, "")
val sig = Utils.sign(id, privateKey)
val sig = CryptoUtils.sign(id, privateKey)
return PinListEvent(id.toHexKey(), pubKey, createdAt, tags, "", sig.toHexKey())
}
}
@@ -4,7 +4,7 @@ import androidx.compose.runtime.Immutable
import com.vitorpamplona.amethyst.model.HexKey
import com.vitorpamplona.amethyst.model.TimeUtils
import com.vitorpamplona.amethyst.model.toHexKey
import nostr.postr.Utils
import com.vitorpamplona.amethyst.service.CryptoUtils
const val POLL_OPTION = "poll_option"
const val VALUE_MAXIMUM = "value_maximum"
@@ -53,9 +53,10 @@ class PollNoteEvent(
closedAt: Int?,
zapReceiver: String?,
markAsSensitive: Boolean,
zapRaiserAmount: Long?
zapRaiserAmount: Long?,
geohash: String? = null
): PollNoteEvent {
val pubKey = Utils.pubkeyCreate(privateKey).toHexKey()
val pubKey = CryptoUtils.pubkeyCreate(privateKey).toHexKey()
val tags = mutableListOf<List<String>>()
replyTos?.forEach {
tags.add(listOf("e", it))
@@ -83,9 +84,12 @@ class PollNoteEvent(
zapRaiserAmount?.let {
tags.add(listOf("zapraiser", "$it"))
}
geohash?.let {
tags.add(listOf("g", it))
}
val id = generateId(pubKey, createdAt, kind, tags, msg)
val sig = Utils.sign(id, privateKey)
val sig = CryptoUtils.sign(id, privateKey)
return PollNoteEvent(id.toHexKey(), pubKey, createdAt, tags, msg, sig.toHexKey())
}
}
@@ -5,10 +5,9 @@ import androidx.compose.runtime.Immutable
import com.vitorpamplona.amethyst.model.HexKey
import com.vitorpamplona.amethyst.model.TimeUtils
import com.vitorpamplona.amethyst.model.toHexKey
import com.vitorpamplona.amethyst.service.CryptoUtils
import com.vitorpamplona.amethyst.service.HexValidator
import fr.acinq.secp256k1.Hex
import nostr.postr.Utils
import nostr.postr.toHex
@Immutable
class PrivateDmEvent(
@@ -56,9 +55,9 @@ class PrivateDmEvent(
fun plainContent(privKey: ByteArray, pubKey: ByteArray): String? {
return try {
val sharedSecret = Utils.getSharedSecret(privKey, pubKey)
val sharedSecret = CryptoUtils.getSharedSecret(privKey, pubKey)
val retVal = Utils.decrypt(content, sharedSecret)
val retVal = CryptoUtils.decrypt(content, sharedSecret)
if (retVal.startsWith(nip18Advertisement)) {
retVal.substring(16)
@@ -87,17 +86,18 @@ class PrivateDmEvent(
publishedRecipientPubKey: ByteArray? = null,
advertiseNip18: Boolean = true,
markAsSensitive: Boolean,
zapRaiserAmount: Long?
zapRaiserAmount: Long?,
geohash: String? = null
): PrivateDmEvent {
val content = Utils.encrypt(
val content = CryptoUtils.encrypt(
if (advertiseNip18) { nip18Advertisement } else { "" } + msg,
privateKey,
recipientPubKey
)
val pubKey = Utils.pubkeyCreate(privateKey).toHexKey()
val pubKey = CryptoUtils.pubkeyCreate(privateKey).toHexKey()
val tags = mutableListOf<List<String>>()
publishedRecipientPubKey?.let {
tags.add(listOf("p", publishedRecipientPubKey.toHex()))
tags.add(listOf("p", publishedRecipientPubKey.toHexKey()))
}
replyTos?.forEach {
tags.add(listOf("e", it))
@@ -114,8 +114,12 @@ class PrivateDmEvent(
zapRaiserAmount?.let {
tags.add(listOf("zapraiser", "$it"))
}
geohash?.let {
tags.add(listOf("g", it))
}
val id = generateId(pubKey, createdAt, kind, tags, content)
val sig = Utils.sign(id, privateKey)
val sig = CryptoUtils.sign(id, privateKey)
return PrivateDmEvent(id.toHexKey(), pubKey, createdAt, tags, content, sig.toHexKey())
}
}
@@ -4,7 +4,7 @@ import androidx.compose.runtime.Immutable
import com.vitorpamplona.amethyst.model.HexKey
import com.vitorpamplona.amethyst.model.TimeUtils
import com.vitorpamplona.amethyst.model.toHexKey
import nostr.postr.Utils
import com.vitorpamplona.amethyst.service.CryptoUtils
@Immutable
class ReactionEvent(
@@ -31,7 +31,7 @@ class ReactionEvent(
}
fun create(content: String, originalNote: EventInterface, privateKey: ByteArray, createdAt: Long = TimeUtils.now()): ReactionEvent {
val pubKey = Utils.pubkeyCreate(privateKey).toHexKey()
val pubKey = CryptoUtils.pubkeyCreate(privateKey).toHexKey()
var tags = listOf(listOf("e", originalNote.id()), listOf("p", originalNote.pubKey()))
if (originalNote is AddressableEvent) {
@@ -39,13 +39,13 @@ class ReactionEvent(
}
val id = generateId(pubKey, createdAt, kind, tags, content)
val sig = Utils.sign(id, privateKey)
val sig = CryptoUtils.sign(id, privateKey)
return ReactionEvent(id.toHexKey(), pubKey, createdAt, tags, content, sig.toHexKey())
}
fun create(emojiUrl: EmojiUrl, originalNote: EventInterface, privateKey: ByteArray, createdAt: Long = TimeUtils.now()): ReactionEvent {
val content = ":${emojiUrl.code}:"
val pubKey = Utils.pubkeyCreate(privateKey).toHexKey()
val pubKey = CryptoUtils.pubkeyCreate(privateKey).toHexKey()
var tags = listOf(
listOf("e", originalNote.id()),
@@ -58,7 +58,7 @@ class ReactionEvent(
}
val id = generateId(pubKey, createdAt, kind, tags, content)
val sig = Utils.sign(id, privateKey)
val sig = CryptoUtils.sign(id, privateKey)
return ReactionEvent(id.toHexKey(), pubKey, createdAt, tags, content, sig.toHexKey())
}
}
@@ -4,7 +4,7 @@ import androidx.compose.runtime.Immutable
import com.vitorpamplona.amethyst.model.HexKey
import com.vitorpamplona.amethyst.model.TimeUtils
import com.vitorpamplona.amethyst.model.toHexKey
import nostr.postr.Utils
import com.vitorpamplona.amethyst.service.CryptoUtils
import java.net.URI
@Immutable
@@ -29,10 +29,10 @@ class RecommendRelayEvent(
fun create(relay: URI, privateKey: ByteArray, createdAt: Long = TimeUtils.now()): RecommendRelayEvent {
val content = relay.toString()
val pubKey = Utils.pubkeyCreate(privateKey).toHexKey()
val pubKey = CryptoUtils.pubkeyCreate(privateKey).toHexKey()
val tags = listOf<List<String>>()
val id = generateId(pubKey, createdAt, kind, tags, content)
val sig = Utils.sign(id, privateKey)
val sig = CryptoUtils.sign(id, privateKey)
return RecommendRelayEvent(id.toHexKey(), pubKey, createdAt, tags, content, sig.toHexKey())
}
}
@@ -4,7 +4,7 @@ import androidx.compose.runtime.Immutable
import com.vitorpamplona.amethyst.model.HexKey
import com.vitorpamplona.amethyst.model.TimeUtils
import com.vitorpamplona.amethyst.model.toHexKey
import nostr.postr.Utils
import com.vitorpamplona.amethyst.service.CryptoUtils
@Immutable
class RelayAuthEvent(
@@ -23,13 +23,13 @@ class RelayAuthEvent(
fun create(relay: String, challenge: String, privateKey: ByteArray, createdAt: Long = TimeUtils.now()): RelayAuthEvent {
val content = ""
val pubKey = Utils.pubkeyCreate(privateKey).toHexKey()
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 = Utils.sign(id, privateKey)
val sig = CryptoUtils.sign(id, privateKey)
return RelayAuthEvent(id.toHexKey(), pubKey, createdAt, tags, content, sig.toHexKey())
}
}
@@ -4,7 +4,7 @@ import androidx.compose.runtime.Immutable
import com.vitorpamplona.amethyst.model.HexKey
import com.vitorpamplona.amethyst.model.TimeUtils
import com.vitorpamplona.amethyst.model.toHexKey
import nostr.postr.Utils
import com.vitorpamplona.amethyst.service.CryptoUtils
@Immutable
class RelaySetEvent(
@@ -35,9 +35,9 @@ class RelaySetEvent(
tags.add(listOf("r", it))
}
val pubKey = Utils.pubkeyCreate(privateKey).toHexKey()
val pubKey = CryptoUtils.pubkeyCreate(privateKey).toHexKey()
val id = generateId(pubKey, createdAt, kind, tags, "")
val sig = Utils.sign(id, privateKey)
val sig = CryptoUtils.sign(id, privateKey)
return RelaySetEvent(id.toHexKey(), pubKey, createdAt, tags, "", sig.toHexKey())
}
}
@@ -4,7 +4,7 @@ import androidx.compose.runtime.Immutable
import com.vitorpamplona.amethyst.model.HexKey
import com.vitorpamplona.amethyst.model.TimeUtils
import com.vitorpamplona.amethyst.model.toHexKey
import nostr.postr.Utils
import com.vitorpamplona.amethyst.service.CryptoUtils
@Immutable
data class ReportedKey(val key: String, val reportType: ReportEvent.ReportType)
@@ -63,7 +63,7 @@ class ReportEvent(
val reportPostTag = listOf("e", reportedPost.id(), type.name.lowercase())
val reportAuthorTag = listOf("p", reportedPost.pubKey(), type.name.lowercase())
val pubKey = Utils.pubkeyCreate(privateKey).toHexKey()
val pubKey = CryptoUtils.pubkeyCreate(privateKey).toHexKey()
var tags: List<List<String>> = listOf(reportPostTag, reportAuthorTag)
if (reportedPost is AddressableEvent) {
@@ -71,7 +71,7 @@ class ReportEvent(
}
val id = generateId(pubKey, createdAt, kind, tags, content)
val sig = Utils.sign(id, privateKey)
val sig = CryptoUtils.sign(id, privateKey)
return ReportEvent(id.toHexKey(), pubKey, createdAt, tags, content, sig.toHexKey())
}
@@ -80,10 +80,10 @@ class ReportEvent(
val reportAuthorTag = listOf("p", reportedUser, type.name.lowercase())
val pubKey = Utils.pubkeyCreate(privateKey).toHexKey()
val pubKey = CryptoUtils.pubkeyCreate(privateKey).toHexKey()
val tags: List<List<String>> = listOf(reportAuthorTag)
val id = generateId(pubKey, createdAt, kind, tags, content)
val sig = Utils.sign(id, privateKey)
val sig = CryptoUtils.sign(id, privateKey)
return ReportEvent(id.toHexKey(), pubKey, createdAt, tags, content, sig.toHexKey())
}
}
@@ -4,8 +4,8 @@ import androidx.compose.runtime.Immutable
import com.vitorpamplona.amethyst.model.HexKey
import com.vitorpamplona.amethyst.model.TimeUtils
import com.vitorpamplona.amethyst.model.toHexKey
import com.vitorpamplona.amethyst.service.CryptoUtils
import com.vitorpamplona.amethyst.service.relays.Client
import nostr.postr.Utils
@Immutable
class RepostEvent(
@@ -35,7 +35,7 @@ class RepostEvent(
val replyToPost = listOf("e", boostedPost.id())
val replyToAuthor = listOf("p", boostedPost.pubKey())
val pubKey = Utils.pubkeyCreate(privateKey).toHexKey()
val pubKey = CryptoUtils.pubkeyCreate(privateKey).toHexKey()
var tags: List<List<String>> = listOf(replyToPost, replyToAuthor)
if (boostedPost is AddressableEvent) {
@@ -43,7 +43,7 @@ class RepostEvent(
}
val id = generateId(pubKey, createdAt, kind, tags, content)
val sig = Utils.sign(id, privateKey)
val sig = CryptoUtils.sign(id, privateKey)
return RepostEvent(id.toHexKey(), pubKey, createdAt, tags, content, sig.toHexKey())
}
}
@@ -6,8 +6,8 @@ import com.linkedin.urls.detection.UrlDetectorOptions
import com.vitorpamplona.amethyst.model.HexKey
import com.vitorpamplona.amethyst.model.TimeUtils
import com.vitorpamplona.amethyst.model.toHexKey
import com.vitorpamplona.amethyst.service.CryptoUtils
import com.vitorpamplona.amethyst.ui.screen.loggedIn.findHashtags
import nostr.postr.Utils
@Immutable
class TextNoteEvent(
@@ -38,11 +38,12 @@ class TextNoteEvent(
replyingTo: String?,
root: String?,
directMentions: Set<HexKey>,
geohash: String? = null,
privateKey: ByteArray,
createdAt: Long = TimeUtils.now()
): TextNoteEvent {
val pubKey = Utils.pubkeyCreate(privateKey).toHexKey()
val pubKey = CryptoUtils.pubkeyCreate(privateKey).toHexKey()
val tags = mutableListOf<List<String>>()
replyTos?.forEach {
if (it == replyingTo) {
@@ -93,9 +94,12 @@ class TextNoteEvent(
zapRaiserAmount?.let {
tags.add(listOf("zapraiser", "$it"))
}
geohash?.let {
tags.add(listOf("g", it))
}
val id = generateId(pubKey, createdAt, kind, tags, msg)
val sig = Utils.sign(id, privateKey)
val sig = CryptoUtils.sign(id, privateKey)
return TextNoteEvent(id.toHexKey(), pubKey, createdAt, tags, msg, sig.toHexKey())
}
}
@@ -4,9 +4,9 @@ import android.util.Log
import androidx.compose.runtime.Immutable
import com.vitorpamplona.amethyst.model.hexToByteArray
import com.vitorpamplona.amethyst.model.toHexKey
import nostr.postr.Bech32
import nostr.postr.bechToBytes
import nostr.postr.toByteArray
import com.vitorpamplona.amethyst.service.bechToBytes
import com.vitorpamplona.amethyst.service.toByteArray
import com.vitorpamplona.amethyst.service.toNEvent
import java.util.regex.Pattern
object Nip19 {
@@ -167,6 +167,6 @@ object Nip19 {
fullArray = fullArray + byteArrayOf(Tlv.Type.KIND.id, kind.size.toByte()) + kind
}
return Bech32.encodeBytes(hrp = "nevent", fullArray, Bech32.Encoding.Bech32)
return fullArray.toNEvent()
}
}
@@ -26,9 +26,12 @@ class EventNotificationConsumer(private val applicationContext: Context) {
// adds to database
LocalCache.verifyAndConsume(event, null)
when (event) {
is PrivateDmEvent -> notify(event)
is LnZapEvent -> notify(event)
val manager = notificationManager()
if (manager.areNotificationsEnabled()) {
when (event) {
is PrivateDmEvent -> notify(event)
is LnZapEvent -> notify(event)
}
}
}
}
@@ -56,7 +56,9 @@ class RegisterAccounts(
val client = HttpClient.getHttpClient()
client.newCall(request).execute()
val isSucess = client.newCall(request).execute().use {
it.isSuccessful
}
} catch (e: java.lang.Exception) {
Log.e("FirebaseMsgService", "Unable to register with push server", e)
}
@@ -3,6 +3,7 @@ package com.vitorpamplona.amethyst.service.previews
import com.vitorpamplona.amethyst.service.HttpClient
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import okhttp3.Request
import org.jsoup.Jsoup
import org.jsoup.nodes.Document
@@ -61,10 +62,12 @@ private const val CONTENT = "content"
suspend fun getDocument(url: String, timeOut: Int = 30000): Document =
withContext(Dispatchers.IO) {
return@withContext Jsoup.connect(url)
.proxy(HttpClient.getProxy())
.timeout(timeOut)
.get()
val request: Request = Request.Builder().url(url).get().build()
val html = HttpClient.getHttpClient().newCall(request).execute().use {
it.body.string()
}
Jsoup.parse(html)
}
suspend fun parseHtml(url: String, document: Document): UrlInfoItem =
@@ -112,7 +112,7 @@ object Client : RelayPool.Listener {
val relay = Relay(url, true, true, feedTypes ?: emptySet(), HttpClient.getProxy())
RelayPool.addRelay(relay)
relay.requestAndWatch {
relay.connectAndRun {
allSubscriptions().forEach {
relay.sendFilter(requestId = it)
}
@@ -178,6 +178,7 @@ object Client : RelayPool.Listener {
}
}
@OptIn(DelicateCoroutinesApi::class)
override fun onAuth(relay: Relay, challenge: String) {
// Releases the Web thread for the new payload.
// May need to add a processing queue if processing new events become too costly.
@@ -15,6 +15,7 @@ import okhttp3.WebSocket
import okhttp3.WebSocketListener
import java.net.Proxy
import java.time.Duration
import java.util.concurrent.atomic.AtomicBoolean
enum class FeedType {
FOLLOWS, PUBLIC_CHATS, PRIVATE_DMS, GLOBAL, SEARCH, WALLET_CONNECT
@@ -29,14 +30,16 @@ class Relay(
var activeTypes: Set<FeedType> = FeedType.values().toSet(),
proxy: Proxy?
) {
val seconds = if (proxy != null) 20L else 10L
val duration = Duration.ofSeconds(seconds)
companion object {
// waits 3 minutes to reconnect once things fail
const val RECONNECTING_IN_SECONDS = 60 * 3
}
private val httpClient = OkHttpClient.Builder()
.proxy(proxy)
.readTimeout(duration)
.connectTimeout(duration)
.writeTimeout(duration)
.readTimeout(Duration.ofSeconds(if (proxy != null) 20L else 10L))
.connectTimeout(Duration.ofSeconds(if (proxy != null) 20L else 10L))
.writeTimeout(Duration.ofSeconds(if (proxy != null) 20L else 10L))
.followRedirects(true)
.followSslRedirects(true)
.build()
@@ -50,9 +53,9 @@ class Relay(
var spamCounter = 0
var errorCounter = 0
var ping: Long? = null
var pingInMs: Long? = null
var closingTime = 0L
var closingTimeInSeconds = 0L
var afterEOSE = false
@@ -68,10 +71,8 @@ class Relay(
return socket != null
}
@Synchronized
fun requestAndWatch() {
checkNotInMainThread()
requestAndWatch {
fun connect() {
connectAndRun {
checkNotInMainThread()
// Sends everything.
@@ -81,9 +82,16 @@ class Relay(
}
}
@Synchronized
fun requestAndWatch(onConnected: (Relay) -> Unit) {
private var connectingBlock = AtomicBoolean()
fun connectAndRun(onConnected: (Relay) -> Unit) {
// If there is a connection, don't wait.
if (connectingBlock.getAndSet(true)) {
return
}
checkNotInMainThread()
if (socket != null) return
try {
@@ -91,132 +99,143 @@ class Relay(
.header("User-Agent", "Amethyst/${BuildConfig.VERSION_NAME}")
.url(url.trim())
.build()
val listener = object : WebSocketListener() {
override fun onOpen(webSocket: WebSocket, response: Response) {
checkNotInMainThread()
socket = httpClient.newWebSocket(request, RelayListener(onConnected))
} catch (e: Exception) {
errorCounter++
markConnectionAsClosed()
Log.e("Relay", "Relay Invalid $url")
e.printStackTrace()
} finally {
connectingBlock.set(false)
}
}
afterEOSE = false
isReady = true
ping = response.receivedResponseAtMillis - response.sentRequestAtMillis
// Log.w("Relay", "Relay OnOpen, Loading All subscriptions $url")
onConnected(this@Relay)
inner class RelayListener(val onConnected: (Relay) -> Unit) : WebSocketListener() {
override fun onOpen(webSocket: WebSocket, response: Response) {
checkNotInMainThread()
listeners.forEach { it.onRelayStateChange(this@Relay, Type.CONNECT, null) }
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) {
checkNotInMainThread()
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 onMessage(webSocket: WebSocket, text: String) {
checkNotInMainThread()
override fun onClosing(webSocket: WebSocket, code: Int, reason: String) {
checkNotInMainThread()
eventDownloadCounterInBytes += text.bytesUsedInMemory()
listeners.forEach {
it.onRelayStateChange(
this@Relay,
Type.DISCONNECTING,
null
)
}
}
try {
val msg = Event.gson.fromJson(text, JsonElement::class.java).asJsonArray
val type = msg[0].asString
val channel = msg[1].asString
override fun onClosed(webSocket: WebSocket, code: Int, reason: String) {
checkNotInMainThread()
when (type) {
"EVENT" -> {
val event = Event.fromJson(msg[2], Client.lenient)
markConnectionAsClosed()
// 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, ${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 {
// Log.w("Relay", "Relay$url, ${msg[1].asString}")
it.onAuth(this@Relay, msg[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 $text")
)
}
}
} catch (t: Throwable) {
t.printStackTrace()
text.chunked(2000) { chunked ->
listeners.forEach { it.onError(this@Relay, "", Error("Problem with $chunked")) }
}
}
}
listeners.forEach { it.onRelayStateChange(this@Relay, Type.DISCONNECT, null) }
}
override fun onClosing(webSocket: WebSocket, code: Int, reason: String) {
checkNotInMainThread()
override fun onFailure(webSocket: WebSocket, t: Throwable, response: Response?) {
checkNotInMainThread()
listeners.forEach {
it.onRelayStateChange(
this@Relay,
Type.DISCONNECTING,
null
)
}
}
errorCounter++
override fun onClosed(webSocket: WebSocket, code: Int, reason: String) {
checkNotInMainThread()
socket?.close(1000, "Normal close")
// Failures disconnect the relay.
markConnectionAsClosed()
socket = null
isReady = false
afterEOSE = false
closingTime = TimeUtils.now()
listeners.forEach { it.onRelayStateChange(this@Relay, Type.DISCONNECT, null) }
}
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))
}
}
}
override fun onFailure(webSocket: WebSocket, t: Throwable, response: Response?) {
checkNotInMainThread()
fun markConnectionAsReady(pingInMs: Long) {
this.afterEOSE = false
this.isReady = true
this.pingInMs = pingInMs
}
errorCounter++
fun markConnectionAsClosed() {
this.socket = null
this.isReady = false
this.afterEOSE = false
this.closingTimeInSeconds = TimeUtils.now()
}
socket?.close(1000, "Normal close")
// Failures disconnect the relay.
socket = null
isReady = false
afterEOSE = false
closingTime = 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
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))
when (type) {
"EVENT" -> {
val event = Event.fromJson(msgArray[2], Client.lenient)
// Log.w("Relay", "Relay onEVENT $url, $channel")
listeners.forEach {
it.onEvent(this@Relay, channel, event)
if (afterEOSE) {
it.onRelayStateChange(this@Relay, Type.EOSE, channel)
}
}
}
socket = httpClient.newWebSocket(request, listener)
} catch (e: Exception) {
errorCounter++
isReady = false
afterEOSE = false
closingTime = TimeUtils.now()
Log.e("Relay", "Relay Invalid $url")
e.printStackTrace()
"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()
closingTime = TimeUtils.now()
closingTimeInSeconds = TimeUtils.now()
socket?.close(1000, "Normal close")
socket = null
isReady = false
@@ -241,9 +260,9 @@ class Relay(
}
} else {
// waits 60 seconds to reconnect after disconnected.
if (TimeUtils.now() > closingTime + 60) {
if (TimeUtils.now() > closingTimeInSeconds + RECONNECTING_IN_SECONDS) {
// sends all filters after connection is successful.
requestAndWatch()
connect()
}
}
}
@@ -254,9 +273,9 @@ class Relay(
if (socket == null) {
// waits 60 seconds to reconnect after disconnected.
if (TimeUtils.now() > closingTime + 60) {
if (TimeUtils.now() > closingTimeInSeconds + RECONNECTING_IN_SECONDS) {
// println("sendfilter Only if Disconnected ${url} ")
requestAndWatch()
connect()
}
}
}
@@ -24,7 +24,7 @@ object RelayPool : Relay.Listener {
}
fun connectedRelays(): Int {
return relays.filter { it.isConnected() }.size
return relays.count { it.isConnected() }
}
fun getRelay(url: String): Relay? {
@@ -51,7 +51,7 @@ object RelayPool : Relay.Listener {
fun requestAndWatch() {
checkNotInMainThread()
relays.forEach { it.requestAndWatch() }
relays.forEach { it.connect() }
}
fun sendFilter(subscriptionId: String) {
@@ -14,7 +14,6 @@ import androidx.activity.result.contract.ActivityResultContracts
import androidx.annotation.RequiresApi
import androidx.appcompat.app.AppCompatActivity
import androidx.appcompat.app.AppCompatDelegate
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.material.MaterialTheme
import androidx.compose.material.Surface
@@ -94,7 +93,10 @@ class MainActivity : AppCompatActivity() {
var actionableNextPage by remember { mutableStateOf(startingPage) }
actionableNextPage?.let {
LaunchedEffect(it) {
navController.navigate(it)
navController.navigate(it) {
popUpTo(Route.Home.route)
launchSingleTop = true
}
}
actionableNextPage = null
}
@@ -128,24 +130,26 @@ class MainActivity : AppCompatActivity() {
}
override fun onPause() {
ServiceManager.pause()
if (BuildConfig.DEBUG) {
debugState(this)
}
ServiceManager.pause()
super.onPause()
}
override fun onDestroy() {
super.onDestroy()
keepPlayingMutex?.stop()
keepPlayingMutex?.release()
keepPlayingMutex = null
}
/**
* Release memory when the UI becomes hidden or when system resources become low.
* @param level the memory-related event that was raised.
*/
@OptIn(DelicateCoroutinesApi::class)
override fun onTrimMemory(level: Int) {
super.onTrimMemory(level)
println("Trim Memory $level")
@@ -157,15 +161,17 @@ class MainActivity : AppCompatActivity() {
override fun onNewIntent(intent: Intent?) {
super.onNewIntent(intent)
val uri = intent?.data?.toString()
val startingPage = uriToRoute(uri)
if (this::navController.isInitialized) {
val uri = intent?.data?.toString()
val startingPage = uriToRoute(uri)
startingPage?.let { route ->
val currentRoute = getRouteWithArguments(navController)
if (!isSameRoute(currentRoute, route)) {
navController.navigate(route) {
popUpTo(Route.Home.route)
launchSingleTop = true
startingPage?.let { route ->
val currentRoute = getRouteWithArguments(navController)
if (!isSameRoute(currentRoute, route)) {
navController.navigate(route) {
popUpTo(Route.Home.route)
launchSingleTop = true
}
}
}
}
@@ -187,6 +193,7 @@ class MainActivity : AppCompatActivity() {
return false
}
@OptIn(DelicateCoroutinesApi::class)
private val networkCallback = object : ConnectivityManager.NetworkCallback() {
// network is available for use
override fun onAvailable(network: Network) {
@@ -207,14 +214,13 @@ class MainActivity : AppCompatActivity() {
super.onCapabilitiesChanged(network, networkCapabilities)
GlobalScope.launch(Dispatchers.IO) {
val hasMobileData =
networkCapabilities.hasTransport(NetworkCapabilities.TRANSPORT_CELLULAR)
val hasMobileData = networkCapabilities.hasTransport(NetworkCapabilities.TRANSPORT_CELLULAR)
val hasWifi = networkCapabilities.hasTransport(NetworkCapabilities.TRANSPORT_WIFI)
Log.d("NETWORKCALLBACK", "onCapabilitiesChanged: hasMobileData $hasMobileData")
Log.d("NETWORKCALLBACK", "onCapabilitiesChanged: hasWifi $hasWifi")
ConnectivityStatus.updateConnectivityStatus(
networkCapabilities.hasTransport(NetworkCapabilities.TRANSPORT_CELLULAR),
networkCapabilities.hasTransport(NetworkCapabilities.TRANSPORT_WIFI)
hasMobileData,
hasWifi
)
}
}
@@ -224,7 +224,7 @@ class NostrCheckMeServer : FileServer() {
var id = tree?.get("id")?.asText()
var isCompleted = false
val client = OkHttpClient()
val client = HttpClient.getHttpClient()
var requrl = "https://nostrcheck.me/api/v1/media?id=" + id // + "&apikey=26d075787d261660682fb9d20dbffa538c708b1eda921d0efa2be95fbef4910a"
val request = Request.Builder()
@@ -234,14 +234,14 @@ class NostrCheckMeServer : FileServer() {
.build()
while (!isCompleted) {
val response = client.newCall(request).execute()
val body = response.body?.string()
val tree = jacksonObjectMapper().readTree(body)
isCompleted = tree?.get("status")?.asText() == "completed"
try {
Thread.sleep(500)
} catch (e: InterruptedException) {
e.printStackTrace()
client.newCall(request).execute().use {
val tree = jacksonObjectMapper().readTree(it.body.string())
isCompleted = tree?.get("status")?.asText() == "completed"
try {
Thread.sleep(500)
} catch (e: InterruptedException) {
e.printStackTrace()
}
}
}
return url
@@ -4,10 +4,17 @@ import com.vitorpamplona.amethyst.model.HexKey
import com.vitorpamplona.amethyst.model.LocalCache
import com.vitorpamplona.amethyst.model.Note
import com.vitorpamplona.amethyst.model.User
import com.vitorpamplona.amethyst.model.parseDirtyWordForKey
import com.vitorpamplona.amethyst.service.KeyPair
import com.vitorpamplona.amethyst.service.bechToBytes
import com.vitorpamplona.amethyst.service.nip19.Nip19
import com.vitorpamplona.amethyst.service.toNpub
class NewMessageTagger(var channelHex: String?, var mentions: List<User>?, var replyTos: List<Note>?, var message: String) {
class NewMessageTagger(
var message: String,
var mentions: List<User>? = null,
var replyTos: List<Note>? = null,
var channelHex: String? = null
) {
val directMentions = mutableSetOf<HexKey>()
@@ -83,4 +90,55 @@ class NewMessageTagger(var channelHex: String?, var mentions: List<User>?, var r
}.joinToString(" ")
}.joinToString("\n")
}
data class DirtyKeyInfo(val key: Nip19.Return, val restOfWord: String)
fun parseDirtyWordForKey(mightBeAKey: String): DirtyKeyInfo? {
var key = mightBeAKey
if (key.startsWith("nostr:", true)) {
key = key.substring("nostr:".length)
}
key = key.removePrefix("@")
if (key.length < 63) {
return null
}
try {
val keyB32 = key.substring(0, 63)
val restOfWord = key.substring(63)
if (key.startsWith("nsec1", true)) {
// Converts to npub
val pubkey = Nip19.uriToRoute(KeyPair(privKey = keyB32.bechToBytes()).pubKey.toNpub()) ?: return null
return DirtyKeyInfo(pubkey, restOfWord)
} else if (key.startsWith("npub1", true)) {
val pubkey = Nip19.uriToRoute(keyB32) ?: return null
return DirtyKeyInfo(pubkey, restOfWord)
} else if (key.startsWith("note1", true)) {
val noteId = Nip19.uriToRoute(keyB32) ?: return null
return DirtyKeyInfo(noteId, restOfWord)
} else if (key.startsWith("nprofile", true)) {
val pubkeyRelay = Nip19.uriToRoute(keyB32 + restOfWord) ?: return null
return DirtyKeyInfo(pubkeyRelay, pubkeyRelay.additionalChars)
} else if (key.startsWith("nevent1", true)) {
val noteRelayId = Nip19.uriToRoute(keyB32 + restOfWord) ?: return null
return DirtyKeyInfo(noteRelayId, noteRelayId.additionalChars)
} else if (key.startsWith("naddr1", true)) {
val address = Nip19.uriToRoute(keyB32 + restOfWord) ?: return null
return DirtyKeyInfo(address, address.additionalChars) // no way to know when they address ends and dirt begins
}
} catch (e: Exception) {
e.printStackTrace()
}
return null
}
}
@@ -1,5 +1,6 @@
package com.vitorpamplona.amethyst.ui.actions
import android.Manifest
import android.graphics.Bitmap
import android.net.Uri
import android.os.Build
@@ -22,6 +23,8 @@ import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.ArrowForwardIos
import androidx.compose.material.icons.filled.Bolt
import androidx.compose.material.icons.filled.CurrencyBitcoin
import androidx.compose.material.icons.filled.LocationOff
import androidx.compose.material.icons.filled.LocationOn
import androidx.compose.material.icons.filled.ShowChart
import androidx.compose.material.icons.filled.Visibility
import androidx.compose.material.icons.filled.VisibilityOff
@@ -32,6 +35,7 @@ import androidx.compose.runtime.Composable
import androidx.compose.runtime.DisposableEffect
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.Stable
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.livedata.observeAsState
import androidx.compose.runtime.mutableStateOf
@@ -66,10 +70,15 @@ import androidx.compose.ui.window.Dialog
import androidx.compose.ui.window.DialogProperties
import androidx.lifecycle.viewmodel.compose.viewModel
import coil.compose.AsyncImage
import com.fonfon.kgeohash.toGeoHash
import com.google.accompanist.permissions.ExperimentalPermissionsApi
import com.google.accompanist.permissions.isGranted
import com.google.accompanist.permissions.rememberPermissionState
import com.vitorpamplona.amethyst.R
import com.vitorpamplona.amethyst.model.Note
import com.vitorpamplona.amethyst.model.User
import com.vitorpamplona.amethyst.service.NostrSearchEventOrUserDataSource
import com.vitorpamplona.amethyst.service.ReverseGeoLocationUtil
import com.vitorpamplona.amethyst.service.noProtocolUrlValidator
import com.vitorpamplona.amethyst.ui.components.*
import com.vitorpamplona.amethyst.ui.note.CancelIcon
@@ -82,8 +91,9 @@ import com.vitorpamplona.amethyst.ui.screen.loggedIn.TextSpinner
import com.vitorpamplona.amethyst.ui.screen.loggedIn.UserLine
import com.vitorpamplona.amethyst.ui.theme.BitcoinOrange
import com.vitorpamplona.amethyst.ui.theme.ButtonBorder
import com.vitorpamplona.amethyst.ui.theme.Font14SP
import com.vitorpamplona.amethyst.ui.theme.QuoteBorder
import com.vitorpamplona.amethyst.ui.theme.Size10dp
import com.vitorpamplona.amethyst.ui.theme.Size5dp
import com.vitorpamplona.amethyst.ui.theme.mediumImportanceLink
import com.vitorpamplona.amethyst.ui.theme.placeholderText
import com.vitorpamplona.amethyst.ui.theme.replyModifier
@@ -92,6 +102,7 @@ import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.toImmutableList
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
@@ -278,28 +289,72 @@ fun NewPostView(onClose: () -> Unit, baseReplyTo: Note? = null, quote: Note? = n
)
if (postViewModel.wantsPoll) {
postViewModel.pollOptions.values.forEachIndexed { index, _ ->
NewPollOption(postViewModel, index)
}
Button(
onClick = { postViewModel.pollOptions[postViewModel.pollOptions.size] = "" },
border = BorderStroke(1.dp, MaterialTheme.colors.placeholderText),
colors = ButtonDefaults.outlinedButtonColors(
contentColor = MaterialTheme.colors.placeholderText
)
Row(
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier.padding(vertical = Size5dp, horizontal = Size10dp)
) {
Image(
painterResource(id = android.R.drawable.ic_input_add),
contentDescription = "Add poll option button",
modifier = Modifier.size(18.dp)
)
Column(
modifier = Modifier.fillMaxWidth()
) {
postViewModel.pollOptions.values.forEachIndexed { index, _ ->
NewPollOption(postViewModel, index)
}
Button(
onClick = {
postViewModel.pollOptions[postViewModel.pollOptions.size] =
""
},
border = BorderStroke(
1.dp,
MaterialTheme.colors.placeholderText
),
colors = ButtonDefaults.outlinedButtonColors(
contentColor = MaterialTheme.colors.placeholderText
)
) {
Image(
painterResource(id = android.R.drawable.ic_input_add),
contentDescription = "Add poll option button",
modifier = Modifier.size(18.dp)
)
}
}
}
}
if (postViewModel.wantsToMarkAsSensitive) {
Row(
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier
.padding(vertical = Size5dp, horizontal = Size10dp)
) {
ContentSensitivityExplainer(postViewModel)
}
}
if (postViewModel.wantsToAddGeoHash) {
Row(
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier
.padding(vertical = Size5dp, horizontal = Size10dp)
) {
LocationAsHash(postViewModel)
}
}
if (postViewModel.wantsForwardZapTo) {
Row(
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier.padding(vertical = Size5dp, horizontal = Size10dp)
) {
FowardZapTo(postViewModel)
}
}
val url = postViewModel.contentToAddUrl
if (url != null) {
Row(verticalAlignment = Alignment.CenterVertically) {
Row(verticalAlignment = Alignment.CenterVertically, modifier = Modifier.padding(vertical = Size5dp, horizontal = Size10dp)) {
ImageVideoDescription(
url,
account.defaultFileServer,
@@ -324,26 +379,28 @@ fun NewPostView(onClose: () -> Unit, baseReplyTo: Note? = null, quote: Note? = n
val lud16 = user?.info?.lnAddress()
if (lud16 != null && postViewModel.wantsInvoice) {
Row(verticalAlignment = Alignment.CenterVertically, modifier = Modifier.padding(vertical = 5.dp)) {
InvoiceRequest(
lud16,
user.pubkeyHex,
account,
stringResource(id = R.string.lightning_invoice),
stringResource(id = R.string.lightning_create_and_add_invoice),
onSuccess = {
postViewModel.message = TextFieldValue(postViewModel.message.text + "\n\n" + it)
postViewModel.wantsInvoice = false
},
onClose = {
postViewModel.wantsInvoice = false
}
)
Row(verticalAlignment = Alignment.CenterVertically, modifier = Modifier.padding(vertical = Size5dp, horizontal = Size10dp)) {
Column(Modifier.fillMaxWidth()) {
InvoiceRequest(
lud16,
user.pubkeyHex,
account,
stringResource(id = R.string.lightning_invoice),
stringResource(id = R.string.lightning_create_and_add_invoice),
onSuccess = {
postViewModel.message = TextFieldValue(postViewModel.message.text + "\n\n" + it)
postViewModel.wantsInvoice = false
},
onClose = {
postViewModel.wantsInvoice = false
}
)
}
}
}
if (lud16 != null && postViewModel.wantsZapraiser) {
Row(verticalAlignment = Alignment.CenterVertically, modifier = Modifier.padding(vertical = 5.dp)) {
Row(verticalAlignment = Alignment.CenterVertically, modifier = Modifier.padding(vertical = Size5dp, horizontal = Size10dp)) {
ZapRaiserRequest(
stringResource(id = R.string.zapraiser),
postViewModel
@@ -353,7 +410,7 @@ fun NewPostView(onClose: () -> Unit, baseReplyTo: Note? = null, quote: Note? = n
val myUrlPreview = postViewModel.urlPreview
if (myUrlPreview != null) {
Row(modifier = Modifier.padding(top = 5.dp)) {
Row(modifier = Modifier.padding(vertical = Size5dp, horizontal = Size10dp)) {
if (isValidURL(myUrlPreview)) {
val removedParamsFromUrl =
myUrlPreview.split("?")[0].lowercase()
@@ -456,6 +513,10 @@ fun NewPostView(onClose: () -> Unit, baseReplyTo: Note? = null, quote: Note? = n
postViewModel.wantsToMarkAsSensitive = !postViewModel.wantsToMarkAsSensitive
}
AddGeoHash(postViewModel) {
postViewModel.wantsToAddGeoHash = !postViewModel.wantsToAddGeoHash
}
ForwardZapTo(postViewModel) {
postViewModel.wantsForwardZapTo = !postViewModel.wantsForwardZapTo
}
@@ -466,6 +527,227 @@ fun NewPostView(onClose: () -> Unit, baseReplyTo: Note? = null, quote: Note? = n
}
}
@Composable
fun ContentSensitivityExplainer(postViewModel: NewPostViewModel) {
Column(
modifier = Modifier.fillMaxWidth()
) {
Row(
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier
.fillMaxWidth()
.padding(bottom = 10.dp)
) {
Box(
Modifier
.height(20.dp)
.width(25.dp)
) {
Icon(
imageVector = Icons.Default.VisibilityOff,
contentDescription = stringResource(id = R.string.content_warning),
modifier = Modifier
.size(18.dp)
.align(Alignment.BottomStart),
tint = Color.Red
)
Icon(
imageVector = Icons.Rounded.Warning,
contentDescription = stringResource(id = R.string.content_warning),
modifier = Modifier
.size(10.dp)
.align(Alignment.TopEnd),
tint = Color.Yellow
)
}
Text(
text = stringResource(R.string.add_sensitive_content_label),
fontSize = 20.sp,
fontWeight = FontWeight.W500,
modifier = Modifier.padding(start = 10.dp)
)
}
Divider()
Text(
text = stringResource(R.string.add_sensitive_content_explainer),
color = MaterialTheme.colors.placeholderText,
modifier = Modifier.padding(vertical = 10.dp)
)
}
}
@Composable
fun FowardZapTo(postViewModel: NewPostViewModel) {
Column(
modifier = Modifier.fillMaxWidth()
) {
Row(
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier
.fillMaxWidth()
.padding(bottom = 10.dp)
) {
Box(
Modifier
.height(20.dp)
.width(25.dp)
) {
Icon(
imageVector = Icons.Outlined.Bolt,
contentDescription = stringResource(id = R.string.zaps),
modifier = Modifier
.size(20.dp)
.align(Alignment.CenterStart),
tint = BitcoinOrange
)
Icon(
imageVector = Icons.Outlined.ArrowForwardIos,
contentDescription = stringResource(id = R.string.zaps),
modifier = Modifier
.size(13.dp)
.align(Alignment.CenterEnd),
tint = BitcoinOrange
)
}
Text(
text = stringResource(R.string.zap_forward_title),
fontSize = 20.sp,
fontWeight = FontWeight.W500,
modifier = Modifier.padding(start = 10.dp)
)
}
Divider()
Text(
text = stringResource(R.string.zap_forward_explainer),
color = MaterialTheme.colors.placeholderText,
modifier = Modifier.padding(vertical = 10.dp)
)
OutlinedTextField(
value = postViewModel.forwardZapToEditting,
onValueChange = {
postViewModel.updateZapForwardTo(it)
},
label = { Text(text = stringResource(R.string.zap_forward_lnAddress)) },
modifier = Modifier.fillMaxWidth(),
placeholder = {
Text(
text = stringResource(R.string.zap_forward_lnAddress),
color = MaterialTheme.colors.placeholderText
)
},
singleLine = true,
visualTransformation = UrlUserTagTransformation(
MaterialTheme.colors.primary
),
textStyle = LocalTextStyle.current.copy(textDirection = TextDirection.Content)
)
}
}
@OptIn(ExperimentalPermissionsApi::class)
@Composable
fun LocationAsHash(postViewModel: NewPostViewModel) {
val context = LocalContext.current
val locationPermissionState = rememberPermissionState(
Manifest.permission.ACCESS_COARSE_LOCATION
)
if (locationPermissionState.status.isGranted) {
var locationDescriptionFlow by remember(postViewModel) {
mutableStateOf<Flow<String>?>(null)
}
DisposableEffect(key1 = Unit) {
postViewModel.startLocation(context = context)
locationDescriptionFlow = postViewModel.location
onDispose {
postViewModel.stopLocation()
}
}
Column(
modifier = Modifier.fillMaxWidth()
) {
Row(
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier
.fillMaxWidth()
.padding(bottom = 10.dp)
) {
Box(
Modifier
.height(20.dp)
.width(20.dp)
) {
Icon(
imageVector = Icons.Default.LocationOn,
null,
modifier = Modifier.size(20.dp),
tint = MaterialTheme.colors.primary
)
}
Text(
text = stringResource(R.string.geohash_title),
fontSize = 20.sp,
fontWeight = FontWeight.W500,
modifier = Modifier.padding(start = 10.dp)
)
locationDescriptionFlow?.let { geoLocation ->
DisplayLocationObserver(geoLocation)
}
}
Divider()
Text(
text = stringResource(R.string.geohash_explainer),
color = MaterialTheme.colors.placeholderText,
modifier = Modifier.padding(vertical = 10.dp)
)
}
} else {
LaunchedEffect(locationPermissionState) {
locationPermissionState.launchPermissionRequest()
}
}
}
@Composable
fun DisplayLocationObserver(geoLocation: Flow<String>) {
val location by geoLocation.collectAsState(initial = null)
location?.let {
DisplayLocationInTitle(geohash = it)
}
}
@Composable
fun DisplayLocationInTitle(geohash: String) {
val context = LocalContext.current
val cityName = remember(geohash) {
ReverseGeoLocationUtil().execute(geohash.toGeoHash().toLocation(), context)
}
Text(
text = cityName ?: geohash,
fontSize = 20.sp,
fontWeight = FontWeight.W500,
modifier = Modifier.padding(start = Size5dp)
)
}
@OptIn(ExperimentalLayoutApi::class)
@Composable
fun Notifying(baseMentions: ImmutableList<User>?, onClick: (User) -> Unit) {
@@ -582,6 +864,31 @@ private fun AddZapraiserButton(
}
}
@Composable
fun AddGeoHash(postViewModel: NewPostViewModel, onClick: () -> Unit) {
IconButton(
onClick = {
onClick()
}
) {
if (!postViewModel.wantsToAddGeoHash) {
Icon(
imageVector = Icons.Default.LocationOff,
null,
modifier = Modifier.size(20.dp),
tint = MaterialTheme.colors.onBackground
)
} else {
Icon(
imageVector = Icons.Default.LocationOn,
null,
modifier = Modifier.size(20.dp),
tint = MaterialTheme.colors.primary
)
}
}
}
@Composable
private fun AddLnInvoiceButton(
isLnInvoiceActive: Boolean,
@@ -662,33 +969,6 @@ private fun ForwardZapTo(
}
}
}
if (postViewModel.wantsForwardZapTo) {
OutlinedTextField(
value = postViewModel.forwardZapToEditting,
onValueChange = {
postViewModel.updateZapForwardTo(it)
},
modifier = Modifier
.fillMaxWidth()
.windowInsetsPadding(WindowInsets(0.dp, 0.dp, 0.dp, 0.dp))
.padding(0.dp),
placeholder = {
Text(
text = stringResource(R.string.zap_forward_lnAddress),
color = MaterialTheme.colors.placeholderText,
fontSize = Font14SP
)
},
colors = TextFieldDefaults
.outlinedTextFieldColors(
unfocusedBorderColor = Color.Transparent,
focusedBorderColor = Color.Transparent
),
visualTransformation = UrlUserTagTransformation(MaterialTheme.colors.primary),
textStyle = LocalTextStyle.current.copy(textDirection = TextDirection.Content)
)
}
}
@Composable
@@ -913,7 +1193,9 @@ fun ImageVideoDescription(
)
IconButton(
modifier = Modifier.size(30.dp).padding(end = 5.dp),
modifier = Modifier
.size(30.dp)
.padding(end = 5.dp),
onClick = onCancel
) {
CancelIcon()
@@ -13,8 +13,10 @@ import androidx.compose.ui.text.TextRange
import androidx.compose.ui.text.input.TextFieldValue
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.fonfon.kgeohash.toGeoHash
import com.vitorpamplona.amethyst.model.*
import com.vitorpamplona.amethyst.service.FileHeader
import com.vitorpamplona.amethyst.service.LocationUtil
import com.vitorpamplona.amethyst.service.NostrSearchEventOrUserDataSource
import com.vitorpamplona.amethyst.service.model.AddressableEvent
import com.vitorpamplona.amethyst.service.model.BaseTextNoteEvent
@@ -26,7 +28,9 @@ import com.vitorpamplona.amethyst.service.relays.Relay
import com.vitorpamplona.amethyst.ui.components.MediaCompressor
import com.vitorpamplona.amethyst.ui.components.isValidURL
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.mapLatest
import kotlinx.coroutines.launch
@Stable
@@ -77,6 +81,11 @@ open class NewPostViewModel() : ViewModel() {
// NSFW, Sensitive
var wantsToMarkAsSensitive by mutableStateOf(false)
// GeoHash
var wantsToAddGeoHash by mutableStateOf(false)
var locUtil: LocationUtil? = null
var location: Flow<String>? = null
// ZapRaiser
var canAddZapRaiser by mutableStateOf(false)
var wantsZapraiser by mutableStateOf(false)
@@ -121,6 +130,7 @@ open class NewPostViewModel() : ViewModel() {
wantsForwardZapTo = false
wantsToMarkAsSensitive = false
wantsToAddGeoHash = false
wantsZapraiser = false
zapRaiserAmount = null
forwardZapTo = null
@@ -130,7 +140,7 @@ open class NewPostViewModel() : ViewModel() {
}
fun sendPost(relayList: List<Relay>? = null) {
val tagger = NewMessageTagger(originalNote?.channelHex(), mentions, replyTos, message.text)
val tagger = NewMessageTagger(message.text, mentions, replyTos, originalNote?.channelHex())
tagger.run()
val zapReceiver = if (wantsForwardZapTo) {
@@ -143,6 +153,13 @@ open class NewPostViewModel() : ViewModel() {
null
}
val geoLocation = locUtil?.locationStateFlow?.value
val geoHash = if (wantsToAddGeoHash && geoLocation != null) {
geoLocation.toGeoHash(GeohashPrecision.KM_5_X_5.digits).toString()
} else {
null
}
val localZapRaiserAmount = if (wantsZapraiser) zapRaiserAmount else null
if (wantsPoll) {
@@ -158,16 +175,17 @@ open class NewPostViewModel() : ViewModel() {
zapReceiver,
wantsToMarkAsSensitive,
localZapRaiserAmount,
relayList
relayList,
geoHash
)
} else if (originalNote?.channelHex() != null) {
if (originalNote is AddressableEvent && originalNote?.address() != null) {
account?.sendLiveMessage(tagger.message, originalNote?.address()!!, tagger.replyTos, tagger.mentions, zapReceiver, wantsToMarkAsSensitive, localZapRaiserAmount)
account?.sendLiveMessage(tagger.message, originalNote?.address()!!, tagger.replyTos, tagger.mentions, zapReceiver, wantsToMarkAsSensitive, localZapRaiserAmount, geoHash)
} else {
account?.sendChannelMessage(tagger.message, tagger.channelHex!!, tagger.replyTos, tagger.mentions, zapReceiver, wantsToMarkAsSensitive, localZapRaiserAmount)
account?.sendChannelMessage(tagger.message, tagger.channelHex!!, tagger.replyTos, tagger.mentions, zapReceiver, wantsToMarkAsSensitive, localZapRaiserAmount, geoHash)
}
} else if (originalNote?.event is PrivateDmEvent) {
account?.sendPrivateMessage(tagger.message, originalNote!!.author!!, originalNote!!, tagger.mentions, zapReceiver, wantsToMarkAsSensitive, localZapRaiserAmount)
account?.sendPrivateMessage(tagger.message, originalNote!!.author!!, originalNote!!, tagger.mentions, zapReceiver, wantsToMarkAsSensitive, localZapRaiserAmount, geoHash)
} else {
// adds markers
val rootId =
@@ -187,7 +205,8 @@ open class NewPostViewModel() : ViewModel() {
replyingTo = replyId,
root = rootId,
directMentions = tagger.directMentions,
relayList = relayList
relayList = relayList,
geohash = geoHash
)
}
@@ -267,6 +286,7 @@ open class NewPostViewModel() : ViewModel() {
wantsForwardZapTo = false
wantsToMarkAsSensitive = false
wantsToAddGeoHash = false
forwardZapTo = null
forwardZapToEditting = TextFieldValue("")
@@ -449,4 +469,50 @@ open class NewPostViewModel() : ViewModel() {
fun selectImage(uri: Uri) {
contentToAddUrl = uri
}
fun startLocation(context: Context) {
locUtil = LocationUtil(context)
locUtil?.let {
location = it.locationStateFlow.mapLatest {
it.toGeoHash(GeohashPrecision.KM_5_X_5.digits).toString()
}
}
viewModelScope.launch(Dispatchers.IO) {
locUtil?.start()
}
}
fun stopLocation() {
viewModelScope.launch(Dispatchers.IO) {
locUtil?.stop()
}
location = null
locUtil = null
}
override fun onCleared() {
super.onCleared()
viewModelScope.launch(Dispatchers.IO) {
locUtil?.stop()
}
location = null
locUtil = null
}
}
enum class GeohashPrecision(val digits: Int) {
KM_5000_X_5000(1), // 5,000km × 5,000km
KM_1250_X_625(2), // 1,250km × 625km
KM_156_X_156(3), // 156km × 156km
KM_39_X_19(4), // 39.1km × 19.5km
KM_5_X_5(5), // 4.89km × 4.89km
M_1000_X_600(6), // 1.22km × 0.61km
M_153_X_153(7), // 153m × 153m
M_38_X_19(8), // 38.2m × 19.1m
M_5_X_5(9), // 4.77m × 4.77m
MM_1000_X_1000(10), // 1.19m × 0.596m
MM_149_X_149(11), // 149mm × 149mm
MM_37_X_18(12) // 37.2mm × 18.6mm
}
@@ -7,11 +7,12 @@ import androidx.compose.foundation.clickable
import androidx.compose.foundation.combinedClickable
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.ExperimentalLayoutApi
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.imePadding
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.lazy.LazyColumn
@@ -58,16 +59,19 @@ import androidx.lifecycle.viewmodel.compose.viewModel
import com.vitorpamplona.amethyst.R
import com.vitorpamplona.amethyst.model.RelayInformation
import com.vitorpamplona.amethyst.model.RelaySetupInfo
import com.vitorpamplona.amethyst.service.relays.Constants.defaultRelays
import com.vitorpamplona.amethyst.service.relays.FeedType
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
import com.vitorpamplona.amethyst.ui.theme.ButtonBorder
import com.vitorpamplona.amethyst.ui.theme.Font14SP
import com.vitorpamplona.amethyst.ui.theme.Size35dp
import com.vitorpamplona.amethyst.ui.theme.StdVertSpacer
import com.vitorpamplona.amethyst.ui.theme.placeholderText
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.launch
import java.lang.Math.round
@OptIn(ExperimentalLayoutApi::class)
@Composable
fun NewRelayListView(onClose: () -> Unit, accountViewModel: AccountViewModel, relayToAdd: String = "", nav: (String) -> Unit) {
val postViewModel: NewRelayListViewModel = viewModel()
@@ -87,11 +91,12 @@ fun NewRelayListView(onClose: () -> Unit, accountViewModel: AccountViewModel, re
Dialog(
onDismissRequest = { onClose() },
properties = DialogProperties(
decorFitsSystemWindows = false,
usePlatformDefaultWidth = false,
dismissOnClickOutside = false
)
) {
Surface() {
Surface(modifier = Modifier.imePadding()) {
Column(
modifier = Modifier.padding(10.dp)
) {
@@ -105,6 +110,22 @@ fun NewRelayListView(onClose: () -> Unit, accountViewModel: AccountViewModel, re
onClose()
})
Button(
onClick = {
postViewModel.deleteAll()
defaultRelays.forEach {
postViewModel.addRelay(it)
}
postViewModel.relays.value.forEach { item ->
loadRelayInfo(item.url, context, scope) {
postViewModel.togglePaidRelay(item, it.limitation?.payment_required ?: false)
}
}
}
) {
Text(stringResource(R.string.default_relays))
}
PostButton(
onPost = {
postViewModel.create()
@@ -114,7 +135,7 @@ fun NewRelayListView(onClose: () -> Unit, accountViewModel: AccountViewModel, re
)
}
Spacer(modifier = Modifier.height(10.dp))
Spacer(modifier = StdVertSpacer)
Row(modifier = Modifier.weight(1f), verticalAlignment = Alignment.CenterVertically) {
LazyColumn(
@@ -149,7 +170,7 @@ fun NewRelayListView(onClose: () -> Unit, accountViewModel: AccountViewModel, re
}
}
Spacer(modifier = Modifier.height(10.dp))
Spacer(modifier = StdVertSpacer)
EditableServerConfig(relayToAdd) {
postViewModel.addRelay(it)
@@ -90,6 +90,12 @@ class NewRelayListViewModel : ViewModel() {
}
}
fun deleteAll() {
_relays.update { relays ->
relays.dropWhile { relays.isNotEmpty() }
}
}
fun toggleDownload(relay: RelaySetupInfo) {
_relays.update {
it.updated(relay, relay.copy(read = !relay.read))
@@ -24,9 +24,11 @@ import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import androidx.compose.ui.window.Dialog
import androidx.compose.ui.window.DialogProperties
import com.vitorpamplona.amethyst.R
import com.vitorpamplona.amethyst.model.RelayInformation
import com.vitorpamplona.amethyst.service.relays.Relay
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
@@ -37,7 +39,6 @@ data class RelayList(
val isSelected: Boolean
)
@OptIn(ExperimentalFoundationApi::class)
@Composable
fun RelaySelectionDialog(
list: List<Relay>,
@@ -79,6 +80,10 @@ fun RelaySelectionDialog(
)
}
var selected by remember {
mutableStateOf(true)
}
Dialog(
onDismissRequest = { onClose() },
properties = DialogProperties(
@@ -126,6 +131,17 @@ fun RelaySelectionDialog(
)
}
RelaySwitch(
text = stringResource(R.string.select_deselect_all),
checked = selected,
onClick = {
selected = !selected
relays = relays.mapIndexed { _, item ->
item.copy(isSelected = selected)
}
}
)
LazyColumn(
contentPadding = PaddingValues(
top = 10.dp,
@@ -136,48 +152,55 @@ fun RelaySelectionDialog(
relays,
key = { _, item -> item.relay.url }
) { index, item ->
Row(
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier
.fillMaxWidth()
.combinedClickable(
onClick = {
relays = relays.mapIndexed { j, item ->
if (index == j) {
item.copy(isSelected = !item.isSelected)
} else {
item
}
}
},
onLongClick = {
loadRelayInfo(item.relay.url, context, scope) {
relayInfo = it
}
}
)
) {
Text(
item.relay.url
.removePrefix("ws://")
.removePrefix("wss://")
.removeSuffix("/")
)
Switch(
checked = item.isSelected,
onCheckedChange = {
relays = relays.mapIndexed { j, item ->
if (index == j) {
item.copy(isSelected = !item.isSelected)
} else { 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
}
}
)
}
},
onLongPress = {
loadRelayInfo(item.relay.url, context, scope) {
relayInfo = it
}
}
)
}
}
}
}
}
}
@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()
}
)
}
}
@@ -42,7 +42,7 @@ import com.vitorpamplona.amethyst.ui.theme.subtleBorder
import kotlinx.coroutines.launch
@Composable
fun InvoiceRequest(
fun InvoiceRequestCard(
lud16: String,
toUserPubKeyHex: String,
account: Account,
@@ -51,9 +51,6 @@ fun InvoiceRequest(
onSuccess: (String) -> Unit,
onClose: () -> Unit
) {
val context = LocalContext.current
val scope = rememberCoroutineScope()
Column(
modifier = Modifier
.fillMaxWidth()
@@ -66,102 +63,118 @@ fun InvoiceRequest(
.fillMaxWidth()
.padding(30.dp)
) {
Row(
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier
.fillMaxWidth()
.padding(bottom = 10.dp)
) {
Icon(
painter = painterResource(R.drawable.lightning),
null,
modifier = Modifier.size(20.dp),
tint = Color.Unspecified
)
Text(
text = titleText ?: stringResource(R.string.lightning_tips),
fontSize = 20.sp,
fontWeight = FontWeight.W500,
modifier = Modifier.padding(start = 10.dp)
)
}
Divider()
var message by remember { mutableStateOf("") }
var amount by remember { mutableStateOf(1000L) }
OutlinedTextField(
label = { Text(text = stringResource(R.string.note_to_receiver)) },
modifier = Modifier.fillMaxWidth(),
value = message,
onValueChange = { message = it },
placeholder = {
Text(
text = stringResource(R.string.thank_you_so_much),
color = MaterialTheme.colors.placeholderText
)
},
keyboardOptions = KeyboardOptions.Default.copy(
capitalization = KeyboardCapitalization.Sentences
),
singleLine = true
)
OutlinedTextField(
label = { Text(text = stringResource(R.string.amount_in_sats)) },
modifier = Modifier.fillMaxWidth(),
value = amount.toString(),
onValueChange = {
runCatching {
if (it.isEmpty()) {
amount = 0
} else {
amount = it.toLong()
}
}
},
placeholder = {
Text(
text = "1000",
color = MaterialTheme.colors.placeholderText
)
},
keyboardOptions = KeyboardOptions.Default.copy(
keyboardType = KeyboardType.Number
),
singleLine = true
)
Button(
modifier = Modifier.fillMaxWidth().padding(vertical = 10.dp),
onClick = {
val zapRequest = account.createZapRequestFor(toUserPubKeyHex, message, account.defaultZapType)
LightningAddressResolver().lnAddressInvoice(
lud16,
amount * 1000,
message,
zapRequest?.toJson(),
onSuccess = onSuccess,
onError = {
scope.launch {
Toast.makeText(context, it, Toast.LENGTH_SHORT).show()
onClose()
}
},
onProgress = {
}
)
},
shape = QuoteBorder,
colors = ButtonDefaults.buttonColors(
backgroundColor = MaterialTheme.colors.primary
)
) {
Text(text = buttonText ?: stringResource(R.string.send_sats), color = Color.White, fontSize = 20.sp)
}
InvoiceRequest(lud16, toUserPubKeyHex, account, titleText, buttonText, onSuccess, onClose)
}
}
}
@Composable
fun InvoiceRequest(
lud16: String,
toUserPubKeyHex: String,
account: Account,
titleText: String? = null,
buttonText: String? = null,
onSuccess: (String) -> Unit,
onClose: () -> Unit
) {
val context = LocalContext.current
val scope = rememberCoroutineScope()
Row(
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier
.fillMaxWidth()
.padding(bottom = 10.dp)
) {
Icon(
painter = painterResource(R.drawable.lightning),
null,
modifier = Modifier.size(20.dp),
tint = Color.Unspecified
)
Text(
text = titleText ?: stringResource(R.string.lightning_tips),
fontSize = 20.sp,
fontWeight = FontWeight.W500,
modifier = Modifier.padding(start = 10.dp)
)
}
Divider()
var message by remember { mutableStateOf("") }
var amount by remember { mutableStateOf(1000L) }
OutlinedTextField(
label = { Text(text = stringResource(R.string.note_to_receiver)) },
modifier = Modifier.fillMaxWidth(),
value = message,
onValueChange = { message = it },
placeholder = {
Text(
text = stringResource(R.string.thank_you_so_much),
color = MaterialTheme.colors.placeholderText
)
},
keyboardOptions = KeyboardOptions.Default.copy(
capitalization = KeyboardCapitalization.Sentences
),
singleLine = true
)
OutlinedTextField(
label = { Text(text = stringResource(R.string.amount_in_sats)) },
modifier = Modifier.fillMaxWidth(),
value = amount.toString(),
onValueChange = {
runCatching {
if (it.isEmpty()) {
amount = 0
} else {
amount = it.toLong()
}
}
},
placeholder = {
Text(
text = "1000",
color = MaterialTheme.colors.placeholderText
)
},
keyboardOptions = KeyboardOptions.Default.copy(
keyboardType = KeyboardType.Number
),
singleLine = true
)
Button(
modifier = Modifier.fillMaxWidth().padding(vertical = 10.dp),
onClick = {
val zapRequest = account.createZapRequestFor(toUserPubKeyHex, message, account.defaultZapType)
LightningAddressResolver().lnAddressInvoice(
lud16,
amount * 1000,
message,
zapRequest?.toJson(),
onSuccess = onSuccess,
onError = {
scope.launch {
Toast.makeText(context, it, Toast.LENGTH_SHORT).show()
onClose()
}
},
onProgress = {
}
)
},
shape = QuoteBorder,
colors = ButtonDefaults.buttonColors(
backgroundColor = MaterialTheme.colors.primary
)
) {
Text(text = buttonText ?: stringResource(R.string.send_sats), color = Color.White, fontSize = 20.sp)
}
}
@@ -64,6 +64,7 @@ import com.vitorpamplona.amethyst.ui.note.NoteCompose
import com.vitorpamplona.amethyst.ui.note.toShortenHex
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
import com.vitorpamplona.amethyst.ui.theme.Font17SP
import com.vitorpamplona.amethyst.ui.theme.HalfVertPadding
import com.vitorpamplona.amethyst.ui.theme.MarkdownTextStyle
import com.vitorpamplona.amethyst.ui.theme.innerPostModifier
import com.vitorpamplona.amethyst.ui.theme.markdownStyle
@@ -285,7 +286,9 @@ private fun ZoomableContentView(
accountViewModel: AccountViewModel
) {
state.imagesForPager[word]?.let {
ZoomableContentView(it, state.imageList, accountViewModel)
Box(modifier = HalfVertPadding) {
ZoomableContentView(it, state.imageList, accountViewModel)
}
}
}
@@ -13,9 +13,9 @@ import coil.fetch.Fetcher
import coil.fetch.SourceResult
import coil.request.ImageRequest
import coil.request.Options
import com.vitorpamplona.amethyst.service.CryptoUtils
import com.vitorpamplona.amethyst.service.checkNotInMainThread
import okio.Buffer
import java.security.MessageDigest
private fun toHex(color: Color): String {
val argb = color.toArgb()
@@ -35,7 +35,7 @@ private fun bytesToRGB(b1: Byte, b2: Byte, b3: Byte): Color {
private fun svgString(msg: String): String {
checkNotInMainThread()
val hash = MessageDigest.getInstance("SHA-256").digest(msg.toByteArray())
val hash = CryptoUtils.sha256(msg.toByteArray())
val hashHex = hash.joinToString(separator = "") { b -> "%02x".format(b) }
val bgColor = bytesToRGB(hash[0], hash[1], hash[2])
val fgColor = bytesToRGB(hash[3], hash[4], hash[5])
@@ -8,6 +8,7 @@ import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import com.vitorpamplona.amethyst.model.ConnectivityType
import com.vitorpamplona.amethyst.model.UrlCachedPreviewer
import com.vitorpamplona.amethyst.service.connectivitystatus.ConnectivityStatus
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
@@ -16,13 +17,12 @@ import kotlinx.coroutines.launch
@Composable
fun UrlPreview(url: String, urlText: String, accountViewModel: AccountViewModel) {
val settings = accountViewModel.account.settings
val isMobile = ConnectivityStatus.isOnMobileData.value
val automaticallyShowUrlPreview = when (settings.automaticallyShowUrlPreview) {
true -> !isMobile
false -> false
else -> true
val automaticallyShowUrlPreview = remember {
when (accountViewModel.account.settings.automaticallyShowUrlPreview) {
ConnectivityType.WIFI_ONLY -> !ConnectivityStatus.isOnMobileData.value
ConnectivityType.NEVER -> false
ConnectivityType.ALWAYS -> true
}
}
if (!automaticallyShowUrlPreview) {
@@ -1,6 +1,5 @@
package com.vitorpamplona.amethyst.ui.components
import androidx.compose.foundation.border
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
@@ -9,8 +8,8 @@ import androidx.compose.foundation.layout.padding
import androidx.compose.material.MaterialTheme
import androidx.compose.material.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.platform.LocalUriHandler
@@ -19,11 +18,11 @@ import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp
import coil.compose.AsyncImage
import com.vitorpamplona.amethyst.R
import com.vitorpamplona.amethyst.model.ConnectivityType
import com.vitorpamplona.amethyst.service.connectivitystatus.ConnectivityStatus
import com.vitorpamplona.amethyst.service.previews.UrlInfoItem
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
import com.vitorpamplona.amethyst.ui.theme.QuoteBorder
import com.vitorpamplona.amethyst.ui.theme.subtleBorder
import com.vitorpamplona.amethyst.ui.theme.innerPostModifier
@Composable
fun UrlPreviewCard(
@@ -31,13 +30,12 @@ fun UrlPreviewCard(
previewInfo: UrlInfoItem,
accountViewModel: AccountViewModel
) {
val settings = accountViewModel.account.settings
val isMobile = ConnectivityStatus.isOnMobileData.value
val automaticallyShowUrlPreview = when (settings.automaticallyShowUrlPreview) {
true -> !isMobile
false -> false
else -> true
val automaticallyShowUrlPreview = remember {
when (accountViewModel.account.settings.automaticallyShowUrlPreview) {
ConnectivityType.WIFI_ONLY -> !ConnectivityStatus.isOnMobileData.value
ConnectivityType.NEVER -> false
ConnectivityType.ALWAYS -> true
}
}
if (!automaticallyShowUrlPreview) {
@@ -46,14 +44,10 @@ fun UrlPreviewCard(
val uri = LocalUriHandler.current
Row(
modifier = Modifier
.clickable { runCatching { uri.openUri(url) } }
.clip(shape = QuoteBorder)
.border(
1.dp,
MaterialTheme.colors.subtleBorder,
QuoteBorder
)
modifier = MaterialTheme.colors.innerPostModifier
.clickable {
runCatching { uri.openUri(url) }
}
) {
Column {
AsyncImage(
@@ -34,7 +34,6 @@ import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.composed
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.layout.LayoutCoordinates
import androidx.compose.ui.layout.boundsInWindow
import androidx.compose.ui.layout.onGloballyPositioned
@@ -56,6 +55,7 @@ import androidx.media3.ui.PlayerView
import coil.imageLoader
import coil.request.ImageRequest
import com.vitorpamplona.amethyst.PlaybackClientController
import com.vitorpamplona.amethyst.model.ConnectivityType
import com.vitorpamplona.amethyst.service.connectivitystatus.ConnectivityStatus
import com.vitorpamplona.amethyst.ui.note.LyricsIcon
import com.vitorpamplona.amethyst.ui.note.LyricsOffIcon
@@ -174,16 +174,13 @@ fun VideoViewInner(
accountViewModel: AccountViewModel,
onDialog: ((Boolean) -> Unit)? = null
) {
val settings = accountViewModel.account.settings
val isMobile = ConnectivityStatus.isOnMobileData.value
val automaticallyStartPlayback = remember {
mutableStateOf(
if (alwaysShowVideo) { true } else {
when (settings.automaticallyStartPlayback) {
true -> !isMobile
false -> false
else -> true
when (accountViewModel.account.settings.automaticallyStartPlayback) {
ConnectivityType.WIFI_ONLY -> !ConnectivityStatus.isOnMobileData.value
ConnectivityType.NEVER -> false
ConnectivityType.ALWAYS -> true
}
}
)
@@ -192,7 +189,7 @@ fun VideoViewInner(
if (!automaticallyStartPlayback.value) {
ImageUrlWithDownloadButton(url = videoUri, showImage = automaticallyStartPlayback)
} else {
VideoPlayerMutex(videoUri) { activeOnScreen ->
VideoPlayerActiveMutex(videoUri) { activeOnScreen ->
val mediaItem = remember(videoUri) {
MediaItem.Builder()
.setMediaId(videoUri)
@@ -230,7 +227,7 @@ fun VideoViewInner(
}
@Composable
@androidx.annotation.OptIn(androidx.media3.common.util.UnstableApi::class)
@OptIn(UnstableApi::class)
fun GetVideoController(
mediaItem: MediaItem,
videoUri: String,
@@ -327,10 +324,6 @@ fun GetVideoController(
}
}
controller.value?.let {
inner(it, keepPlaying)
}
// User pauses and resumes the app. What to do with videos?
val scope = rememberCoroutineScope()
val lifeCycleOwner = LocalLifecycleOwner.current
@@ -395,6 +388,10 @@ fun GetVideoController(
}
}
}
controller.value?.let {
inner(it, keepPlaying)
}
}
// background playing mutex.
@@ -413,7 +410,7 @@ class VisibilityData() {
* the screen wins the mutex.
*/
@Composable
fun VideoPlayerMutex(videoUri: String, inner: @Composable (MutableState<Boolean>) -> Unit) {
fun VideoPlayerActiveMutex(videoUri: String, inner: @Composable (MutableState<Boolean>) -> Unit) {
val myCache = remember(videoUri) {
VisibilityData()
}
@@ -532,6 +529,7 @@ private fun RenderVideoPlayer(
// if the user unmutes a video and it's not the current playing, switches to that one.
if (!mute && keepPlayingMutex != null && keepPlayingMutex != controller) {
keepPlayingMutex?.stop()
keepPlayingMutex?.release()
keepPlayingMutex = null
}
@@ -543,6 +541,7 @@ private fun RenderVideoPlayer(
if (newKeepPlaying) {
if (keepPlayingMutex != null && keepPlayingMutex != controller) {
keepPlayingMutex?.stop()
keepPlayingMutex?.release()
}
keepPlayingMutex = controller
} else {
@@ -608,38 +607,6 @@ fun ControlWhenPlayerIsActive(
}
}
fun Modifier.onVisibilityChanges(onVisibilityChanges: (Boolean) -> Unit): Modifier = composed {
val view = LocalView.current
var isVisible: Boolean? by remember { mutableStateOf(null) }
onGloballyPositioned { coordinates ->
val newIsVisible = coordinates.isCompletelyVisible(view)
if (isVisible != newIsVisible) {
isVisible = newIsVisible
onVisibilityChanges(isVisible == true)
}
}
}
fun LayoutCoordinates.isCompletelyVisible(view: View): Boolean {
if (!isAttached) return false
// Window relative bounds of our compose root view that are visible on the screen
val globalRootRect = Rect()
if (!view.getGlobalVisibleRect(globalRootRect)) {
// we aren't visible at all.
return false
}
val bounds = boundsInWindow()
if (bounds.isEmpty) return false
// Make sure we are completely in bounds.
return bounds.top >= globalRootRect.top &&
bounds.left >= globalRootRect.left &&
bounds.right <= globalRootRect.right &&
bounds.bottom <= globalRootRect.bottom
}
fun Modifier.onVisiblePositionChanges(onVisiblePosition: (Float?) -> Unit): Modifier = composed {
val view = LocalView.current
@@ -757,9 +724,9 @@ private fun KeepPlayingButton(
modifier = Size50Modifier
) {
if (keepPlaying.value) {
LyricsIcon(Size22Modifier, Color.White)
LyricsIcon(Size22Modifier, MaterialTheme.colors.onBackground)
} else {
LyricsOffIcon(Size22Modifier, Color.White)
LyricsOffIcon(Size22Modifier, MaterialTheme.colors.onBackground)
}
}
}
@@ -35,66 +35,62 @@ fun ZapRaiserRequest(
Column(
modifier = Modifier.fillMaxWidth()
) {
Column(
modifier = Modifier.fillMaxWidth()
Row(
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier
.fillMaxWidth()
.padding(bottom = 10.dp)
) {
Row(
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier
.fillMaxWidth()
.padding(bottom = 10.dp)
) {
Icon(
painter = painterResource(R.drawable.lightning),
null,
modifier = Size20Modifier,
tint = Color.Unspecified
)
Text(
text = titleText ?: stringResource(R.string.zapraiser),
fontSize = 20.sp,
fontWeight = FontWeight.W500,
modifier = Modifier.padding(start = 10.dp)
)
}
Divider()
Icon(
painter = painterResource(R.drawable.lightning),
null,
modifier = Size20Modifier,
tint = Color.Unspecified
)
Text(
text = stringResource(R.string.zapraiser_explainer),
color = MaterialTheme.colors.placeholderText,
modifier = Modifier.padding(vertical = 10.dp)
)
OutlinedTextField(
label = { Text(text = stringResource(R.string.zapraiser_target_amount_in_sats)) },
modifier = Modifier.fillMaxWidth(),
value = if (newPostViewModel.zapRaiserAmount != null) {
newPostViewModel.zapRaiserAmount.toString()
} else {
""
},
onValueChange = {
runCatching {
if (it.isEmpty()) {
newPostViewModel.zapRaiserAmount = null
} else {
newPostViewModel.zapRaiserAmount = it.toLongOrNull()
}
}
},
placeholder = {
Text(
text = "1000",
color = MaterialTheme.colors.placeholderText
)
},
keyboardOptions = KeyboardOptions.Default.copy(
keyboardType = KeyboardType.Number
),
singleLine = true
text = titleText ?: stringResource(R.string.zapraiser),
fontSize = 20.sp,
fontWeight = FontWeight.W500,
modifier = Modifier.padding(start = 10.dp)
)
}
Divider()
Text(
text = stringResource(R.string.zapraiser_explainer),
color = MaterialTheme.colors.placeholderText,
modifier = Modifier.padding(vertical = 10.dp)
)
OutlinedTextField(
label = { Text(text = stringResource(R.string.zapraiser_target_amount_in_sats)) },
modifier = Modifier.fillMaxWidth(),
value = if (newPostViewModel.zapRaiserAmount != null) {
newPostViewModel.zapRaiserAmount.toString()
} else {
""
},
onValueChange = {
runCatching {
if (it.isEmpty()) {
newPostViewModel.zapRaiserAmount = null
} else {
newPostViewModel.zapRaiserAmount = it.toLongOrNull()
}
}
},
placeholder = {
Text(
text = "1000",
color = MaterialTheme.colors.placeholderText
)
},
keyboardOptions = KeyboardOptions.Default.copy(
keyboardType = KeyboardType.Number
),
singleLine = true
)
}
}
@@ -66,8 +66,10 @@ import coil.compose.AsyncImage
import coil.compose.AsyncImagePainter
import coil.imageLoader
import com.vitorpamplona.amethyst.R
import com.vitorpamplona.amethyst.model.ConnectivityType
import com.vitorpamplona.amethyst.model.toHexKey
import com.vitorpamplona.amethyst.service.BlurHashRequester
import com.vitorpamplona.amethyst.service.CryptoUtils
import com.vitorpamplona.amethyst.service.connectivitystatus.ConnectivityStatus
import com.vitorpamplona.amethyst.ui.actions.CloseButton
import com.vitorpamplona.amethyst.ui.actions.LoadingAnimation
@@ -90,7 +92,6 @@ import kotlinx.coroutines.launch
import net.engawapg.lib.zoomable.rememberZoomState
import net.engawapg.lib.zoomable.zoomable
import java.io.File
import java.security.MessageDigest
@Immutable
abstract class ZoomableContent(
@@ -248,15 +249,13 @@ private fun LocalImageView(
) {
if (content.localFile != null && content.localFile.exists()) {
BoxWithConstraints(contentAlignment = Alignment.Center) {
val settings = accountViewModel?.account?.settings
val isMobile = ConnectivityStatus.isOnMobileData.value
val showImage = remember {
mutableStateOf(
if (alwayShowImage) { true } else {
when (settings?.automaticallyShowImages) {
true -> !isMobile
false -> false
when (accountViewModel?.account?.settings?.automaticallyShowImages) {
ConnectivityType.WIFI_ONLY -> !ConnectivityStatus.isOnMobileData.value
ConnectivityType.NEVER -> false
ConnectivityType.ALWAYS -> true
else -> true
}
}
@@ -318,15 +317,13 @@ private fun UrlImageView(
alwayShowImage: Boolean = false
) {
BoxWithConstraints(contentAlignment = Alignment.Center) {
val settings = accountViewModel?.account?.settings
val isMobile = ConnectivityStatus.isOnMobileData.value
val showImage = remember {
mutableStateOf(
if (alwayShowImage) { true } else {
when (settings?.automaticallyShowImages) {
true -> !isMobile
false -> false
when (accountViewModel?.account?.settings?.automaticallyShowImages) {
ConnectivityType.WIFI_ONLY -> !ConnectivityStatus.isOnMobileData.value
ConnectivityType.NEVER -> false
ConnectivityType.ALWAYS -> true
else -> true
}
}
@@ -700,11 +697,7 @@ private fun verifyHash(content: ZoomableUrlContent, context: Context): Boolean?
if (content.hash == null) return null
context.imageLoader.diskCache?.get(content.url)?.use { snapshot ->
val imageFile = snapshot.data.toFile()
val bytes = imageFile.readBytes()
val sha256 = MessageDigest.getInstance("SHA-256")
val hash = sha256.digest(bytes).toHexKey()
val hash = CryptoUtils.sha256(snapshot.data.toFile().readBytes()).toHexKey()
Log.d("Image Hash Verification", "$hash == ${content.hash}")
@@ -12,7 +12,7 @@ object BookmarkPrivateFeedFilter : FeedFilter<Note>() {
}
override fun feed(): List<Note> {
val privKey = account.loggedIn.privKey ?: return emptyList()
val privKey = account.keyPair.privKey ?: return emptyList()
val bookmarks = account.userProfile().latestBookmarkList
@@ -36,6 +36,7 @@ open class DiscoverChatFeedFilter(val account: Account) : AdditiveFeedFilter<Not
val followingKeySet = account.selectedUsersFollowList(account.defaultDiscoveryFollowList) ?: emptySet()
val followingTagSet = account.selectedTagsFollowList(account.defaultDiscoveryFollowList) ?: emptySet()
val followingGeohashSet = account.selectedGeohashesFollowList(account.defaultDiscoveryFollowList) ?: emptySet()
val createEvents = collection.filter { it.event is ChannelCreateEvent }
val anyOtherChannelEvent = collection
@@ -48,7 +49,7 @@ open class DiscoverChatFeedFilter(val account: Account) : AdditiveFeedFilter<Not
val activities = (createEvents + anyOtherChannelEvent)
.asSequence()
// .filter { it.event is ChannelCreateEvent } // Event heads might not be loaded yet.
.filter { isGlobal || it.author?.pubkeyHex in followingKeySet || it.event?.isTaggedHashes(followingTagSet) == true }
.filter { isGlobal || it.author?.pubkeyHex in followingKeySet || it.event?.isTaggedHashes(followingTagSet) == true || it.event?.isTaggedGeoHashes(followingGeohashSet) == true }
.filter { isHiddenList || it.author?.let { !account.isHidden(it.pubkeyHex) } ?: true }
.filter { (it.createdAt() ?: 0) <= now }
.toSet()

Some files were not shown because too many files have changed in this diff Show More