mirror of
https://github.com/vitorpamplona/amethyst.git
synced 2026-07-22 15:52:22 +00:00
Compare commits
61 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
145b0d8f0a | ||
|
|
eae07e2fe0 | ||
|
|
6b60b1434f | ||
|
|
3ff4db264a | ||
|
|
21f042fa95 | ||
|
|
e26bbc2e61 | ||
|
|
5135026e6a | ||
|
|
a72f5ae01f | ||
|
|
f2ed2d94c4 | ||
|
|
75d703ac0a | ||
|
|
277bf0a603 | ||
|
|
1ac990b67c | ||
|
|
e59f676d47 | ||
|
|
312d82fdf6 | ||
|
|
08659738bd | ||
|
|
f1b0958903 | ||
|
|
998aa2aed1 | ||
|
|
370a18f716 | ||
|
|
6638defbb9 | ||
|
|
d88eb014b7 | ||
|
|
14e9a91d40 | ||
|
|
3ed45f282c | ||
|
|
2ff090dac1 | ||
|
|
281d86b4bf | ||
|
|
504e55a93f | ||
|
|
c7b2c20119 | ||
|
|
3e102c064b | ||
|
|
e0ec8a741c | ||
|
|
f1388b46d5 | ||
|
|
0f24e458d8 | ||
|
|
b8f3625954 | ||
|
|
0babc46f7c | ||
|
|
6d368148cc | ||
|
|
3241f0ada1 | ||
|
|
49ad972af8 | ||
|
|
e061575454 | ||
|
|
694642b42b | ||
|
|
f4684ed078 | ||
|
|
db12ff0532 | ||
|
|
13d9c1ab10 | ||
|
|
ac8f0e8600 | ||
|
|
21472daab5 | ||
|
|
c6a00386c8 | ||
|
|
9e3aa31c25 | ||
|
|
47987fac0f | ||
|
|
46c782cf37 | ||
|
|
fd67f4875d | ||
|
|
4e2ff591e6 | ||
|
|
0e46145e2d | ||
|
|
383d859544 | ||
|
|
81400eb863 | ||
|
|
422446f53e | ||
|
|
f1e721f323 | ||
|
|
0fc72eb84d | ||
|
|
9234f4b7a8 | ||
|
|
0198d56099 | ||
|
|
8f31c60cd1 | ||
|
|
7cb17954d9 | ||
|
|
baf35b202f | ||
|
|
7b7e3624ac | ||
|
|
d27b9ae5a8 |
8
.github/workflows/build.yml
vendored
8
.github/workflows/build.yml
vendored
@@ -12,15 +12,17 @@ jobs:
|
||||
timeout-minutes: 30
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Set up JDK 17
|
||||
uses: actions/setup-java@v1
|
||||
uses: actions/setup-java@v4
|
||||
with:
|
||||
distribution: 'temurin'
|
||||
java-version: 17
|
||||
|
||||
- name: Cache gradle
|
||||
uses: actions/cache@v1
|
||||
uses: actions/cache@v3
|
||||
with:
|
||||
path: ~/.gradle/caches
|
||||
key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle*') }}
|
||||
|
||||
19
.github/workflows/create-release.yml
vendored
19
.github/workflows/create-release.yml
vendored
@@ -10,15 +10,16 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v2
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Set up JDK 17
|
||||
uses: actions/setup-java@v1
|
||||
uses: actions/setup-java@v4
|
||||
with:
|
||||
distribution: 'temurin'
|
||||
java-version: 17
|
||||
|
||||
- name: Cache gradle
|
||||
uses: actions/cache@v1
|
||||
uses: actions/cache@v3
|
||||
with:
|
||||
path: ~/.gradle/caches
|
||||
key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle*') }}
|
||||
@@ -36,6 +37,9 @@ jobs:
|
||||
alias: ${{ secrets.KEY_ALIAS }}
|
||||
keyStorePassword: ${{ secrets.KEY_STORE_PASSWORD }}
|
||||
keyPassword: ${{ secrets.KEY_PASSWORD }}
|
||||
env:
|
||||
# override default build-tools version (29.0.3) -- optional
|
||||
BUILD_TOOLS_VERSION: "34.0.0"
|
||||
|
||||
- name: Sign AAB (F-Droid)
|
||||
uses: r0adkll/sign-android-release@v1
|
||||
@@ -45,6 +49,9 @@ jobs:
|
||||
alias: ${{ secrets.KEY_ALIAS }}
|
||||
keyStorePassword: ${{ secrets.KEY_STORE_PASSWORD }}
|
||||
keyPassword: ${{ secrets.KEY_PASSWORD }}
|
||||
env:
|
||||
# override default build-tools version (29.0.3) -- optional
|
||||
BUILD_TOOLS_VERSION: "34.0.0"
|
||||
|
||||
- name: Build APK
|
||||
run: ./gradlew assembleRelease --stacktrace
|
||||
@@ -57,6 +64,9 @@ jobs:
|
||||
alias: ${{ secrets.KEY_ALIAS }}
|
||||
keyStorePassword: ${{ secrets.KEY_STORE_PASSWORD }}
|
||||
keyPassword: ${{ secrets.KEY_PASSWORD }}
|
||||
env:
|
||||
# override default build-tools version (29.0.3) -- optional
|
||||
BUILD_TOOLS_VERSION: "34.0.0"
|
||||
|
||||
- name: Sign APK (F-Droid)
|
||||
uses: r0adkll/sign-android-release@v1
|
||||
@@ -66,6 +76,9 @@ jobs:
|
||||
alias: ${{ secrets.KEY_ALIAS }}
|
||||
keyStorePassword: ${{ secrets.KEY_STORE_PASSWORD }}
|
||||
keyPassword: ${{ secrets.KEY_PASSWORD }}
|
||||
env:
|
||||
# override default build-tools version (29.0.3) -- optional
|
||||
BUILD_TOOLS_VERSION: "34.0.0"
|
||||
|
||||
- name: Create Release
|
||||
id: create_release
|
||||
|
||||
4
.github/workflows/crowdin.yml
vendored
4
.github/workflows/crowdin.yml
vendored
@@ -10,8 +10,8 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v3
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: crowdin action
|
||||
uses: crowdin/github-action@v1
|
||||
|
||||
1
.gitignore
vendored
1
.gitignore
vendored
@@ -9,6 +9,7 @@
|
||||
/.idea/deploymentTargetDropDown.xml
|
||||
/.idea/appInsightsSettings.xml
|
||||
/.idea/ktlint-plugin.xml
|
||||
/.idea/ktfmt.xml
|
||||
.DS_Store
|
||||
/build
|
||||
/captures
|
||||
|
||||
23
README.md
23
README.md
@@ -2,7 +2,7 @@
|
||||
|
||||
<img align="right" src="./docs/screenshots/home.png" data-canonical-src="./docs/screenshots/home.png" width="350px"/>
|
||||
|
||||
Amethyst brings the best social network to your Android phone. Just insert your Nostr private key and start posting.
|
||||
Amethyst brings the best social network to your Android phone. Just insert your Nostr private key and start posting.
|
||||
|
||||
[<img src="./docs/design/obtainium.png"
|
||||
alt="Get it on Obtaininum"
|
||||
@@ -59,6 +59,7 @@ height="80">](https://github.com/vitorpamplona/amethyst/releases)
|
||||
- [ ] Nostr Connect (NIP-46)
|
||||
- [x] Wallet Connect API (NIP-47)
|
||||
- [ ] Proxy Tags (NIP-48, Not applicable)
|
||||
- [ ] Private key encryption for import/export (NIP-49)
|
||||
- [x] Online Relay Search (NIP-50)
|
||||
- [x] Lists (NIP-51)
|
||||
- [ ] Calendar Events (NIP-52)
|
||||
@@ -69,6 +70,7 @@ height="80">](https://github.com/vitorpamplona/amethyst/releases)
|
||||
- [x] Zaps (NIP-57)
|
||||
- [x] Private Zaps
|
||||
- [x] Zap Splits (NIP-57)
|
||||
- [x] Gift Wraps & Seals (NIP-59)
|
||||
- [x] Zapraiser (NIP-TBD)
|
||||
- [x] Badges (NIP-58)
|
||||
- [ ] Relay List Metadata (NIP-65)
|
||||
@@ -85,7 +87,6 @@ height="80">](https://github.com/vitorpamplona/amethyst/releases)
|
||||
- [x] HTTP Auth (NIP-98)
|
||||
- [x] Classifieds (NIP-99)
|
||||
- [x] Private Messages and Small Groups (NIP-24/Draft)
|
||||
- [x] Gift Wraps & Seals (NIP-59/Draft)
|
||||
- [x] Versioned Encrypted Payloads (NIP-44/Draft)
|
||||
- [x] Audio Tracks (zapstr.live) (Kind:31337)
|
||||
- [x] Push Notifications (Google and Unified Push)
|
||||
@@ -103,7 +104,7 @@ height="80">](https://github.com/vitorpamplona/amethyst/releases)
|
||||
|
||||
# Development Overview
|
||||
|
||||
## Overall Architecture
|
||||
## Overall Architecture
|
||||
|
||||
This is a native Android app made with Kotlin and Jetpack Compose.
|
||||
The app uses a modified version of the [nostrpostrlib](https://github.com/Giszmo/NostrPostr/tree/master/nostrpostrlib) to talk to Nostr relays.
|
||||
@@ -166,6 +167,12 @@ For the Play build:
|
||||
./gradlew installPlayDebug
|
||||
```
|
||||
|
||||
## Contributing
|
||||
|
||||
[Issues](https://github.com/vitorpamplona/amethyst/issues) and [pull requests](https://github.com/vitorpamplona/amethyst/pulls) here are also very welcome.
|
||||
|
||||
You can also send patches in Nostr using [GitStr](https://github.com/fiatjaf/gitstr) to `naddr1qqyxzmt9w358jum5qyg8v6t5daezumn0wd68yvfwvdhk6qg7waehxw309ahx7um5wgkhqatz9emk2mrvdaexgetj9ehx2ap0qy2hwumn8ghj7un9d3shjtnwdaehgu3wvfnj7q3qgcxzte5zlkncx26j68ez60fzkvtkm9e0vrwdcvsjakxf9mu9qewqxpqqqpmej720gac`
|
||||
|
||||
## How to Deploy
|
||||
|
||||
1. Generate a new signing key
|
||||
@@ -186,7 +193,7 @@ openssl base64 < <my-release-key.keystore> | tr -d '\n' | tee some_signing_key.j
|
||||
8. Download the `aab` file and upload it to the` PlayStore.
|
||||
|
||||
# Privacy on Relays & nostr
|
||||
Your internet protocol (IP) address is exposed to the relays you connect to. If you want to improve your privacy, consider utilizing a service that masks your IP address (e.g. a VPN) from trackers online.
|
||||
Your internet protocol (IP) address is exposed to the relays you connect to. If you want to improve your privacy, consider utilizing a service that masks your IP address (e.g. a VPN) from trackers online.
|
||||
|
||||
The relay also learns which public keys you are requesting, meaning your public key will be tied to your IP address.
|
||||
|
||||
@@ -197,11 +204,11 @@ While the content of direct messages (DMs) is only visible to you and your DM co
|
||||
|
||||
# Visibility & Permanence of Your Content on nostr
|
||||
## Information Visibility ##
|
||||
Content that you share can be shared to other relays.
|
||||
Content that you share can be shared to other relays.
|
||||
Information that you share publicly is visible to anyone reading from relays that have your information. Your information may also be visible to nostr users who do not share relays with you.
|
||||
|
||||
## Information Permanence ##
|
||||
Information shared on nostr should be assumed permanent for privacy purposes. There is no way to guarantee edit or deletion of any content once posted.
|
||||
Information shared on nostr should be assumed permanent for privacy purposes. There is no way to guarantee edit or deletion of any content once posted.
|
||||
|
||||
# Screenshots
|
||||
|
||||
@@ -209,10 +216,6 @@ Information shared on nostr should be assumed permanent for privacy purposes. Th
|
||||
|-------------------------------------------|----------------------------------------------|-------------------------------------------------|--------------------------------------------------------|
|
||||
|  |  |  |  |
|
||||
|
||||
# Contributing
|
||||
|
||||
[Issues](https://github.com/vitorpamplona/amethyst/issues) and [pull requests](https://github.com/vitorpamplona/amethyst/pulls) are very welcome.
|
||||
|
||||
## Contributors
|
||||
|
||||
<a align="center" href="https://github.com/vitorpamplona/amethyst/graphs/contributors">
|
||||
|
||||
@@ -12,8 +12,8 @@ android {
|
||||
applicationId "com.vitorpamplona.amethyst"
|
||||
minSdk 26
|
||||
targetSdk 34
|
||||
versionCode 346
|
||||
versionName "0.83.5"
|
||||
versionCode 349
|
||||
versionName "0.83.8"
|
||||
|
||||
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
||||
vectorDrawables {
|
||||
@@ -26,7 +26,7 @@ android {
|
||||
'cy-rGB',
|
||||
'da-rDK',
|
||||
'de',
|
||||
'el-rGB',
|
||||
'el-rGR',
|
||||
'en-rGB',
|
||||
'eo',
|
||||
'es',
|
||||
@@ -99,6 +99,7 @@ android {
|
||||
}
|
||||
create("benchmark") {
|
||||
initWith(getByName("release"))
|
||||
profileable true
|
||||
signingConfig signingConfigs.debug
|
||||
}
|
||||
}
|
||||
@@ -180,7 +181,7 @@ dependencies {
|
||||
|
||||
// Adaptive Layout / Two Pane
|
||||
implementation "androidx.compose.material3:material3-window-size-class:${material3_version}"
|
||||
implementation "com.google.accompanist:accompanist-adaptive:0.32.0"
|
||||
implementation 'com.google.accompanist:accompanist-adaptive:0.34.0'
|
||||
|
||||
|
||||
// Lifecycle
|
||||
@@ -250,7 +251,7 @@ dependencies {
|
||||
playImplementation 'com.google.mlkit:translate:17.0.2'
|
||||
|
||||
// PushNotifications
|
||||
playImplementation platform('com.google.firebase:firebase-bom:32.7.0')
|
||||
playImplementation platform('com.google.firebase:firebase-bom:32.7.1')
|
||||
playImplementation 'com.google.firebase:firebase-messaging-ktx'
|
||||
|
||||
//PushNotifications(FDroid)
|
||||
@@ -274,9 +275,9 @@ dependencies {
|
||||
implementation 'id.zelory:compressor:3.0.1'
|
||||
|
||||
testImplementation 'junit:junit:4.13.2'
|
||||
testImplementation 'io.mockk:mockk:1.13.8'
|
||||
androidTestImplementation 'androidx.test.ext:junit:1.2.0-alpha02'
|
||||
androidTestImplementation 'androidx.test.ext:junit-ktx:1.2.0-alpha02'
|
||||
testImplementation 'io.mockk:mockk:1.13.9'
|
||||
androidTestImplementation 'androidx.test.ext:junit:1.2.0-alpha03'
|
||||
androidTestImplementation 'androidx.test.ext:junit-ktx:1.2.0-alpha03'
|
||||
androidTestImplementation 'androidx.test.espresso:espresso-core:3.5.1'
|
||||
androidTestImplementation "androidx.compose.ui:ui-test-junit4:$compose_ui_version"
|
||||
debugImplementation "androidx.compose.ui:ui-tooling:$compose_ui_version"
|
||||
|
||||
@@ -32,6 +32,7 @@ import com.vitorpamplona.amethyst.service.notifications.NotificationUtils.getOrC
|
||||
import com.vitorpamplona.amethyst.service.notifications.NotificationUtils.getOrCreateZapChannel
|
||||
import com.vitorpamplona.quartz.events.Event
|
||||
import com.vitorpamplona.quartz.events.GiftWrapEvent
|
||||
import kotlinx.coroutines.CancellationException
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.SupervisorJob
|
||||
@@ -60,6 +61,7 @@ class PushMessageReceiver : MessagingReceiver() {
|
||||
try {
|
||||
parseMessage(messageStr)?.let { receiveIfNew(it) }
|
||||
} catch (e: Exception) {
|
||||
if (e is CancellationException) throw e
|
||||
Log.d(TAG, "Message could not be parsed: ${e.message}")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -22,6 +22,7 @@ package com.vitorpamplona.amethyst.service.notifications
|
||||
|
||||
import android.util.Log
|
||||
import com.vitorpamplona.amethyst.AccountInfo
|
||||
import kotlinx.coroutines.CancellationException
|
||||
|
||||
object PushNotificationUtils {
|
||||
var hasInit: Boolean = false
|
||||
@@ -36,6 +37,7 @@ object PushNotificationUtils {
|
||||
RegisterAccounts(accounts).go(pushHandler.getSavedEndpoint())
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
if (e is CancellationException) throw e
|
||||
Log.d("Amethyst-OSSPushUtils", "Failed to get endpoint.")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,8 +3,11 @@
|
||||
xmlns:tools="http://schemas.android.com/tools">
|
||||
<queries>
|
||||
<package android:name="org.torproject.android"/>
|
||||
<package android:name="com.greenart7c3.nostrsigner"/>
|
||||
<package android:name="com.greenart7c3.nostrsigner.debug"/>
|
||||
<intent>
|
||||
<action android:name="android.intent.action.VIEW" />
|
||||
<category android:name="android.intent.category.BROWSABLE" />
|
||||
<data android:scheme="nostrsigner" />
|
||||
</intent>
|
||||
</queries>
|
||||
|
||||
<!-- Doesn't require a camera -->
|
||||
|
||||
@@ -40,7 +40,7 @@ import com.vitorpamplona.amethyst.model.ThemeType
|
||||
import com.vitorpamplona.amethyst.model.parseBooleanType
|
||||
import com.vitorpamplona.amethyst.model.parseConnectivityType
|
||||
import com.vitorpamplona.amethyst.model.parseThemeType
|
||||
import com.vitorpamplona.amethyst.service.HttpClient
|
||||
import com.vitorpamplona.amethyst.service.HttpClientManager
|
||||
import com.vitorpamplona.amethyst.service.Nip96MediaServers
|
||||
import com.vitorpamplona.amethyst.service.checkNotInMainThread
|
||||
import com.vitorpamplona.quartz.crypto.KeyPair
|
||||
@@ -53,6 +53,7 @@ import com.vitorpamplona.quartz.events.LnZapEvent
|
||||
import com.vitorpamplona.quartz.signers.ExternalSignerLauncher
|
||||
import com.vitorpamplona.quartz.signers.NostrSignerExternal
|
||||
import com.vitorpamplona.quartz.signers.NostrSignerInternal
|
||||
import kotlinx.coroutines.CancellationException
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.sync.Mutex
|
||||
@@ -130,8 +131,11 @@ object LocalPreferences {
|
||||
return currentAccount
|
||||
}
|
||||
|
||||
private suspend fun updateCurrentAccount(npub: String) {
|
||||
if (currentAccount != npub) {
|
||||
private fun updateCurrentAccount(npub: String?) {
|
||||
if (npub == null) {
|
||||
currentAccount = null
|
||||
encryptedPreferences().edit().clear().apply()
|
||||
} else if (currentAccount != npub) {
|
||||
currentAccount = npub
|
||||
|
||||
encryptedPreferences().edit().apply { putString(PrefKeys.CURRENT_ACCOUNT, npub) }.apply()
|
||||
@@ -250,7 +254,7 @@ object LocalPreferences {
|
||||
deleteUserPreferenceFile(accountInfo.npub)
|
||||
|
||||
if (savedAccounts().isEmpty()) {
|
||||
encryptedPreferences().edit().clear().apply()
|
||||
updateCurrentAccount(null)
|
||||
} else if (currentAccount() == accountInfo.npub) {
|
||||
updateCurrentAccount(savedAccounts().elementAt(0).npub)
|
||||
}
|
||||
@@ -365,6 +369,7 @@ object LocalPreferences {
|
||||
return try {
|
||||
getString(PrefKeys.SHARED_SETTINGS, "{}")?.let { Event.mapper.readValue<Settings>(it) }
|
||||
} catch (e: Throwable) {
|
||||
if (e is CancellationException) throw e
|
||||
Log.w(
|
||||
"LocalPreferences",
|
||||
"Unable to decode shared preferences: ${getString(PrefKeys.SHARED_SETTINGS, null)}",
|
||||
@@ -507,6 +512,7 @@ object LocalPreferences {
|
||||
}
|
||||
?: Nip96MediaServers.DEFAULT[0]
|
||||
} catch (e: Exception) {
|
||||
if (e is CancellationException) throw e
|
||||
Log.w("LocalPreferences", "Failed to decode saved File Server", e)
|
||||
e.printStackTrace()
|
||||
Nip96MediaServers.DEFAULT[0]
|
||||
@@ -518,6 +524,7 @@ object LocalPreferences {
|
||||
Event.mapper.readValue<Nip47URI?>(it)
|
||||
}
|
||||
} catch (e: Throwable) {
|
||||
if (e is CancellationException) throw e
|
||||
Log.w(
|
||||
"LocalPreferences",
|
||||
"Error Decoding Zap Payment Request Server ${getString(PrefKeys.ZAP_PAYMENT_REQUEST_SERVER, null)}",
|
||||
@@ -538,6 +545,7 @@ object LocalPreferences {
|
||||
}
|
||||
}
|
||||
} catch (e: Throwable) {
|
||||
if (e is CancellationException) throw e
|
||||
Log.w(
|
||||
"LocalPreferences",
|
||||
"Error Decoding Contact List ${getString(PrefKeys.LATEST_CONTACT_LIST, null)}",
|
||||
@@ -553,6 +561,7 @@ object LocalPreferences {
|
||||
}
|
||||
?: mapOf()
|
||||
} catch (e: Throwable) {
|
||||
if (e is CancellationException) throw e
|
||||
Log.w(
|
||||
"LocalPreferences",
|
||||
"Error Decoding Language Preferences ${getString(PrefKeys.LANGUAGE_PREFS, null)}",
|
||||
@@ -567,7 +576,7 @@ object LocalPreferences {
|
||||
val hideNIP24WarningDialog = getBoolean(PrefKeys.HIDE_NIP_24_WARNING_DIALOG, false)
|
||||
val useProxy = getBoolean(PrefKeys.USE_PROXY, false)
|
||||
val proxyPort = getInt(PrefKeys.PROXY_PORT, 9050)
|
||||
val proxy = HttpClient.initProxy(useProxy, "127.0.0.1", proxyPort)
|
||||
val proxy = HttpClientManager.initProxy(useProxy, "127.0.0.1", proxyPort)
|
||||
|
||||
val showSensitiveContent =
|
||||
if (contains(PrefKeys.SHOW_SENSITIVE_CONTENT)) {
|
||||
@@ -585,6 +594,7 @@ object LocalPreferences {
|
||||
}
|
||||
?: mapOf()
|
||||
} catch (e: Throwable) {
|
||||
if (e is CancellationException) throw e
|
||||
Log.w(
|
||||
"LocalPreferences",
|
||||
"Error Decoding Last Read per route ${getString(PrefKeys.LAST_READ_PER_ROUTE, null)}",
|
||||
|
||||
@@ -30,7 +30,7 @@ import coil.decode.SvgDecoder
|
||||
import coil.size.Precision
|
||||
import com.vitorpamplona.amethyst.model.Account
|
||||
import com.vitorpamplona.amethyst.model.LocalCache
|
||||
import com.vitorpamplona.amethyst.service.HttpClient
|
||||
import com.vitorpamplona.amethyst.service.HttpClientManager
|
||||
import com.vitorpamplona.amethyst.service.NostrAccountDataSource
|
||||
import com.vitorpamplona.amethyst.service.NostrChannelDataSource
|
||||
import com.vitorpamplona.amethyst.service.NostrChatroomDataSource
|
||||
@@ -51,12 +51,12 @@ import com.vitorpamplona.amethyst.service.relays.Client
|
||||
import com.vitorpamplona.quartz.encoders.bechToBytes
|
||||
import com.vitorpamplona.quartz.encoders.decodePublicKeyAsHexOrNull
|
||||
import com.vitorpamplona.quartz.encoders.toHexKey
|
||||
import kotlinx.coroutines.CancellationException
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.GlobalScope
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.SupervisorJob
|
||||
import kotlinx.coroutines.cancel
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
@@ -84,7 +84,7 @@ class ServiceManager {
|
||||
val myAccount = account
|
||||
|
||||
// Resets Proxy Use
|
||||
HttpClient.start(account?.proxy)
|
||||
HttpClientManager.setDefaultProxy(account?.proxy)
|
||||
LocalCache.antiSpam.active = account?.filterSpamFromStrangers ?: true
|
||||
Coil.setImageLoader {
|
||||
Amethyst.instance
|
||||
@@ -97,7 +97,7 @@ class ServiceManager {
|
||||
}
|
||||
add(SvgDecoder.Factory())
|
||||
} // .logger(DebugLogger())
|
||||
.okHttpClient { HttpClient.getHttpClient() }
|
||||
.okHttpClient { HttpClientManager.getHttpClient() }
|
||||
.precision(Precision.INEXACT)
|
||||
.respectCacheHeaders(false)
|
||||
.build()
|
||||
@@ -127,6 +127,7 @@ class ServiceManager {
|
||||
try {
|
||||
it.npub.bechToBytes().toHexKey()
|
||||
} catch (e: Exception) {
|
||||
if (e is CancellationException) throw e
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
@@ -66,7 +66,6 @@ import com.vitorpamplona.quartz.events.GeneralListEvent
|
||||
import com.vitorpamplona.quartz.events.GenericRepostEvent
|
||||
import com.vitorpamplona.quartz.events.GiftWrapEvent
|
||||
import com.vitorpamplona.quartz.events.HTTPAuthorizationEvent
|
||||
import com.vitorpamplona.quartz.events.IdentityClaim
|
||||
import com.vitorpamplona.quartz.events.LiveActivitiesChatMessageEvent
|
||||
import com.vitorpamplona.quartz.events.LnZapEvent
|
||||
import com.vitorpamplona.quartz.events.LnZapPaymentRequestEvent
|
||||
@@ -455,11 +454,21 @@ class Account(
|
||||
|
||||
val liveHiddenUsers = flowHiddenUsers.asLiveData()
|
||||
|
||||
val decryptBookmarks: LiveData<BookmarkListEvent> by lazy {
|
||||
val decryptBookmarks: LiveData<BookmarkListEvent?> by lazy {
|
||||
userProfile().live().innerBookmarks.switchMap { userState ->
|
||||
liveData(Dispatchers.IO) {
|
||||
userState.user.latestBookmarkList?.privateTags(signer) {
|
||||
scope.launch(Dispatchers.IO) { userState.user.latestBookmarkList?.let { emit(it) } }
|
||||
if (userState.user.latestBookmarkList == null) {
|
||||
emit(null)
|
||||
} else {
|
||||
emit(
|
||||
withTimeoutOrNull(1000) {
|
||||
suspendCancellableCoroutine { continuation ->
|
||||
userState.user.latestBookmarkList?.privateTags(signer) {
|
||||
continuation.resume(userState.user.latestBookmarkList)
|
||||
}
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -541,14 +550,36 @@ class Account(
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun sendNewUserMetadata(
|
||||
toString: String,
|
||||
newName: String,
|
||||
identities: List<IdentityClaim>,
|
||||
fun sendNewUserMetadata(
|
||||
name: String? = null,
|
||||
picture: String? = null,
|
||||
banner: String? = null,
|
||||
website: String? = null,
|
||||
about: String? = null,
|
||||
nip05: String? = null,
|
||||
lnAddress: String? = null,
|
||||
lnURL: String? = null,
|
||||
twitter: String? = null,
|
||||
mastodon: String? = null,
|
||||
github: String? = null,
|
||||
) {
|
||||
if (!isWriteable()) return
|
||||
|
||||
MetadataEvent.create(toString, newName, identities, signer) {
|
||||
MetadataEvent.updateFromPast(
|
||||
latest = userProfile().info?.latestMetadata,
|
||||
name = name,
|
||||
picture = picture,
|
||||
banner = banner,
|
||||
website = website,
|
||||
about = about,
|
||||
nip05 = nip05,
|
||||
lnAddress = lnAddress,
|
||||
lnURL = lnURL,
|
||||
twitter = twitter,
|
||||
mastodon = mastodon,
|
||||
github = github,
|
||||
signer = signer,
|
||||
) {
|
||||
Client.send(it)
|
||||
LocalCache.justConsume(it, null)
|
||||
}
|
||||
@@ -1763,7 +1794,14 @@ class Account(
|
||||
note: Note,
|
||||
onReady: (Boolean) -> Unit,
|
||||
) {
|
||||
if (!isWriteable()) return
|
||||
if (!isWriteable()) {
|
||||
onReady(false)
|
||||
false
|
||||
}
|
||||
if (userProfile().latestBookmarkList == null) {
|
||||
onReady(false)
|
||||
false
|
||||
}
|
||||
|
||||
if (note is AddressableNote) {
|
||||
userProfile().latestBookmarkList?.privateTaggedAddresses(signer) {
|
||||
|
||||
@@ -98,6 +98,7 @@ import kotlinx.collections.immutable.ImmutableList
|
||||
import kotlinx.collections.immutable.persistentSetOf
|
||||
import kotlinx.collections.immutable.toImmutableList
|
||||
import kotlinx.collections.immutable.toImmutableSet
|
||||
import kotlinx.coroutines.CancellationException
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.channels.BufferOverflow
|
||||
import kotlinx.coroutines.flow.MutableSharedFlow
|
||||
@@ -1397,6 +1398,7 @@ object LocalCache {
|
||||
try {
|
||||
Nip19.uriToRoute(text)?.hex ?: Hex.decode(text).toHexKey()
|
||||
} catch (e: Exception) {
|
||||
if (e is CancellationException) throw e
|
||||
null
|
||||
}
|
||||
|
||||
@@ -1446,6 +1448,7 @@ object LocalCache {
|
||||
try {
|
||||
Nip19.uriToRoute(text)?.hex ?: Hex.decode(text).toHexKey()
|
||||
} catch (e: Exception) {
|
||||
if (e is CancellationException) throw e
|
||||
null
|
||||
}
|
||||
|
||||
@@ -1738,6 +1741,7 @@ object LocalCache {
|
||||
try {
|
||||
event.checkSignature()
|
||||
} catch (e: Exception) {
|
||||
if (e is CancellationException) throw e
|
||||
Log.w("Event failed retest ${event.kind}", (e.message ?: "") + event.toJson())
|
||||
}
|
||||
false
|
||||
@@ -1831,6 +1835,7 @@ object LocalCache {
|
||||
}
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
if (e is CancellationException) throw e
|
||||
e.printStackTrace()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -63,6 +63,7 @@ import com.vitorpamplona.quartz.events.WrappedEvent
|
||||
import com.vitorpamplona.quartz.signers.NostrSigner
|
||||
import com.vitorpamplona.quartz.utils.TimeUtils
|
||||
import com.vitorpamplona.quartz.utils.containsAny
|
||||
import kotlinx.coroutines.CancellationException
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import java.math.BigDecimal
|
||||
@@ -644,6 +645,7 @@ open class Note(val idHex: String) {
|
||||
try {
|
||||
LnInvoiceUtil.getAmountInSats(invoice)
|
||||
} catch (e: java.lang.Exception) {
|
||||
if (e is CancellationException) throw e
|
||||
null
|
||||
}
|
||||
|
||||
@@ -694,6 +696,7 @@ open class Note(val idHex: String) {
|
||||
try {
|
||||
BigDecimal(it.event?.content())
|
||||
} catch (e: Exception) {
|
||||
if (e is CancellationException) throw e
|
||||
null
|
||||
// do nothing if it can't convert to bigdecimal
|
||||
}
|
||||
@@ -709,6 +712,7 @@ open class Note(val idHex: String) {
|
||||
try {
|
||||
BigDecimal(it.event?.content())
|
||||
} catch (e: Exception) {
|
||||
if (e is CancellationException) throw e
|
||||
null
|
||||
// do nothing if it can't convert to bigdecimal
|
||||
}
|
||||
|
||||
@@ -42,6 +42,7 @@ import kotlinx.collections.immutable.persistentListOf
|
||||
import kotlinx.collections.immutable.toImmutableList
|
||||
import kotlinx.collections.immutable.toImmutableMap
|
||||
import kotlinx.collections.immutable.toImmutableSet
|
||||
import kotlinx.coroutines.CancellationException
|
||||
import java.util.regex.Pattern
|
||||
|
||||
@Immutable
|
||||
@@ -103,6 +104,7 @@ class RichTextParser() {
|
||||
hash = frags["x"],
|
||||
blurhash = frags["blurhash"],
|
||||
dim = frags["dim"],
|
||||
contentWarning = frags["content-warning"],
|
||||
)
|
||||
} else if (videoExtensions.any { removedParamsFromUrl.endsWith(it) }) {
|
||||
val frags = Nip44UrlParser().parse(fullUrl)
|
||||
@@ -112,6 +114,7 @@ class RichTextParser() {
|
||||
hash = frags["x"],
|
||||
blurhash = frags["blurhash"],
|
||||
dim = frags["dim"],
|
||||
contentWarning = frags["content-warning"],
|
||||
)
|
||||
} else {
|
||||
null
|
||||
@@ -283,6 +286,7 @@ class RichTextParser() {
|
||||
}
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
if (e is CancellationException) throw e
|
||||
Log.w("Tag Parser", "Couldn't link tag $word", e)
|
||||
}
|
||||
|
||||
@@ -297,6 +301,7 @@ class RichTextParser() {
|
||||
}
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
if (e is CancellationException) throw e
|
||||
Log.e("Hashtag Parser", "Couldn't link hashtag $word", e)
|
||||
}
|
||||
|
||||
|
||||
@@ -32,6 +32,7 @@ import okhttp3.MediaType.Companion.toMediaType
|
||||
import okhttp3.Request
|
||||
import okhttp3.RequestBody.Companion.toRequestBody
|
||||
import java.util.Base64
|
||||
import kotlin.coroutines.cancellation.CancellationException
|
||||
|
||||
@Immutable
|
||||
data class CashuToken(
|
||||
@@ -59,6 +60,7 @@ class CashuProcessor {
|
||||
|
||||
return GenericLoadable.Loaded(CashuToken(cashuToken, mint, totalAmount, proofs))
|
||||
} catch (e: Exception) {
|
||||
if (e is CancellationException) throw e
|
||||
return GenericLoadable.Error<CashuToken>("Could not parse this cashu token")
|
||||
}
|
||||
}
|
||||
@@ -119,7 +121,7 @@ class CashuProcessor {
|
||||
checkNotInMainThread()
|
||||
|
||||
try {
|
||||
val client = HttpClient.getHttpClient()
|
||||
val client = HttpClientManager.getHttpClient()
|
||||
val url = "$mintAddress/checkfees" // Melt cashu tokens at Mint
|
||||
|
||||
val factory = Event.mapper.nodeFactory
|
||||
@@ -154,6 +156,7 @@ class CashuProcessor {
|
||||
}
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
if (e is CancellationException) throw e
|
||||
onError(
|
||||
context.getString(R.string.cashu_successful_redemption),
|
||||
context.getString(R.string.cashu_failed_redemption_explainer_error_msg, e.message),
|
||||
@@ -170,7 +173,7 @@ class CashuProcessor {
|
||||
context: Context,
|
||||
) {
|
||||
try {
|
||||
val client = HttpClient.getHttpClient()
|
||||
val client = HttpClientManager.getHttpClient()
|
||||
val url = token.mint + "/melt" // Melt cashu tokens at Mint
|
||||
|
||||
val factory = Event.mapper.nodeFactory
|
||||
@@ -211,6 +214,7 @@ class CashuProcessor {
|
||||
}
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
if (e is CancellationException) throw e
|
||||
onError(
|
||||
context.getString(R.string.cashu_successful_redemption),
|
||||
context.getString(R.string.cashu_failed_redemption_explainer_error_msg, e.message),
|
||||
|
||||
@@ -32,6 +32,7 @@ import com.vitorpamplona.amethyst.ui.actions.ImageDownloader
|
||||
import com.vitorpamplona.quartz.crypto.CryptoUtils
|
||||
import com.vitorpamplona.quartz.encoders.toHexKey
|
||||
import io.trbl.blurhash.BlurHash
|
||||
import kotlinx.coroutines.CancellationException
|
||||
import java.io.IOException
|
||||
import kotlin.math.roundToInt
|
||||
|
||||
@@ -59,6 +60,7 @@ class FileHeader(
|
||||
onError(null)
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
if (e is CancellationException) throw e
|
||||
Log.e("ImageDownload", "Couldn't download image from server: ${e.message}")
|
||||
onError(e.message)
|
||||
}
|
||||
@@ -174,6 +176,7 @@ class FileHeader(
|
||||
|
||||
onReady(FileHeader(mimeType, hash, size, dim, blurHash))
|
||||
} catch (e: Exception) {
|
||||
if (e is CancellationException) throw e
|
||||
Log.e("ImageDownload", "Couldn't convert image in to File Header: ${e.message}")
|
||||
onError(e.message)
|
||||
}
|
||||
|
||||
@@ -32,14 +32,13 @@ import java.net.Proxy
|
||||
import java.time.Duration
|
||||
import kotlin.properties.Delegates
|
||||
|
||||
object HttpClient {
|
||||
val DEFAULT_TIMEOUT_ON_WIFI = Duration.ofSeconds(10L)
|
||||
val DEFAULT_TIMEOUT_ON_MOBILE = Duration.ofSeconds(30L)
|
||||
object HttpClientManager {
|
||||
val DEFAULT_TIMEOUT_ON_WIFI: Duration = Duration.ofSeconds(10L)
|
||||
val DEFAULT_TIMEOUT_ON_MOBILE: Duration = Duration.ofSeconds(30L)
|
||||
|
||||
var proxyChangeListeners = ArrayList<() -> Unit>()
|
||||
var defaultTimeout = DEFAULT_TIMEOUT_ON_WIFI
|
||||
|
||||
var defaultHttpClient: OkHttpClient? = null
|
||||
private var defaultTimeout = DEFAULT_TIMEOUT_ON_WIFI
|
||||
private var defaultHttpClient: OkHttpClient? = null
|
||||
|
||||
// fires off every time value of the property changes
|
||||
private var internalProxy: Proxy? by
|
||||
@@ -49,26 +48,34 @@ object HttpClient {
|
||||
}
|
||||
}
|
||||
|
||||
fun start(proxy: Proxy?) {
|
||||
fun setDefaultProxy(proxy: Proxy?) {
|
||||
if (internalProxy != proxy) {
|
||||
Log.d("HttpClient", "Changing proxy to: ${proxy != null}")
|
||||
this.internalProxy = proxy
|
||||
this.defaultHttpClient = getHttpClient()
|
||||
|
||||
// recreates singleton
|
||||
this.defaultHttpClient = buildHttpClient(internalProxy, defaultTimeout)
|
||||
}
|
||||
}
|
||||
|
||||
fun changeTimeouts(timeout: Duration) {
|
||||
fun setDefaultTimeout(timeout: Duration) {
|
||||
Log.d("HttpClient", "Changing timeout to: $timeout")
|
||||
if (this.defaultTimeout.seconds != timeout.seconds) {
|
||||
this.defaultTimeout = timeout
|
||||
this.defaultHttpClient = getHttpClient()
|
||||
|
||||
// recreates singleton
|
||||
this.defaultHttpClient = buildHttpClient(internalProxy, defaultTimeout)
|
||||
}
|
||||
}
|
||||
|
||||
fun getHttpClient(timeout: Duration): OkHttpClient {
|
||||
val seconds = if (internalProxy != null) timeout.seconds * 2 else timeout.seconds
|
||||
private fun buildHttpClient(
|
||||
proxy: Proxy?,
|
||||
timeout: Duration,
|
||||
): OkHttpClient {
|
||||
val seconds = if (proxy != null) timeout.seconds * 2 else timeout.seconds
|
||||
val duration = Duration.ofSeconds(seconds)
|
||||
return OkHttpClient.Builder()
|
||||
.proxy(internalProxy)
|
||||
.proxy(proxy)
|
||||
.readTimeout(duration)
|
||||
.connectTimeout(duration)
|
||||
.writeTimeout(duration)
|
||||
@@ -91,24 +98,13 @@ object HttpClient {
|
||||
}
|
||||
}
|
||||
|
||||
fun getHttpClientForRelays(): OkHttpClient {
|
||||
if (this.defaultHttpClient == null) {
|
||||
this.defaultHttpClient = getHttpClient(defaultTimeout)
|
||||
}
|
||||
return defaultHttpClient!!
|
||||
}
|
||||
|
||||
fun getHttpClient(): OkHttpClient {
|
||||
if (this.defaultHttpClient == null) {
|
||||
this.defaultHttpClient = getHttpClient(defaultTimeout)
|
||||
this.defaultHttpClient = buildHttpClient(internalProxy, defaultTimeout)
|
||||
}
|
||||
return defaultHttpClient!!
|
||||
}
|
||||
|
||||
fun getProxy(): Proxy? {
|
||||
return internalProxy
|
||||
}
|
||||
|
||||
fun initProxy(
|
||||
useProxy: Boolean,
|
||||
hostname: String,
|
||||
@@ -29,6 +29,7 @@ import android.location.LocationManager
|
||||
import android.os.HandlerThread
|
||||
import androidx.compose.runtime.MutableState
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import kotlinx.coroutines.CancellationException
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
|
||||
class LocationUtil(context: Context) {
|
||||
@@ -105,6 +106,7 @@ class ReverseGeoLocationUtil {
|
||||
.joinToString(", ")
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
if (e is CancellationException) throw e
|
||||
e.printStackTrace()
|
||||
return null
|
||||
}
|
||||
|
||||
@@ -22,6 +22,7 @@ package com.vitorpamplona.amethyst.service
|
||||
|
||||
import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper
|
||||
import com.vitorpamplona.amethyst.BuildConfig
|
||||
import kotlinx.coroutines.CancellationException
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.withContext
|
||||
import okhttp3.Call
|
||||
@@ -64,7 +65,7 @@ class Nip05NostrAddressVerifier() {
|
||||
.url(url)
|
||||
.build()
|
||||
|
||||
HttpClient.getHttpClient()
|
||||
HttpClientManager.getHttpClient()
|
||||
.newCall(request)
|
||||
.enqueue(
|
||||
object : Callback {
|
||||
@@ -96,7 +97,8 @@ class Nip05NostrAddressVerifier() {
|
||||
}
|
||||
},
|
||||
)
|
||||
} catch (e: java.lang.Exception) {
|
||||
} catch (e: Exception) {
|
||||
if (e is CancellationException) throw e
|
||||
onError("Could not resolve '$url': ${e.message}")
|
||||
}
|
||||
}
|
||||
@@ -122,7 +124,8 @@ class Nip05NostrAddressVerifier() {
|
||||
val nip05url =
|
||||
try {
|
||||
mapper.readTree(it.lowercase())
|
||||
} catch (t: Throwable) {
|
||||
} catch (e: Throwable) {
|
||||
if (e is CancellationException) throw e
|
||||
onError("Error Parsing JSON from Lightning Address. Check the user's lightning setup")
|
||||
null
|
||||
}
|
||||
|
||||
@@ -23,6 +23,7 @@ package com.vitorpamplona.amethyst.service
|
||||
import android.util.Log
|
||||
import android.util.LruCache
|
||||
import com.vitorpamplona.amethyst.model.RelayInformation
|
||||
import kotlinx.coroutines.CancellationException
|
||||
import okhttp3.Call
|
||||
import okhttp3.Callback
|
||||
import okhttp3.Request
|
||||
@@ -84,7 +85,7 @@ class Nip11Retriever {
|
||||
val request: Request =
|
||||
Request.Builder().header("Accept", "application/nostr+json").url(url).build()
|
||||
|
||||
HttpClient.getHttpClient()
|
||||
HttpClientManager.getHttpClient()
|
||||
.newCall(request)
|
||||
.enqueue(
|
||||
object : Callback {
|
||||
@@ -102,6 +103,7 @@ class Nip11Retriever {
|
||||
onError(dirtyUrl, ErrorCode.FAIL_WITH_HTTP_STATUS, it.code.toString())
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
if (e is CancellationException) throw e
|
||||
Log.e(
|
||||
"RelayInfoFail",
|
||||
"Resulting Message from Relay $dirtyUrl in not parseable: $body",
|
||||
@@ -122,6 +124,7 @@ class Nip11Retriever {
|
||||
},
|
||||
)
|
||||
} catch (e: Exception) {
|
||||
if (e is CancellationException) throw e
|
||||
Log.e("RelayInfoFail", "Invalid URL $dirtyUrl", e)
|
||||
onError(dirtyUrl, ErrorCode.FAIL_TO_ASSEMBLE_URL, e.message)
|
||||
}
|
||||
|
||||
@@ -22,12 +22,14 @@ package com.vitorpamplona.amethyst.service
|
||||
|
||||
import java.net.URI
|
||||
import java.net.URLDecoder
|
||||
import kotlin.coroutines.cancellation.CancellationException
|
||||
|
||||
class Nip44UrlParser {
|
||||
fun parse(url: String): Map<String, String> {
|
||||
return try {
|
||||
fragments(URI(url))
|
||||
} catch (e: Exception) {
|
||||
if (e is CancellationException) throw e
|
||||
emptyMap()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -24,6 +24,7 @@ import android.net.Uri
|
||||
import com.vitorpamplona.amethyst.model.Nip47URI
|
||||
import com.vitorpamplona.quartz.encoders.decodePublicKey
|
||||
import com.vitorpamplona.quartz.encoders.toHexKey
|
||||
import kotlinx.coroutines.CancellationException
|
||||
|
||||
// Rename to the corect nip number when ready.
|
||||
object Nip47WalletConnectParser {
|
||||
@@ -42,6 +43,7 @@ object Nip47WalletConnectParser {
|
||||
try {
|
||||
decodePublicKey(pubkey).toHexKey()
|
||||
} catch (e: Exception) {
|
||||
if (e is CancellationException) throw e
|
||||
throw IllegalArgumentException("Hostname is not a valid Nostr Pubkey")
|
||||
}
|
||||
|
||||
|
||||
@@ -24,6 +24,7 @@ import android.util.Log
|
||||
import com.fasterxml.jackson.annotation.JsonProperty
|
||||
import com.fasterxml.jackson.databind.DeserializationFeature
|
||||
import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper
|
||||
import kotlinx.coroutines.CancellationException
|
||||
import okhttp3.Request
|
||||
|
||||
object Nip96MediaServers {
|
||||
@@ -87,7 +88,7 @@ class Nip96Retriever {
|
||||
.url(baseUrl.removeSuffix("/") + "/.well-known/nostr/nip96.json")
|
||||
.build()
|
||||
|
||||
HttpClient.getHttpClient().newCall(request).execute().use { response ->
|
||||
HttpClientManager.getHttpClient().newCall(request).execute().use { response ->
|
||||
checkNotInMainThread()
|
||||
response.use {
|
||||
val body = it.body.string()
|
||||
@@ -100,6 +101,7 @@ class Nip96Retriever {
|
||||
)
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
if (e is CancellationException) throw e
|
||||
Log.e("RelayInfoFail", "Resulting Message from $baseUrl in not parseable: $body", e)
|
||||
throw e
|
||||
}
|
||||
|
||||
@@ -128,7 +128,7 @@ class Nip96Uploader(val account: Account?) {
|
||||
val extension =
|
||||
contentType?.let { MimeTypeMap.getSingleton().getExtensionFromMimeType(it) } ?: ""
|
||||
|
||||
val client = HttpClient.getHttpClient()
|
||||
val client = HttpClientManager.getHttpClient()
|
||||
val requestBody: RequestBody
|
||||
val requestBuilder = Request.Builder()
|
||||
|
||||
@@ -194,7 +194,7 @@ class Nip96Uploader(val account: Account?) {
|
||||
val extension =
|
||||
contentType?.let { MimeTypeMap.getSingleton().getExtensionFromMimeType(it) } ?: ""
|
||||
|
||||
val client = HttpClient.getHttpClient()
|
||||
val client = HttpClientManager.getHttpClient()
|
||||
|
||||
val requestBuilder = Request.Builder()
|
||||
|
||||
@@ -227,7 +227,7 @@ class Nip96Uploader(val account: Account?) {
|
||||
server: Nip96Retriever.ServerInfo,
|
||||
onProgress: (percentage: Float) -> Unit,
|
||||
): PartialEvent {
|
||||
val client = HttpClient.getHttpClient()
|
||||
val client = HttpClientManager.getHttpClient()
|
||||
var currentResult = result
|
||||
|
||||
while (!result.processingUrl.isNullOrBlank() && (currentResult.percentage ?: 100) < 100) {
|
||||
|
||||
@@ -248,19 +248,11 @@ abstract class NostrDataSource(val debugName: String) {
|
||||
if (updatedSubscription.toJson() != currentFilters[updatedSubscription.id]) {
|
||||
Client.close(updatedSubscription.id)
|
||||
if (active) {
|
||||
Log.d(
|
||||
this@NostrDataSource.javaClass.simpleName,
|
||||
"Update Filter 1 ${updatedSubscription.id} ${Client.isSubscribed(clientListener)}",
|
||||
)
|
||||
Client.sendFilter(updatedSubscription.id, updatedSubscriptionNewFilters)
|
||||
}
|
||||
} else {
|
||||
// hasn't changed, does nothing.
|
||||
if (active) {
|
||||
Log.d(
|
||||
this@NostrDataSource.javaClass.simpleName,
|
||||
"Update Filter 2 ${updatedSubscription.id} ${Client.isSubscribed(clientListener)}",
|
||||
)
|
||||
Client.sendFilterOnlyIfDisconnected(
|
||||
updatedSubscription.id,
|
||||
updatedSubscriptionNewFilters,
|
||||
|
||||
@@ -46,6 +46,7 @@ import com.vitorpamplona.quartz.events.PeopleListEvent
|
||||
import com.vitorpamplona.quartz.events.PinListEvent
|
||||
import com.vitorpamplona.quartz.events.PollNoteEvent
|
||||
import com.vitorpamplona.quartz.events.TextNoteEvent
|
||||
import kotlin.coroutines.cancellation.CancellationException
|
||||
|
||||
object NostrSearchEventOrUserDataSource : NostrDataSource("SearchEventFeed") {
|
||||
private var searchString: String? = null
|
||||
@@ -67,6 +68,7 @@ object NostrSearchEventOrUserDataSource : NostrDataSource("SearchEventFeed") {
|
||||
|
||||
Nip19.uriToRoute(mySearchString)?.hex ?: isAStraightHex
|
||||
} catch (e: Exception) {
|
||||
if (e is CancellationException) throw e
|
||||
null
|
||||
}
|
||||
|
||||
|
||||
@@ -25,6 +25,7 @@ import android.util.LruCache
|
||||
import androidx.compose.runtime.Immutable
|
||||
import com.vitorpamplona.amethyst.BuildConfig
|
||||
import okhttp3.Request
|
||||
import kotlin.coroutines.cancellation.CancellationException
|
||||
|
||||
@Immutable data class OnlineCheckResult(val timeInMs: Long, val online: Boolean)
|
||||
|
||||
@@ -59,13 +60,14 @@ object OnlineChecker {
|
||||
.build()
|
||||
|
||||
val result =
|
||||
HttpClient.getHttpClient().newCall(request).execute().use {
|
||||
HttpClientManager.getHttpClient().newCall(request).execute().use {
|
||||
checkNotInMainThread()
|
||||
it.isSuccessful
|
||||
}
|
||||
checkOnlineCache.put(url, OnlineCheckResult(System.currentTimeMillis(), result))
|
||||
result
|
||||
} catch (e: Exception) {
|
||||
if (e is CancellationException) throw e
|
||||
checkOnlineCache.put(url, OnlineCheckResult(System.currentTimeMillis(), false))
|
||||
Log.e("LiveActivities", "Failed to check streaming url $url", e)
|
||||
false
|
||||
|
||||
@@ -21,6 +21,8 @@
|
||||
package com.vitorpamplona.amethyst.service
|
||||
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.net.Uri
|
||||
|
||||
object PackageUtils {
|
||||
private fun isPackageInstalled(
|
||||
@@ -36,7 +38,13 @@ object PackageUtils {
|
||||
return isPackageInstalled(context, "org.torproject.android")
|
||||
}
|
||||
|
||||
fun isAmberInstalled(context: Context): Boolean {
|
||||
return isPackageInstalled(context, "com.greenart7c3.nostrsigner")
|
||||
fun isExternalSignerInstalled(context: Context): Boolean {
|
||||
val intent =
|
||||
Intent().apply {
|
||||
action = Intent.ACTION_VIEW
|
||||
data = Uri.parse("nostrsigner:")
|
||||
}
|
||||
val infos = context.packageManager.queryIntentActivities(intent, 0)
|
||||
return infos.size > 0
|
||||
}
|
||||
}
|
||||
|
||||
@@ -24,7 +24,7 @@ import android.content.Context
|
||||
import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper
|
||||
import com.vitorpamplona.amethyst.BuildConfig
|
||||
import com.vitorpamplona.amethyst.R
|
||||
import com.vitorpamplona.amethyst.service.HttpClient
|
||||
import com.vitorpamplona.amethyst.service.HttpClientManager
|
||||
import com.vitorpamplona.amethyst.service.checkNotInMainThread
|
||||
import com.vitorpamplona.quartz.encoders.LnInvoiceUtil
|
||||
import com.vitorpamplona.quartz.encoders.Lud06
|
||||
@@ -33,9 +33,10 @@ import okhttp3.Request
|
||||
import java.math.BigDecimal
|
||||
import java.math.RoundingMode
|
||||
import java.net.URLEncoder
|
||||
import kotlin.coroutines.cancellation.CancellationException
|
||||
|
||||
class LightningAddressResolver() {
|
||||
val client = HttpClient.getHttpClient()
|
||||
val client = HttpClientManager.getHttpClient()
|
||||
|
||||
fun assembleUrl(lnaddress: String): String? {
|
||||
val parts = lnaddress.split("@")
|
||||
@@ -96,14 +97,16 @@ class LightningAddressResolver() {
|
||||
}
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
if (e is CancellationException) throw e
|
||||
e.printStackTrace()
|
||||
onError(
|
||||
context.getString(R.string.error_unable_to_fetch_invoice),
|
||||
context.getString(
|
||||
R.string
|
||||
.could_not_resolve_check_if_you_are_connected_if_the_server_is_up_and_if_the_lightning_address_is_correct,
|
||||
.could_not_resolve_check_if_you_are_connected_if_the_server_is_up_and_if_the_lightning_address_is_correct_exception,
|
||||
url,
|
||||
lnaddress,
|
||||
e.suppressedExceptions.getOrNull(0)?.message ?: e.cause?.message ?: e.message,
|
||||
),
|
||||
)
|
||||
}
|
||||
@@ -183,6 +186,7 @@ class LightningAddressResolver() {
|
||||
try {
|
||||
mapper.readTree(lnAddressJson)
|
||||
} catch (t: Throwable) {
|
||||
if (t is CancellationException) throw t
|
||||
onError(
|
||||
context.getString(R.string.error_unable_to_fetch_invoice),
|
||||
context.getString(
|
||||
@@ -218,6 +222,7 @@ class LightningAddressResolver() {
|
||||
try {
|
||||
mapper.readTree(it)
|
||||
} catch (t: Throwable) {
|
||||
if (t is CancellationException) throw t
|
||||
onError(
|
||||
context.getString(R.string.error_unable_to_fetch_invoice),
|
||||
context.getString(
|
||||
|
||||
@@ -25,8 +25,9 @@ import com.vitorpamplona.amethyst.AccountInfo
|
||||
import com.vitorpamplona.amethyst.BuildConfig
|
||||
import com.vitorpamplona.amethyst.LocalPreferences
|
||||
import com.vitorpamplona.amethyst.model.Account
|
||||
import com.vitorpamplona.amethyst.service.HttpClient
|
||||
import com.vitorpamplona.amethyst.service.HttpClientManager
|
||||
import com.vitorpamplona.quartz.events.RelayAuthEvent
|
||||
import kotlinx.coroutines.CancellationException
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.withContext
|
||||
import okhttp3.MediaType.Companion.toMediaType
|
||||
@@ -106,10 +107,11 @@ class RegisterAccounts(
|
||||
.post(body)
|
||||
.build()
|
||||
|
||||
val client = HttpClient.getHttpClient()
|
||||
val client = HttpClientManager.getHttpClient()
|
||||
|
||||
val isSucess = client.newCall(request).execute().use { it.isSuccessful }
|
||||
} catch (e: java.lang.Exception) {
|
||||
if (e is CancellationException) throw e
|
||||
val tag =
|
||||
if (BuildConfig.FLAVOR == "play") {
|
||||
"FirebaseMsgService"
|
||||
|
||||
@@ -28,6 +28,7 @@ import android.util.LruCache
|
||||
import androidx.media3.session.MediaController
|
||||
import androidx.media3.session.SessionToken
|
||||
import com.google.common.util.concurrent.MoreExecutors
|
||||
import kotlinx.coroutines.CancellationException
|
||||
|
||||
object PlaybackClientController {
|
||||
val cache = LruCache<Int, SessionToken>(1)
|
||||
@@ -62,12 +63,14 @@ object PlaybackClientController {
|
||||
try {
|
||||
onReady(controllerFuture.get())
|
||||
} catch (e: Exception) {
|
||||
if (e is CancellationException) throw e
|
||||
Log.e("Playback Client", "Failed to load Playback Client for $videoUri", e)
|
||||
}
|
||||
},
|
||||
MoreExecutors.directExecutor(),
|
||||
)
|
||||
} catch (e: Exception) {
|
||||
if (e is CancellationException) throw e
|
||||
Log.e("Playback Client", "Failed to load Playback Client for $videoUri", e)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -31,7 +31,7 @@ import androidx.media3.exoplayer.source.ProgressiveMediaSource
|
||||
import androidx.media3.session.MediaSession
|
||||
import androidx.media3.session.MediaSessionService
|
||||
import com.vitorpamplona.amethyst.Amethyst
|
||||
import com.vitorpamplona.amethyst.service.HttpClient
|
||||
import com.vitorpamplona.amethyst.service.HttpClientManager
|
||||
|
||||
@UnstableApi // Extend MediaSessionService
|
||||
class PlaybackService : MediaSessionService() {
|
||||
@@ -42,12 +42,12 @@ class PlaybackService : MediaSessionService() {
|
||||
private var managerLocal: MultiPlayerPlaybackManager? = null
|
||||
|
||||
fun newHslDataSource(): MediaSource.Factory {
|
||||
return HlsMediaSource.Factory(OkHttpDataSource.Factory(HttpClient.getHttpClient()))
|
||||
return HlsMediaSource.Factory(OkHttpDataSource.Factory(HttpClientManager.getHttpClient()))
|
||||
}
|
||||
|
||||
fun newProgressiveDataSource(): MediaSource.Factory {
|
||||
return ProgressiveMediaSource.Factory(
|
||||
(applicationContext as Amethyst).videoCache.get(HttpClient.getHttpClient()),
|
||||
(applicationContext as Amethyst).videoCache.get(HttpClientManager.getHttpClient()),
|
||||
)
|
||||
}
|
||||
|
||||
@@ -90,7 +90,7 @@ class PlaybackService : MediaSessionService() {
|
||||
Log.d("Lifetime Event", "PlaybackService.onCreate")
|
||||
|
||||
// Stop all videos and recreates all managers when the proxy changes.
|
||||
HttpClient.proxyChangeListeners.add(this@PlaybackService::onProxyUpdated)
|
||||
HttpClientManager.proxyChangeListeners.add(this@PlaybackService::onProxyUpdated)
|
||||
}
|
||||
|
||||
private fun onProxyUpdated() {
|
||||
@@ -114,7 +114,7 @@ class PlaybackService : MediaSessionService() {
|
||||
override fun onDestroy() {
|
||||
Log.d("Lifetime Event", "PlaybackService.onDestroy")
|
||||
|
||||
HttpClient.proxyChangeListeners.remove(this@PlaybackService::onProxyUpdated)
|
||||
HttpClientManager.proxyChangeListeners.remove(this@PlaybackService::onProxyUpdated)
|
||||
|
||||
managerHls?.releaseAppPlayers()
|
||||
managerLocal?.releaseAppPlayers()
|
||||
|
||||
@@ -20,6 +20,7 @@
|
||||
*/
|
||||
package com.vitorpamplona.amethyst.service.previews
|
||||
|
||||
import kotlinx.coroutines.CancellationException
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.withContext
|
||||
|
||||
@@ -29,6 +30,7 @@ class BahaUrlPreview(val url: String, var callback: IUrlPreviewCallback?) {
|
||||
try {
|
||||
fetch(timeOut)
|
||||
} catch (t: Throwable) {
|
||||
if (t is CancellationException) throw t
|
||||
callback?.onFailed(t)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -20,7 +20,7 @@
|
||||
*/
|
||||
package com.vitorpamplona.amethyst.service.previews
|
||||
|
||||
import com.vitorpamplona.amethyst.service.HttpClient
|
||||
import com.vitorpamplona.amethyst.service.HttpClientManager
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.withContext
|
||||
import okhttp3.MediaType
|
||||
@@ -90,7 +90,7 @@ suspend fun getDocument(
|
||||
): UrlInfoItem =
|
||||
withContext(Dispatchers.IO) {
|
||||
val request: Request = Request.Builder().url(url).get().build()
|
||||
HttpClient.getHttpClient().newCall(request).execute().use {
|
||||
HttpClientManager.getHttpClient().newCall(request).execute().use {
|
||||
if (it.isSuccessful) {
|
||||
val mimeType =
|
||||
it.headers.get("Content-Type")?.toMediaType()
|
||||
|
||||
@@ -62,8 +62,8 @@ object Constants {
|
||||
write = true,
|
||||
feedTypes = activeTypesChats,
|
||||
),
|
||||
RelaySetupInfo("wss://nostr.mom", read = true, write = true, feedTypes = activeTypesChats),
|
||||
RelaySetupInfo("wss://nos.lol", read = true, write = true, feedTypes = activeTypesChats),
|
||||
RelaySetupInfo("wss://nostr.mom", read = true, write = true, feedTypes = activeTypesGlobalChats),
|
||||
RelaySetupInfo("wss://nos.lol", read = true, write = true, feedTypes = activeTypesGlobalChats),
|
||||
// Less Reliable
|
||||
// NewRelayListViewModel.Relay("wss://nostr.orangepill.dev", read = true, write = true,
|
||||
// feedTypes = activeTypes),
|
||||
|
||||
@@ -23,7 +23,7 @@ package com.vitorpamplona.amethyst.service.relays
|
||||
import android.util.Log
|
||||
import com.vitorpamplona.amethyst.BuildConfig
|
||||
import com.vitorpamplona.amethyst.model.RelayBriefInfoCache
|
||||
import com.vitorpamplona.amethyst.service.HttpClient
|
||||
import com.vitorpamplona.amethyst.service.HttpClientManager
|
||||
import com.vitorpamplona.amethyst.service.checkNotInMainThread
|
||||
import com.vitorpamplona.quartz.encoders.HexKey
|
||||
import com.vitorpamplona.quartz.events.Event
|
||||
@@ -31,11 +31,11 @@ import com.vitorpamplona.quartz.events.EventInterface
|
||||
import com.vitorpamplona.quartz.events.RelayAuthEvent
|
||||
import com.vitorpamplona.quartz.events.bytesUsedInMemory
|
||||
import com.vitorpamplona.quartz.utils.TimeUtils
|
||||
import kotlinx.coroutines.CancellationException
|
||||
import okhttp3.Request
|
||||
import okhttp3.Response
|
||||
import okhttp3.WebSocket
|
||||
import okhttp3.WebSocketListener
|
||||
import java.lang.StringBuilder
|
||||
import java.util.concurrent.atomic.AtomicBoolean
|
||||
|
||||
enum class FeedType {
|
||||
@@ -63,7 +63,7 @@ class Relay(
|
||||
const val RECONNECTING_IN_SECONDS = 60 * 3
|
||||
}
|
||||
|
||||
private val httpClient = HttpClient.getHttpClientForRelays()
|
||||
private val httpClient = HttpClientManager.getHttpClient()
|
||||
|
||||
private var listeners = setOf<Listener>()
|
||||
private var socket: WebSocket? = null
|
||||
@@ -107,7 +107,7 @@ class Relay(
|
||||
private var connectingBlock = AtomicBoolean()
|
||||
|
||||
fun connectAndRun(onConnected: (Relay) -> Unit) {
|
||||
Log.d("Relay", "Relay.connect $url")
|
||||
Log.d("Relay", "Relay.connect $url hasProxy: ${this.httpClient.proxy != null}")
|
||||
// BRB is crashing OkHttp Deflater object :(
|
||||
if (url.contains("brb.io")) return
|
||||
|
||||
@@ -129,6 +129,8 @@ class Relay(
|
||||
|
||||
socket = httpClient.newWebSocket(request, RelayListener(onConnected))
|
||||
} catch (e: Exception) {
|
||||
if (e is CancellationException) throw e
|
||||
|
||||
errorCounter++
|
||||
markConnectionAsClosed()
|
||||
Log.e("Relay", "Relay Invalid $url")
|
||||
@@ -168,8 +170,9 @@ class Relay(
|
||||
|
||||
try {
|
||||
processNewRelayMessage(text)
|
||||
} catch (t: Throwable) {
|
||||
t.printStackTrace()
|
||||
} catch (e: Throwable) {
|
||||
if (e is CancellationException) throw e
|
||||
e.printStackTrace()
|
||||
text.chunked(2000) { chunked ->
|
||||
listeners.forEach { it.onError(this@Relay, "", Error("Problem with $chunked")) }
|
||||
}
|
||||
|
||||
@@ -45,7 +45,7 @@ import androidx.lifecycle.viewmodel.compose.viewModel
|
||||
import com.google.accompanist.adaptive.calculateDisplayFeatures
|
||||
import com.vitorpamplona.amethyst.LocalPreferences
|
||||
import com.vitorpamplona.amethyst.ServiceManager
|
||||
import com.vitorpamplona.amethyst.service.HttpClient
|
||||
import com.vitorpamplona.amethyst.service.HttpClientManager
|
||||
import com.vitorpamplona.amethyst.service.lang.LanguageTranslatorService
|
||||
import com.vitorpamplona.amethyst.service.notifications.PushNotificationUtils
|
||||
import com.vitorpamplona.amethyst.ui.components.DEFAULT_MUTED_SETTING
|
||||
@@ -64,6 +64,7 @@ import com.vitorpamplona.quartz.events.ChannelMetadataEvent
|
||||
import com.vitorpamplona.quartz.events.CommunityDefinitionEvent
|
||||
import com.vitorpamplona.quartz.events.LiveActivitiesEvent
|
||||
import com.vitorpamplona.quartz.events.PrivateDmEvent
|
||||
import kotlinx.coroutines.CancellationException
|
||||
import kotlinx.coroutines.DelicateCoroutinesApi
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.GlobalScope
|
||||
@@ -236,9 +237,9 @@ class MainActivity : AppCompatActivity() {
|
||||
|
||||
if (changedNetwork) {
|
||||
if (isOnMobileData) {
|
||||
HttpClient.changeTimeouts(HttpClient.DEFAULT_TIMEOUT_ON_MOBILE)
|
||||
HttpClientManager.setDefaultTimeout(HttpClientManager.DEFAULT_TIMEOUT_ON_MOBILE)
|
||||
} else {
|
||||
HttpClient.changeTimeouts(HttpClient.DEFAULT_TIMEOUT_ON_WIFI)
|
||||
HttpClientManager.setDefaultTimeout(HttpClientManager.DEFAULT_TIMEOUT_ON_WIFI)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -341,6 +342,7 @@ fun uriToRoute(uri: String?): String? {
|
||||
Route.Home.base + "?nip47=" + encodedUri
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
if (e is CancellationException) throw e
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
@@ -20,6 +20,7 @@
|
||||
*/
|
||||
package com.vitorpamplona.amethyst.ui.actions
|
||||
|
||||
import kotlinx.coroutines.CancellationException
|
||||
import kotlinx.coroutines.delay
|
||||
import java.net.HttpURLConnection
|
||||
import java.net.URL
|
||||
@@ -57,6 +58,7 @@ class ImageDownloader {
|
||||
null
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
if (e is CancellationException) throw e
|
||||
tentatives++
|
||||
delay(1000)
|
||||
null
|
||||
|
||||
@@ -30,7 +30,8 @@ import android.provider.MediaStore
|
||||
import android.webkit.MimeTypeMap
|
||||
import androidx.annotation.RequiresApi
|
||||
import com.vitorpamplona.amethyst.BuildConfig
|
||||
import com.vitorpamplona.amethyst.service.HttpClient
|
||||
import com.vitorpamplona.amethyst.service.HttpClientManager
|
||||
import kotlinx.coroutines.CancellationException
|
||||
import okhttp3.Call
|
||||
import okhttp3.Callback
|
||||
import okhttp3.Request
|
||||
@@ -55,7 +56,7 @@ object ImageSaver {
|
||||
onSuccess: () -> Any?,
|
||||
onError: (Throwable) -> Any?,
|
||||
) {
|
||||
val client = HttpClient.getHttpClient()
|
||||
val client = HttpClientManager.getHttpClient()
|
||||
|
||||
val request =
|
||||
Request.Builder()
|
||||
@@ -102,6 +103,7 @@ object ImageSaver {
|
||||
}
|
||||
onSuccess()
|
||||
} catch (e: Exception) {
|
||||
if (e is CancellationException) throw e
|
||||
e.printStackTrace()
|
||||
onError(e)
|
||||
}
|
||||
@@ -138,6 +140,7 @@ object ImageSaver {
|
||||
}
|
||||
onSuccess()
|
||||
} catch (e: Exception) {
|
||||
if (e is CancellationException) throw e
|
||||
e.printStackTrace()
|
||||
onError(e)
|
||||
}
|
||||
@@ -176,6 +179,7 @@ object ImageSaver {
|
||||
|
||||
outputStream.use { contentSource.readAll(it.sink()) }
|
||||
} catch (e: Exception) {
|
||||
if (e is CancellationException) throw e
|
||||
contentResolver.delete(uri, null, null)
|
||||
throw e
|
||||
}
|
||||
|
||||
@@ -36,6 +36,7 @@ import com.vitorpamplona.amethyst.service.Nip96MediaServers
|
||||
import com.vitorpamplona.amethyst.service.Nip96Uploader
|
||||
import com.vitorpamplona.amethyst.service.relays.Relay
|
||||
import com.vitorpamplona.amethyst.ui.components.MediaCompressor
|
||||
import kotlinx.coroutines.CancellationException
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
@@ -148,6 +149,7 @@ open class NewMediaModel : ViewModel() {
|
||||
context,
|
||||
)
|
||||
} catch (e: Exception) {
|
||||
if (e is CancellationException) throw e
|
||||
isUploadingImage = false
|
||||
uploadingPercentage.value = 0.00f
|
||||
uploadingDescription.value = null
|
||||
|
||||
@@ -72,6 +72,7 @@ import com.vitorpamplona.amethyst.ui.screen.loggedIn.TextSpinner
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.TitleExplainer
|
||||
import com.vitorpamplona.amethyst.ui.theme.placeholderText
|
||||
import kotlinx.collections.immutable.toImmutableList
|
||||
import kotlinx.coroutines.CancellationException
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
@@ -231,6 +232,7 @@ fun ImageVideoPost(
|
||||
try {
|
||||
bitmap = resolver.loadThumbnail(it, Size(1200, 1000), null)
|
||||
} catch (e: Exception) {
|
||||
if (e is CancellationException) throw e
|
||||
Log.w("NewPostView", "Couldn't create thumbnail, but the video can be uploaded", e)
|
||||
}
|
||||
}
|
||||
@@ -279,6 +281,7 @@ fun ImageVideoPost(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
) {
|
||||
SettingSwitchItem(
|
||||
modifier = Modifier.fillMaxWidth().padding(8.dp),
|
||||
checked = postViewModel.sensitiveContent,
|
||||
onCheckedChange = { postViewModel.sensitiveContent = it },
|
||||
title = R.string.add_sensitive_content_label,
|
||||
|
||||
@@ -29,6 +29,7 @@ import com.vitorpamplona.quartz.encoders.HexKey
|
||||
import com.vitorpamplona.quartz.encoders.Nip19
|
||||
import com.vitorpamplona.quartz.encoders.bechToBytes
|
||||
import com.vitorpamplona.quartz.encoders.toNpub
|
||||
import kotlinx.coroutines.CancellationException
|
||||
|
||||
class NewMessageTagger(
|
||||
var message: String,
|
||||
@@ -196,6 +197,7 @@ class NewMessageTagger(
|
||||
) // no way to know when they address ends and dirt begins
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
if (e is CancellationException) throw e
|
||||
e.printStackTrace()
|
||||
}
|
||||
|
||||
|
||||
@@ -43,6 +43,7 @@ import androidx.compose.ui.unit.dp
|
||||
import com.vitorpamplona.amethyst.R
|
||||
import com.vitorpamplona.amethyst.ui.actions.NewPostViewModel
|
||||
import com.vitorpamplona.amethyst.ui.theme.placeholderText
|
||||
import kotlinx.coroutines.CancellationException
|
||||
|
||||
@Composable
|
||||
fun NewPollClosing(pollViewModel: NewPostViewModel) {
|
||||
@@ -58,6 +59,7 @@ fun NewPollClosing(pollViewModel: NewPostViewModel) {
|
||||
pollViewModel.closedAt = int
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
if (e is CancellationException) throw e
|
||||
pollViewModel.isValidClosedAt.value = false
|
||||
}
|
||||
}
|
||||
|
||||
@@ -43,6 +43,7 @@ import androidx.compose.ui.unit.dp
|
||||
import com.vitorpamplona.amethyst.R
|
||||
import com.vitorpamplona.amethyst.ui.actions.NewPostViewModel
|
||||
import com.vitorpamplona.amethyst.ui.theme.placeholderText
|
||||
import kotlinx.coroutines.CancellationException
|
||||
|
||||
@Composable
|
||||
fun NewPollConsensusThreshold(pollViewModel: NewPostViewModel) {
|
||||
@@ -58,6 +59,7 @@ fun NewPollConsensusThreshold(pollViewModel: NewPostViewModel) {
|
||||
pollViewModel.consensusThreshold = int
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
if (e is CancellationException) throw e
|
||||
pollViewModel.isValidConsensusThreshold.value = false
|
||||
}
|
||||
}
|
||||
|
||||
@@ -179,6 +179,7 @@ import com.vitorpamplona.quartz.events.ClassifiedsEvent
|
||||
import com.vitorpamplona.quartz.events.toImmutableListOfLists
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
import kotlinx.collections.immutable.toImmutableList
|
||||
import kotlinx.coroutines.CancellationException
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
@@ -1674,6 +1675,7 @@ fun ImageVideoDescription(
|
||||
try {
|
||||
bitmap = resolver.loadThumbnail(uri, Size(1200, 1000), null)
|
||||
} catch (e: Exception) {
|
||||
if (e is CancellationException) throw e
|
||||
onError("Unable to load thumbnail")
|
||||
Log.w("NewPostView", "Couldn't create thumbnail, but the video can be uploaded", e)
|
||||
}
|
||||
@@ -1716,6 +1718,7 @@ fun ImageVideoDescription(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
) {
|
||||
SettingSwitchItem(
|
||||
modifier = Modifier.fillMaxWidth().padding(vertical = 8.dp),
|
||||
checked = sensitiveContent,
|
||||
onCheckedChange = { sensitiveContent = it },
|
||||
title = R.string.add_sensitive_content_label,
|
||||
@@ -1764,7 +1767,7 @@ fun ImageVideoDescription(
|
||||
|
||||
@Composable
|
||||
fun SettingSwitchItem(
|
||||
modifier: Modifier = Modifier,
|
||||
modifier: Modifier = Modifier.fillMaxWidth().padding(horizontal = 16.dp, vertical = 8.dp),
|
||||
checked: Boolean,
|
||||
onCheckedChange: (Boolean) -> Unit,
|
||||
title: Int,
|
||||
@@ -1774,8 +1777,6 @@ fun SettingSwitchItem(
|
||||
Row(
|
||||
modifier =
|
||||
modifier
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = 16.dp, vertical = 8.dp)
|
||||
.toggleable(
|
||||
value = checked,
|
||||
enabled = enabled,
|
||||
@@ -1786,7 +1787,7 @@ fun SettingSwitchItem(
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier.weight(1.0f),
|
||||
verticalArrangement = Arrangement.spacedBy(3.dp),
|
||||
verticalArrangement = Arrangement.spacedBy(Size5dp),
|
||||
) {
|
||||
Text(
|
||||
text = stringResource(id = title),
|
||||
|
||||
@@ -63,6 +63,7 @@ import com.vitorpamplona.quartz.events.PrivateDmEvent
|
||||
import com.vitorpamplona.quartz.events.TextNoteEvent
|
||||
import com.vitorpamplona.quartz.events.ZapSplitSetup
|
||||
import com.vitorpamplona.quartz.events.findURLs
|
||||
import kotlinx.coroutines.CancellationException
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||
import kotlinx.coroutines.channels.BufferOverflow
|
||||
@@ -264,7 +265,7 @@ open class NewPostViewModel() : ViewModel() {
|
||||
}
|
||||
|
||||
val urls = findURLs(tagger.message)
|
||||
val usedAttachments = nip94attachments.filter { it.urls().intersect(urls).isNotEmpty() }
|
||||
val usedAttachments = nip94attachments.filter { it.urls().intersect(urls.toSet()).isNotEmpty() }
|
||||
usedAttachments.forEach { account?.sendHeader(it, relayList, {}) }
|
||||
|
||||
if (originalNote?.channelHex() != null) {
|
||||
@@ -476,6 +477,7 @@ open class NewPostViewModel() : ViewModel() {
|
||||
)
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
if (e is CancellationException) throw e
|
||||
Log.e(
|
||||
"ImageUploader",
|
||||
"Failed to upload ${e.message}",
|
||||
@@ -846,9 +848,7 @@ open class NewPostViewModel() : ViewModel() {
|
||||
alt?.ifBlank { null }?.let { "alt=${URLEncoder.encode(it, "utf-8")}" },
|
||||
blurHash?.ifBlank { null }?.let { "blurhash=${URLEncoder.encode(it, "utf-8")}" },
|
||||
x?.ifBlank { null }?.let { "x=${URLEncoder.encode(it, "utf-8")}" },
|
||||
sensitiveContent
|
||||
?.ifBlank { null }
|
||||
?.let { "content-warning=${URLEncoder.encode(it, "utf-8")}" },
|
||||
sensitiveContent?.let { "content-warning=${URLEncoder.encode(it, "utf-8")}" },
|
||||
)
|
||||
.joinToString("&")
|
||||
|
||||
@@ -944,6 +944,7 @@ open class NewPostViewModel() : ViewModel() {
|
||||
valueMinimum = int
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
if (e is CancellationException) throw e
|
||||
}
|
||||
} else {
|
||||
valueMinimum = null
|
||||
@@ -962,6 +963,7 @@ open class NewPostViewModel() : ViewModel() {
|
||||
valueMaximum = int
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
if (e is CancellationException) throw e
|
||||
}
|
||||
} else {
|
||||
valueMaximum = null
|
||||
|
||||
@@ -27,8 +27,6 @@ import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import com.fasterxml.jackson.databind.ObjectMapper
|
||||
import com.fasterxml.jackson.databind.node.ObjectNode
|
||||
import com.vitorpamplona.amethyst.model.Account
|
||||
import com.vitorpamplona.amethyst.service.Nip96Uploader
|
||||
import com.vitorpamplona.amethyst.ui.components.MediaCompressor
|
||||
@@ -39,8 +37,7 @@ import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.channels.BufferOverflow
|
||||
import kotlinx.coroutines.flow.MutableSharedFlow
|
||||
import kotlinx.coroutines.launch
|
||||
import java.io.ByteArrayInputStream
|
||||
import java.io.StringWriter
|
||||
import kotlin.coroutines.cancellation.CancellationException
|
||||
|
||||
class NewUserMetadataViewModel : ViewModel() {
|
||||
private lateinit var account: Account
|
||||
@@ -97,59 +94,22 @@ class NewUserMetadataViewModel : ViewModel() {
|
||||
|
||||
fun create() {
|
||||
// Tries to not delete any existing attribute that we do not work with.
|
||||
val latest = account.userProfile().info?.latestMetadata
|
||||
val currentJson =
|
||||
if (latest != null) {
|
||||
ObjectMapper()
|
||||
.readTree(
|
||||
ByteArrayInputStream(latest.content.toByteArray(Charsets.UTF_8)),
|
||||
) as ObjectNode
|
||||
} else {
|
||||
ObjectMapper().createObjectNode()
|
||||
}
|
||||
currentJson.put("name", displayName.value.trim())
|
||||
currentJson.put("display_name", displayName.value.trim())
|
||||
currentJson.put("picture", picture.value.trim())
|
||||
currentJson.put("banner", banner.value.trim())
|
||||
currentJson.put("website", website.value.trim())
|
||||
currentJson.put("about", about.value.trim())
|
||||
currentJson.put("nip05", nip05.value.trim())
|
||||
currentJson.put("lud16", lnAddress.value.trim())
|
||||
currentJson.put("lud06", lnURL.value.trim())
|
||||
|
||||
var claims = latest?.identityClaims() ?: emptyList()
|
||||
|
||||
if (twitter.value.isBlank()) {
|
||||
// delete twitter
|
||||
claims = claims.filter { it !is TwitterIdentity }
|
||||
}
|
||||
|
||||
if (github.value.isBlank()) {
|
||||
// delete github
|
||||
claims = claims.filter { it !is GitHubIdentity }
|
||||
}
|
||||
|
||||
if (mastodon.value.isBlank()) {
|
||||
// delete mastodon
|
||||
claims = claims.filter { it !is MastodonIdentity }
|
||||
}
|
||||
|
||||
// Updates while keeping other identities intact
|
||||
val newClaims =
|
||||
listOfNotNull(
|
||||
TwitterIdentity.parseProofUrl(twitter.value),
|
||||
GitHubIdentity.parseProofUrl(github.value),
|
||||
MastodonIdentity.parseProofUrl(mastodon.value),
|
||||
) +
|
||||
claims.filter { it !is TwitterIdentity && it !is GitHubIdentity && it !is MastodonIdentity }
|
||||
|
||||
val writer = StringWriter()
|
||||
ObjectMapper().writeValue(writer, currentJson)
|
||||
|
||||
viewModelScope.launch(Dispatchers.IO) {
|
||||
account.sendNewUserMetadata(writer.buffer.toString(), displayName.value.trim(), newClaims)
|
||||
account.sendNewUserMetadata(
|
||||
name = displayName.value,
|
||||
picture = picture.value,
|
||||
banner = banner.value,
|
||||
website = website.value,
|
||||
about = about.value,
|
||||
nip05 = nip05.value,
|
||||
lnAddress = lnAddress.value,
|
||||
lnURL = lnURL.value,
|
||||
twitter = twitter.value,
|
||||
mastodon = mastodon.value,
|
||||
github = github.value,
|
||||
)
|
||||
clear()
|
||||
}
|
||||
clear()
|
||||
}
|
||||
|
||||
fun clear() {
|
||||
@@ -238,6 +198,7 @@ class NewUserMetadataViewModel : ViewModel() {
|
||||
}
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
if (e is CancellationException) throw e
|
||||
onUploading(false)
|
||||
viewModelScope.launch {
|
||||
imageUploadingError.emit("Failed to upload the image / video")
|
||||
|
||||
@@ -33,6 +33,7 @@ import androidx.compose.ui.text.style.TextDecoration
|
||||
import com.vitorpamplona.amethyst.model.LocalCache
|
||||
import com.vitorpamplona.quartz.encoders.decodePublicKey
|
||||
import com.vitorpamplona.quartz.encoders.toHexKey
|
||||
import kotlin.coroutines.cancellation.CancellationException
|
||||
import kotlin.math.roundToInt
|
||||
|
||||
data class RangesChanges(val original: TextRange, val modified: TextRange)
|
||||
@@ -113,6 +114,7 @@ fun buildAnnotatedStringWithUrlHighlighting(
|
||||
word
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
if (e is CancellationException) throw e
|
||||
// if it can't parse the key, don't try to change.
|
||||
builderBefore.append("$word ")
|
||||
builderAfter.append("$word ")
|
||||
|
||||
@@ -83,6 +83,7 @@ import com.vitorpamplona.amethyst.ui.theme.Size20Modifier
|
||||
import com.vitorpamplona.amethyst.ui.theme.SmallishBorder
|
||||
import com.vitorpamplona.amethyst.ui.theme.StdHorzSpacer
|
||||
import com.vitorpamplona.amethyst.ui.theme.subtleBorder
|
||||
import kotlinx.coroutines.CancellationException
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
@@ -238,6 +239,7 @@ fun CashuPreview(
|
||||
|
||||
startActivity(context, intent, null)
|
||||
} catch (e: Exception) {
|
||||
if (e is CancellationException) throw e
|
||||
toast("Cashu", context.getString(R.string.cashu_no_wallet_found))
|
||||
}
|
||||
},
|
||||
@@ -354,6 +356,7 @@ fun CashuPreviewNew(
|
||||
|
||||
startActivity(context, intent, null)
|
||||
} catch (e: Exception) {
|
||||
if (e is CancellationException) throw e
|
||||
toast("Cashu", context.getString(R.string.cashu_no_wallet_found))
|
||||
}
|
||||
},
|
||||
|
||||
@@ -30,6 +30,7 @@ import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.text.AnnotatedString
|
||||
import kotlinx.coroutines.CancellationException
|
||||
|
||||
@Composable
|
||||
fun ClickableEmail(email: String) {
|
||||
@@ -58,6 +59,7 @@ fun Context.sendMail(
|
||||
} catch (e: ActivityNotFoundException) {
|
||||
// TODO: Handle case where no email app is available
|
||||
} catch (t: Throwable) {
|
||||
if (t is CancellationException) throw t
|
||||
// TODO: Handle potential other type of exceptions
|
||||
}
|
||||
}
|
||||
|
||||
@@ -280,12 +280,10 @@ private fun RenderUserAsClickableText(
|
||||
derivedStateOf { userState?.user?.info?.latestMetadata?.tags?.toImmutableListOfLists() }
|
||||
}
|
||||
|
||||
val addedCharts = remember(nip19) { "${nip19.additionalChars}" }
|
||||
|
||||
userDisplayName?.let {
|
||||
CreateClickableTextWithEmoji(
|
||||
clickablePart = it,
|
||||
suffix = addedCharts,
|
||||
suffix = nip19.additionalChars,
|
||||
maxLines = 1,
|
||||
route = route,
|
||||
nav = nav,
|
||||
@@ -335,6 +333,7 @@ fun CreateClickableText(
|
||||
ClickableText(
|
||||
text = text,
|
||||
maxLines = maxLines,
|
||||
style = currentStyle,
|
||||
onClick = { nav(route) },
|
||||
)
|
||||
}
|
||||
@@ -388,7 +387,7 @@ fun CreateTextWithEmoji(
|
||||
.merge(
|
||||
TextStyle(
|
||||
color = textColor,
|
||||
textAlign = textAlign,
|
||||
textAlign = TextAlign.Unspecified,
|
||||
fontWeight = fontWeight,
|
||||
fontSize = fontSize,
|
||||
),
|
||||
@@ -446,7 +445,7 @@ fun CreateTextWithEmoji(
|
||||
.merge(
|
||||
TextStyle(
|
||||
color = textColor,
|
||||
textAlign = textAlign,
|
||||
textAlign = TextAlign.Unspecified,
|
||||
fontWeight = fontWeight,
|
||||
fontSize = fontSize,
|
||||
),
|
||||
|
||||
@@ -58,6 +58,7 @@ import com.vitorpamplona.amethyst.ui.theme.QuoteBorder
|
||||
import com.vitorpamplona.amethyst.ui.theme.Size20Modifier
|
||||
import com.vitorpamplona.amethyst.ui.theme.subtleBorder
|
||||
import com.vitorpamplona.quartz.encoders.LnInvoiceUtil
|
||||
import kotlinx.coroutines.CancellationException
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import java.text.NumberFormat
|
||||
@@ -79,6 +80,7 @@ fun LoadValueFromInvoice(
|
||||
try {
|
||||
NumberFormat.getInstance().format(LnInvoiceUtil.getAmountInSats(myInvoice))
|
||||
} catch (e: Exception) {
|
||||
if (e is CancellationException) throw e
|
||||
e.printStackTrace()
|
||||
null
|
||||
}
|
||||
|
||||
@@ -27,6 +27,7 @@ import com.vitorpamplona.amethyst.service.checkNotInMainThread
|
||||
import com.vitorpamplona.amethyst.service.startsWithNIP19Scheme
|
||||
import com.vitorpamplona.quartz.encoders.Nip19
|
||||
import com.vitorpamplona.quartz.events.ImmutableListOfLists
|
||||
import kotlinx.coroutines.CancellationException
|
||||
|
||||
class MarkdownParser {
|
||||
private fun getDisplayNameAndNIP19FromTag(
|
||||
@@ -39,6 +40,7 @@ class MarkdownParser {
|
||||
matcher.find()
|
||||
Pair(matcher.group(1)?.toInt(), matcher.group(2) ?: "")
|
||||
} catch (e: Exception) {
|
||||
if (e is CancellationException) throw e
|
||||
Log.w("Tag Parser", "Couldn't link tag $tag", e)
|
||||
Pair(null, null)
|
||||
}
|
||||
@@ -161,6 +163,7 @@ class MarkdownParser {
|
||||
hashtagMatcher.find()
|
||||
Pair(hashtagMatcher.group(1), hashtagMatcher.group(2))
|
||||
} catch (e: Exception) {
|
||||
if (e is CancellationException) throw e
|
||||
Log.e("Hashtag Parser", "Couldn't link hashtag $word", e)
|
||||
Pair(null, null)
|
||||
}
|
||||
|
||||
@@ -33,6 +33,7 @@ import com.abedelazizshe.lightcompressorlibrary.config.Configuration
|
||||
import com.vitorpamplona.amethyst.service.checkNotInMainThread
|
||||
import id.zelory.compressor.Compressor
|
||||
import id.zelory.compressor.constraint.default
|
||||
import kotlinx.coroutines.CancellationException
|
||||
import java.io.File
|
||||
import java.io.FileOutputStream
|
||||
import java.util.UUID
|
||||
@@ -115,6 +116,7 @@ class MediaCompressor {
|
||||
}
|
||||
onReady(compressedImageFile.toUri(), contentType, compressedImageFile.length())
|
||||
} catch (e: Exception) {
|
||||
if (e is CancellationException) throw e
|
||||
e.printStackTrace()
|
||||
onReady(uri, contentType, null)
|
||||
}
|
||||
|
||||
@@ -580,7 +580,7 @@ fun BechLink(
|
||||
)
|
||||
}
|
||||
} else if (loadedLink?.nip19 != null) {
|
||||
Row { ClickableRoute(loadedLink?.nip19!!, accountViewModel, nav) }
|
||||
ClickableRoute(loadedLink?.nip19!!, accountViewModel, nav)
|
||||
} else {
|
||||
val text =
|
||||
remember(word) {
|
||||
|
||||
@@ -74,8 +74,17 @@ fun SensitivityWarning(
|
||||
accountViewModel: AccountViewModel,
|
||||
content: @Composable () -> Unit,
|
||||
) {
|
||||
val hasSensitiveContent = remember(event) { event.isSensitive() ?: false }
|
||||
val hasSensitiveContent = remember(event) { event.isSensitive() }
|
||||
|
||||
SensitivityWarning(hasSensitiveContent, accountViewModel, content)
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun SensitivityWarning(
|
||||
hasSensitiveContent: Boolean,
|
||||
accountViewModel: AccountViewModel,
|
||||
content: @Composable () -> Unit,
|
||||
) {
|
||||
if (hasSensitiveContent) {
|
||||
SensitivityWarning(accountViewModel, content)
|
||||
} else {
|
||||
|
||||
@@ -20,18 +20,26 @@
|
||||
*/
|
||||
package com.vitorpamplona.amethyst.ui.components
|
||||
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.ExperimentalFoundationApi
|
||||
import androidx.compose.foundation.combinedClickable
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.material3.DropdownMenu
|
||||
import androidx.compose.material3.DropdownMenuItem
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.MutableState
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.layout.ContentScale
|
||||
import androidx.compose.ui.platform.LocalClipboardManager
|
||||
import androidx.compose.ui.platform.LocalUriHandler
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.AnnotatedString
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import coil.compose.AsyncImage
|
||||
import com.vitorpamplona.amethyst.R
|
||||
@@ -41,16 +49,59 @@ import com.vitorpamplona.amethyst.ui.theme.MaxWidthWithHorzPadding
|
||||
import com.vitorpamplona.amethyst.ui.theme.StdVertSpacer
|
||||
import com.vitorpamplona.amethyst.ui.theme.innerPostModifier
|
||||
|
||||
@Composable
|
||||
private fun CopyToClipboard(
|
||||
popupExpanded: MutableState<Boolean>,
|
||||
content: String,
|
||||
onDismiss: () -> Unit,
|
||||
) {
|
||||
DropdownMenu(
|
||||
expanded = popupExpanded.value,
|
||||
onDismissRequest = onDismiss,
|
||||
) {
|
||||
val clipboardManager = LocalClipboardManager.current
|
||||
DropdownMenuItem(
|
||||
text = { Text(stringResource(R.string.copy_url_to_clipboard)) },
|
||||
onClick = {
|
||||
clipboardManager.setText(AnnotatedString(content))
|
||||
onDismiss()
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalFoundationApi::class)
|
||||
@Composable
|
||||
fun UrlPreviewCard(
|
||||
url: String,
|
||||
previewInfo: UrlInfoItem,
|
||||
) {
|
||||
val uri = LocalUriHandler.current
|
||||
val popupExpanded =
|
||||
remember {
|
||||
mutableStateOf(false)
|
||||
}
|
||||
|
||||
if (popupExpanded.value) {
|
||||
CopyToClipboard(
|
||||
popupExpanded = popupExpanded,
|
||||
content = url,
|
||||
) {
|
||||
popupExpanded.value = false
|
||||
}
|
||||
}
|
||||
|
||||
Column(
|
||||
modifier =
|
||||
MaterialTheme.colorScheme.innerPostModifier.clickable { runCatching { uri.openUri(url) } },
|
||||
MaterialTheme.colorScheme.innerPostModifier
|
||||
.combinedClickable(
|
||||
onClick = {
|
||||
runCatching { uri.openUri(url) }
|
||||
},
|
||||
onLongClick = {
|
||||
popupExpanded.value = true
|
||||
},
|
||||
),
|
||||
) {
|
||||
AsyncImage(
|
||||
model = previewInfo.imageUrlFullPath,
|
||||
|
||||
@@ -106,6 +106,7 @@ import com.vitorpamplona.amethyst.ui.theme.Size75dp
|
||||
import com.vitorpamplona.amethyst.ui.theme.VolumeBottomIconSize
|
||||
import com.vitorpamplona.amethyst.ui.theme.imageModifier
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
import kotlinx.coroutines.CancellationException
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.GlobalScope
|
||||
import kotlinx.coroutines.delay
|
||||
@@ -356,6 +357,7 @@ fun GetMediaItem(
|
||||
null
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
if (e is CancellationException) throw e
|
||||
null
|
||||
},
|
||||
)
|
||||
|
||||
@@ -131,6 +131,7 @@ import com.vitorpamplona.quartz.crypto.CryptoUtils
|
||||
import com.vitorpamplona.quartz.encoders.toHexKey
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
import kotlinx.collections.immutable.toImmutableList
|
||||
import kotlinx.coroutines.CancellationException
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.launch
|
||||
@@ -161,6 +162,7 @@ class ZoomableUrlImage(
|
||||
val blurhash: String? = null,
|
||||
dim: String? = null,
|
||||
uri: String? = null,
|
||||
val contentWarning: String? = null,
|
||||
) : ZoomableUrlContent(url, description, hash, dim, uri)
|
||||
|
||||
@Immutable
|
||||
@@ -173,6 +175,7 @@ class ZoomableUrlVideo(
|
||||
val artworkUri: String? = null,
|
||||
val authorName: String? = null,
|
||||
val blurhash: String? = null,
|
||||
val contentWarning: String? = null,
|
||||
) : ZoomableUrlContent(url, description, hash, dim, uri)
|
||||
|
||||
@Immutable
|
||||
@@ -265,20 +268,24 @@ fun ZoomableContentView(
|
||||
|
||||
when (content) {
|
||||
is ZoomableUrlImage ->
|
||||
UrlImageView(content, mainImageModifier, accountViewModel = accountViewModel)
|
||||
SensitivityWarning(content.contentWarning != null, accountViewModel) {
|
||||
UrlImageView(content, mainImageModifier, accountViewModel = accountViewModel)
|
||||
}
|
||||
is ZoomableUrlVideo ->
|
||||
VideoView(
|
||||
videoUri = content.url,
|
||||
title = content.description,
|
||||
artworkUri = content.artworkUri,
|
||||
authorName = content.authorName,
|
||||
dimensions = content.dim,
|
||||
blurhash = content.blurhash,
|
||||
roundedCorner = roundedCorner,
|
||||
nostrUriCallback = content.uri,
|
||||
onDialog = { dialogOpen = true },
|
||||
accountViewModel = accountViewModel,
|
||||
)
|
||||
SensitivityWarning(content.contentWarning != null, accountViewModel) {
|
||||
VideoView(
|
||||
videoUri = content.url,
|
||||
title = content.description,
|
||||
artworkUri = content.artworkUri,
|
||||
authorName = content.authorName,
|
||||
dimensions = content.dim,
|
||||
blurhash = content.blurhash,
|
||||
roundedCorner = roundedCorner,
|
||||
nostrUriCallback = content.uri,
|
||||
onDialog = { dialogOpen = true },
|
||||
accountViewModel = accountViewModel,
|
||||
)
|
||||
}
|
||||
is ZoomableLocalImage ->
|
||||
LocalImageView(content, mainImageModifier, accountViewModel = accountViewModel)
|
||||
is ZoomableLocalVideo ->
|
||||
@@ -387,13 +394,13 @@ private fun UrlImageView(
|
||||
val myModifier =
|
||||
remember {
|
||||
mainImageModifier.widthIn(max = maxWidth).heightIn(max = maxHeight)
|
||||
/* Is this necessary? It makes images bleed into other pages
|
||||
/* Is this necessary? It makes images bleed into other pages
|
||||
.run {
|
||||
aspectRatio(content.dim)?.let { ratio ->
|
||||
this.aspectRatio(ratio, false)
|
||||
} ?: this
|
||||
}
|
||||
*/
|
||||
*/
|
||||
}
|
||||
|
||||
val contentScale =
|
||||
@@ -644,6 +651,7 @@ fun aspectRatio(dim: String?): Float? {
|
||||
width / height
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
if (e is CancellationException) throw e
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
@@ -35,12 +35,12 @@ import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.vitorpamplona.amethyst.R
|
||||
import com.vitorpamplona.amethyst.ui.theme.ButtonBorder
|
||||
import com.vitorpamplona.amethyst.ui.theme.ThemeComparison
|
||||
import com.vitorpamplona.amethyst.ui.theme.ThemeComparisonColumn
|
||||
|
||||
@Composable
|
||||
@Preview
|
||||
fun AddButtonPreview() {
|
||||
ThemeComparison(
|
||||
ThemeComparisonColumn(
|
||||
onDark = {
|
||||
Row {
|
||||
Column {
|
||||
|
||||
@@ -46,15 +46,13 @@ fun DisplayFollowingHashtagsInPost(
|
||||
accountViewModel: AccountViewModel,
|
||||
nav: (String) -> Unit,
|
||||
) {
|
||||
val noteEvent = remember { baseNote.event } ?: return
|
||||
|
||||
val userFollowState by accountViewModel.userFollows.observeAsState()
|
||||
var firstTag by remember { mutableStateOf<String?>(null) }
|
||||
var firstTag by remember(baseNote) { mutableStateOf<String?>(null) }
|
||||
|
||||
LaunchedEffect(key1 = userFollowState) {
|
||||
launch(Dispatchers.Default) {
|
||||
val followingTags = userFollowState?.user?.cachedFollowingTagSet() ?: emptySet()
|
||||
val newFirstTag = noteEvent.firstIsTaggedHashes(followingTags)
|
||||
val newFirstTag = baseNote.event?.firstIsTaggedHashes(followingTags)
|
||||
|
||||
if (firstTag != newFirstTag) {
|
||||
launch(Dispatchers.Main) { firstTag = newFirstTag }
|
||||
|
||||
@@ -27,13 +27,13 @@ import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import com.vitorpamplona.amethyst.ui.theme.Font14SP
|
||||
import com.vitorpamplona.amethyst.ui.theme.ThemeComparison
|
||||
import com.vitorpamplona.amethyst.ui.theme.ThemeComparisonColumn
|
||||
import com.vitorpamplona.amethyst.ui.theme.lessImportantLink
|
||||
|
||||
@Composable
|
||||
@Preview
|
||||
fun DisplayPoWPreview() {
|
||||
ThemeComparison(
|
||||
ThemeComparisonColumn(
|
||||
onDark = { DisplayPoW(pow = 24) },
|
||||
onLight = { DisplayPoW(pow = 24) },
|
||||
)
|
||||
|
||||
@@ -51,13 +51,13 @@ import com.vitorpamplona.amethyst.ui.theme.QuoteBorder
|
||||
import com.vitorpamplona.amethyst.ui.theme.Size16Modifier
|
||||
import com.vitorpamplona.amethyst.ui.theme.Size20Modifier
|
||||
import com.vitorpamplona.amethyst.ui.theme.StdHorzSpacer
|
||||
import com.vitorpamplona.amethyst.ui.theme.ThemeComparison
|
||||
import com.vitorpamplona.amethyst.ui.theme.ThemeComparisonColumn
|
||||
import com.vitorpamplona.amethyst.ui.theme.placeholderText
|
||||
|
||||
@Composable
|
||||
@Preview
|
||||
fun LeftPictureLayoutPreview() {
|
||||
ThemeComparison(
|
||||
ThemeComparisonColumn(
|
||||
onDark = { LeftPictureLayoutPreviewCard() },
|
||||
onLight = { LeftPictureLayoutPreviewCard() },
|
||||
)
|
||||
|
||||
@@ -73,13 +73,14 @@ import com.vitorpamplona.amethyst.ui.note.ArrowBackIcon
|
||||
import com.vitorpamplona.amethyst.ui.note.toShortenHex
|
||||
import com.vitorpamplona.amethyst.ui.screen.AccountStateViewModel
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedOff.LoginPage
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedOff.LoginOrSignupScreen
|
||||
import com.vitorpamplona.amethyst.ui.theme.AccountPictureModifier
|
||||
import com.vitorpamplona.amethyst.ui.theme.Size10dp
|
||||
import com.vitorpamplona.amethyst.ui.theme.Size55dp
|
||||
import com.vitorpamplona.quartz.encoders.decodePublicKey
|
||||
import com.vitorpamplona.quartz.encoders.toHexKey
|
||||
import com.vitorpamplona.quartz.events.toImmutableListOfLists
|
||||
import kotlinx.coroutines.CancellationException
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
@@ -121,7 +122,7 @@ fun AccountSwitchBottomSheet(
|
||||
) {
|
||||
Surface(modifier = Modifier.fillMaxSize()) {
|
||||
Box {
|
||||
LoginPage(accountStateViewModel, isFirstLogin = false)
|
||||
LoginOrSignupScreen(accountStateViewModel, isFirstLogin = false)
|
||||
TopAppBar(
|
||||
title = {
|
||||
Text(text = stringResource(R.string.account_switch_add_account_dialog_title))
|
||||
@@ -166,6 +167,7 @@ fun DisplayAccount(
|
||||
decodePublicKey(acc.npub).toHexKey(),
|
||||
)
|
||||
} catch (e: Exception) {
|
||||
if (e is CancellationException) throw e
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
@@ -34,7 +34,6 @@ import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.fillMaxHeight
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.layout.width
|
||||
import androidx.compose.foundation.rememberScrollState
|
||||
import androidx.compose.foundation.verticalScroll
|
||||
|
||||
@@ -795,7 +795,11 @@ fun ShortCommunityHeader(
|
||||
}
|
||||
|
||||
Column(
|
||||
modifier = Modifier.padding(start = 10.dp).height(Size35dp).weight(1f),
|
||||
modifier =
|
||||
Modifier
|
||||
.padding(start = 10.dp)
|
||||
.height(Size35dp)
|
||||
.weight(1f),
|
||||
verticalArrangement = Arrangement.Center,
|
||||
) {
|
||||
Row(verticalAlignment = CenterVertically) {
|
||||
@@ -808,7 +812,10 @@ fun ShortCommunityHeader(
|
||||
}
|
||||
|
||||
Row(
|
||||
modifier = Modifier.height(Size35dp).padding(start = 5.dp),
|
||||
modifier =
|
||||
Modifier
|
||||
.height(Size35dp)
|
||||
.padding(start = 5.dp),
|
||||
verticalAlignment = CenterVertically,
|
||||
) {
|
||||
ShortCommunityActionOptions(baseNote, accountViewModel, nav)
|
||||
@@ -1429,7 +1436,8 @@ fun RenderAppDefinition(
|
||||
contentDescription = stringResource(id = R.string.profile_image),
|
||||
contentScale = ContentScale.FillWidth,
|
||||
modifier =
|
||||
Modifier.fillMaxWidth()
|
||||
Modifier
|
||||
.fillMaxWidth()
|
||||
.height(125.dp)
|
||||
.combinedClickable(
|
||||
onClick = {},
|
||||
@@ -1449,12 +1457,19 @@ fun RenderAppDefinition(
|
||||
painter = painterResource(R.drawable.profile_banner),
|
||||
contentDescription = stringResource(id = R.string.profile_banner),
|
||||
contentScale = ContentScale.FillWidth,
|
||||
modifier = Modifier.fillMaxWidth().height(125.dp),
|
||||
modifier =
|
||||
Modifier
|
||||
.fillMaxWidth()
|
||||
.height(125.dp),
|
||||
)
|
||||
}
|
||||
|
||||
Column(
|
||||
modifier = Modifier.fillMaxWidth().padding(horizontal = 10.dp).padding(top = 75.dp),
|
||||
modifier =
|
||||
Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = 10.dp)
|
||||
.padding(top = 75.dp),
|
||||
) {
|
||||
Row(
|
||||
horizontalArrangement = Arrangement.SpaceBetween,
|
||||
@@ -1469,11 +1484,12 @@ fun RenderAppDefinition(
|
||||
contentDescription = null,
|
||||
contentScale = ContentScale.FillWidth,
|
||||
modifier =
|
||||
Modifier.border(
|
||||
3.dp,
|
||||
MaterialTheme.colorScheme.background,
|
||||
CircleShape,
|
||||
)
|
||||
Modifier
|
||||
.border(
|
||||
3.dp,
|
||||
MaterialTheme.colorScheme.background,
|
||||
CircleShape,
|
||||
)
|
||||
.clip(shape = CircleShape)
|
||||
.fillMaxSize()
|
||||
.background(MaterialTheme.colorScheme.background)
|
||||
@@ -1496,7 +1512,10 @@ fun RenderAppDefinition(
|
||||
Spacer(Modifier.weight(1f))
|
||||
|
||||
Row(
|
||||
modifier = Modifier.height(Size35dp).padding(bottom = 3.dp),
|
||||
modifier =
|
||||
Modifier
|
||||
.height(Size35dp)
|
||||
.padding(bottom = 3.dp),
|
||||
) {}
|
||||
}
|
||||
|
||||
@@ -1681,7 +1700,10 @@ fun DisplayRelaySet(
|
||||
fontWeight = FontWeight.Bold,
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
modifier = Modifier.fillMaxWidth().padding(5.dp),
|
||||
modifier =
|
||||
Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(5.dp),
|
||||
textAlign = TextAlign.Center,
|
||||
)
|
||||
|
||||
@@ -1690,7 +1712,10 @@ fun DisplayRelaySet(
|
||||
text = it,
|
||||
maxLines = 3,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
modifier = Modifier.fillMaxWidth().padding(5.dp),
|
||||
modifier =
|
||||
Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(5.dp),
|
||||
textAlign = TextAlign.Center,
|
||||
color = Color.Gray,
|
||||
)
|
||||
@@ -1705,7 +1730,10 @@ fun DisplayRelaySet(
|
||||
fontWeight = FontWeight.Bold,
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
modifier = Modifier.padding(start = 10.dp, bottom = 5.dp).weight(1f),
|
||||
modifier =
|
||||
Modifier
|
||||
.padding(start = 10.dp, bottom = 5.dp)
|
||||
.weight(1f),
|
||||
)
|
||||
|
||||
Column(modifier = Modifier.padding(start = 10.dp)) {
|
||||
@@ -1720,7 +1748,8 @@ fun DisplayRelaySet(
|
||||
verticalAlignment = CenterVertically,
|
||||
horizontalArrangement = Arrangement.Center,
|
||||
modifier =
|
||||
Modifier.align(Alignment.BottomCenter)
|
||||
Modifier
|
||||
.align(Alignment.BottomCenter)
|
||||
.fillMaxWidth()
|
||||
.background(getGradient(backgroundColor)),
|
||||
) {
|
||||
@@ -1785,7 +1814,10 @@ fun DisplayPeopleList(
|
||||
fontWeight = FontWeight.Bold,
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
modifier = Modifier.fillMaxWidth().padding(5.dp),
|
||||
modifier =
|
||||
Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(5.dp),
|
||||
textAlign = TextAlign.Center,
|
||||
)
|
||||
|
||||
@@ -1810,7 +1842,8 @@ fun DisplayPeopleList(
|
||||
verticalAlignment = CenterVertically,
|
||||
horizontalArrangement = Arrangement.Center,
|
||||
modifier =
|
||||
Modifier.align(Alignment.BottomCenter)
|
||||
Modifier
|
||||
.align(Alignment.BottomCenter)
|
||||
.fillMaxWidth()
|
||||
.background(getGradient(backgroundColor)),
|
||||
) {
|
||||
@@ -1840,7 +1873,10 @@ private fun RenderBadgeAward(
|
||||
FlowRow(modifier = Modifier.padding(top = 5.dp)) {
|
||||
awardees.take(100).forEach { user ->
|
||||
Row(
|
||||
modifier = Modifier.size(size = Size35dp).clickable { nav("User/${user.pubkeyHex}") },
|
||||
modifier =
|
||||
Modifier
|
||||
.size(size = Size35dp)
|
||||
.clickable { nav("User/${user.pubkeyHex}") },
|
||||
verticalAlignment = CenterVertically,
|
||||
) {
|
||||
ClickableUserPicture(
|
||||
@@ -1952,7 +1988,10 @@ fun RenderPostApproval(
|
||||
fontWeight = FontWeight.Bold,
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
modifier = Modifier.fillMaxWidth().padding(5.dp),
|
||||
modifier =
|
||||
Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(5.dp),
|
||||
textAlign = TextAlign.Center,
|
||||
)
|
||||
|
||||
@@ -2076,7 +2115,10 @@ public fun RenderEmojiPack(
|
||||
fontWeight = FontWeight.Bold,
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
modifier = Modifier.weight(1F).padding(5.dp),
|
||||
modifier =
|
||||
Modifier
|
||||
.weight(1F)
|
||||
.padding(5.dp),
|
||||
textAlign = TextAlign.Center,
|
||||
)
|
||||
|
||||
@@ -2116,7 +2158,8 @@ public fun RenderEmojiPack(
|
||||
verticalAlignment = CenterVertically,
|
||||
horizontalArrangement = Arrangement.Center,
|
||||
modifier =
|
||||
Modifier.align(Alignment.BottomCenter)
|
||||
Modifier
|
||||
.align(Alignment.BottomCenter)
|
||||
.fillMaxWidth()
|
||||
.background(getGradient(backgroundColor)),
|
||||
) {
|
||||
@@ -2189,7 +2232,10 @@ fun RenderPinListEvent(
|
||||
fontWeight = FontWeight.Bold,
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
modifier = Modifier.fillMaxWidth().padding(5.dp),
|
||||
modifier =
|
||||
Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(5.dp),
|
||||
textAlign = TextAlign.Center,
|
||||
)
|
||||
|
||||
@@ -2221,7 +2267,8 @@ fun RenderPinListEvent(
|
||||
verticalAlignment = CenterVertically,
|
||||
horizontalArrangement = Arrangement.Center,
|
||||
modifier =
|
||||
Modifier.align(Alignment.BottomCenter)
|
||||
Modifier
|
||||
.align(Alignment.BottomCenter)
|
||||
.fillMaxWidth()
|
||||
.background(getGradient(backgroundColor)),
|
||||
) {
|
||||
@@ -2320,7 +2367,8 @@ private fun RenderReport(
|
||||
baseNote = it,
|
||||
isQuotedNote = true,
|
||||
modifier =
|
||||
Modifier.padding(top = 5.dp)
|
||||
Modifier
|
||||
.padding(top = 5.dp)
|
||||
.fillMaxWidth()
|
||||
.clip(shape = QuoteBorder)
|
||||
.border(
|
||||
@@ -2925,7 +2973,8 @@ fun BadgeDisplay(baseNote: Note) {
|
||||
|
||||
Row(
|
||||
modifier =
|
||||
Modifier.padding(10.dp)
|
||||
Modifier
|
||||
.padding(10.dp)
|
||||
.clip(shape = CutCornerShape(20, 20, 20, 20))
|
||||
.border(
|
||||
5.dp,
|
||||
@@ -2975,7 +3024,10 @@ private fun RenderBadge(
|
||||
text = it,
|
||||
style = MaterialTheme.typography.bodyLarge,
|
||||
textAlign = TextAlign.Center,
|
||||
modifier = Modifier.fillMaxWidth().padding(start = 10.dp, end = 10.dp),
|
||||
modifier =
|
||||
Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(start = 10.dp, end = 10.dp),
|
||||
color = backgroundFromImage,
|
||||
)
|
||||
}
|
||||
@@ -2985,7 +3037,10 @@ private fun RenderBadge(
|
||||
text = it,
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
textAlign = TextAlign.Center,
|
||||
modifier = Modifier.fillMaxWidth().padding(start = 10.dp, end = 10.dp, bottom = 10.dp),
|
||||
modifier =
|
||||
Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(start = 10.dp, end = 10.dp, bottom = 10.dp),
|
||||
color = Color.Gray,
|
||||
maxLines = 3,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
@@ -3103,7 +3158,10 @@ fun VideoDisplay(
|
||||
|
||||
SensitivityWarning(note = note, accountViewModel = accountViewModel) {
|
||||
Column(
|
||||
modifier = Modifier.fillMaxWidth().padding(top = 5.dp),
|
||||
modifier =
|
||||
Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(top = 5.dp),
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
) {
|
||||
if (isYouTube) {
|
||||
@@ -3139,7 +3197,10 @@ fun VideoDisplay(
|
||||
fontWeight = FontWeight.Bold,
|
||||
maxLines = 3,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
modifier = Modifier.fillMaxWidth().padding(top = 5.dp),
|
||||
modifier =
|
||||
Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(top = 5.dp),
|
||||
)
|
||||
}
|
||||
|
||||
@@ -3291,9 +3352,11 @@ fun AudioTrackHeader(
|
||||
Row(
|
||||
verticalAlignment = CenterVertically,
|
||||
modifier =
|
||||
Modifier.padding(top = 5.dp, start = 10.dp, end = 10.dp).clickable {
|
||||
nav("User/${it.second.pubkeyHex}")
|
||||
},
|
||||
Modifier
|
||||
.padding(top = 5.dp, start = 10.dp, end = 10.dp)
|
||||
.clickable {
|
||||
nav("User/${it.second.pubkeyHex}")
|
||||
},
|
||||
) {
|
||||
ClickableUserPicture(it.second, 25.dp, accountViewModel)
|
||||
Spacer(Modifier.width(5.dp))
|
||||
@@ -3373,7 +3436,10 @@ fun AudioHeader(
|
||||
content?.let {
|
||||
Row(
|
||||
verticalAlignment = CenterVertically,
|
||||
modifier = Modifier.fillMaxWidth().padding(top = 5.dp),
|
||||
modifier =
|
||||
Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(top = 5.dp),
|
||||
) {
|
||||
TranslatableRichTextViewer(
|
||||
content = it,
|
||||
@@ -3432,7 +3498,10 @@ fun RenderLiveActivityEventInner(
|
||||
|
||||
Row(
|
||||
verticalAlignment = CenterVertically,
|
||||
modifier = Modifier.padding(vertical = 5.dp).fillMaxWidth(),
|
||||
modifier =
|
||||
Modifier
|
||||
.padding(vertical = 5.dp)
|
||||
.fillMaxWidth(),
|
||||
) {
|
||||
subject?.let {
|
||||
Text(
|
||||
@@ -3492,7 +3561,10 @@ fun RenderLiveActivityEventInner(
|
||||
} else {
|
||||
Row(
|
||||
verticalAlignment = CenterVertically,
|
||||
modifier = Modifier.padding(10.dp).height(100.dp),
|
||||
modifier =
|
||||
Modifier
|
||||
.padding(10.dp)
|
||||
.height(100.dp),
|
||||
) {
|
||||
Text(
|
||||
text = stringResource(id = R.string.live_stream_is_offline),
|
||||
@@ -3505,7 +3577,10 @@ fun RenderLiveActivityEventInner(
|
||||
} else if (status == STATUS_ENDED) {
|
||||
Row(
|
||||
verticalAlignment = CenterVertically,
|
||||
modifier = Modifier.padding(10.dp).height(100.dp),
|
||||
modifier =
|
||||
Modifier
|
||||
.padding(10.dp)
|
||||
.height(100.dp),
|
||||
) {
|
||||
Text(
|
||||
text = stringResource(id = R.string.live_stream_has_ended),
|
||||
@@ -3519,7 +3594,10 @@ fun RenderLiveActivityEventInner(
|
||||
participantUsers.forEach {
|
||||
Row(
|
||||
verticalAlignment = CenterVertically,
|
||||
modifier = Modifier.padding(vertical = 5.dp).clickable { nav("User/${it.second.pubkeyHex}") },
|
||||
modifier =
|
||||
Modifier
|
||||
.padding(vertical = 5.dp)
|
||||
.clickable { nav("User/${it.second.pubkeyHex}") },
|
||||
) {
|
||||
ClickableUserPicture(it.second, 25.dp, accountViewModel)
|
||||
Spacer(StdHorzSpacer)
|
||||
@@ -3551,7 +3629,8 @@ private fun LongFormHeader(
|
||||
|
||||
Row(
|
||||
modifier =
|
||||
Modifier.padding(top = Size5dp)
|
||||
Modifier
|
||||
.padding(top = Size5dp)
|
||||
.clip(shape = QuoteBorder)
|
||||
.border(
|
||||
1.dp,
|
||||
@@ -3582,7 +3661,10 @@ private fun LongFormHeader(
|
||||
Text(
|
||||
text = it,
|
||||
style = MaterialTheme.typography.bodyLarge,
|
||||
modifier = Modifier.fillMaxWidth().padding(start = 10.dp, end = 10.dp, top = 10.dp),
|
||||
modifier =
|
||||
Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(start = 10.dp, end = 10.dp, top = 10.dp),
|
||||
)
|
||||
}
|
||||
|
||||
@@ -3590,7 +3672,10 @@ private fun LongFormHeader(
|
||||
Text(
|
||||
text = it,
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
modifier = Modifier.fillMaxWidth().padding(start = 10.dp, end = 10.dp, bottom = 10.dp),
|
||||
modifier =
|
||||
Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(start = 10.dp, end = 10.dp, bottom = 10.dp),
|
||||
color = Color.Gray,
|
||||
maxLines = 3,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
@@ -3616,7 +3701,8 @@ private fun RenderClassifieds(
|
||||
|
||||
Row(
|
||||
modifier =
|
||||
Modifier.clip(shape = QuoteBorder)
|
||||
Modifier
|
||||
.clip(shape = QuoteBorder)
|
||||
.border(
|
||||
1.dp,
|
||||
MaterialTheme.colorScheme.subtleBorder,
|
||||
@@ -3674,7 +3760,12 @@ private fun RenderClassifieds(
|
||||
maxLines = 1,
|
||||
color = MaterialTheme.colorScheme.primary,
|
||||
fontWeight = FontWeight.Bold,
|
||||
modifier = remember { Modifier.clip(SmallBorder).padding(start = 5.dp) },
|
||||
modifier =
|
||||
remember {
|
||||
Modifier
|
||||
.clip(SmallBorder)
|
||||
.padding(start = 5.dp)
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -3767,11 +3858,22 @@ fun CreateImageHeader(
|
||||
painter = painterResource(R.drawable.profile_banner),
|
||||
contentDescription = stringResource(R.string.profile_banner),
|
||||
contentScale = ContentScale.FillWidth,
|
||||
modifier = remember { Modifier.fillMaxWidth().height(150.dp) },
|
||||
modifier =
|
||||
remember {
|
||||
Modifier
|
||||
.fillMaxWidth()
|
||||
.height(150.dp)
|
||||
},
|
||||
)
|
||||
|
||||
Box(
|
||||
remember { Modifier.width(75.dp).height(75.dp).padding(10.dp).align(Alignment.BottomStart) },
|
||||
remember {
|
||||
Modifier
|
||||
.width(75.dp)
|
||||
.height(75.dp)
|
||||
.padding(10.dp)
|
||||
.align(Alignment.BottomStart)
|
||||
},
|
||||
) {
|
||||
NoteAuthorPicture(baseNote = note, accountViewModel = accountViewModel, size = Size55dp)
|
||||
}
|
||||
|
||||
@@ -103,6 +103,7 @@ import com.vitorpamplona.quartz.encoders.decodePublicKey
|
||||
import com.vitorpamplona.quartz.encoders.toHexKey
|
||||
import com.vitorpamplona.quartz.events.LnZapEvent
|
||||
import kotlinx.collections.immutable.toImmutableList
|
||||
import kotlinx.coroutines.CancellationException
|
||||
|
||||
class UpdateZapAmountViewModel(val account: Account) : ViewModel() {
|
||||
var nextAmount by mutableStateOf(TextFieldValue(""))
|
||||
@@ -149,6 +150,7 @@ class UpdateZapAmountViewModel(val account: Account) : ViewModel() {
|
||||
try {
|
||||
decodePublicKey(walletConnectPubkey.text.trim()).toHexKey()
|
||||
} catch (e: Exception) {
|
||||
if (e is CancellationException) throw e
|
||||
null
|
||||
}
|
||||
|
||||
@@ -168,6 +170,7 @@ class UpdateZapAmountViewModel(val account: Account) : ViewModel() {
|
||||
try {
|
||||
unverifiedPrivKey?.let { decodePublicKey(it).toHexKey() }
|
||||
} catch (e: Exception) {
|
||||
if (e is CancellationException) throw e
|
||||
null
|
||||
}
|
||||
|
||||
|
||||
@@ -88,6 +88,7 @@ import com.vitorpamplona.amethyst.ui.theme.placeholderText
|
||||
import com.vitorpamplona.quartz.events.LnZapEvent
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
import kotlinx.collections.immutable.toImmutableList
|
||||
import kotlinx.coroutines.CancellationException
|
||||
|
||||
class ZapOptionstViewModel : ViewModel() {
|
||||
private var account: Account? = null
|
||||
@@ -107,6 +108,7 @@ class ZapOptionstViewModel : ViewModel() {
|
||||
return try {
|
||||
customAmount.text.trim().toLongOrNull()
|
||||
} catch (e: Exception) {
|
||||
if (e is CancellationException) throw e
|
||||
null
|
||||
}
|
||||
}
|
||||
@@ -446,6 +448,7 @@ fun payViaIntent(
|
||||
|
||||
ContextCompat.startActivity(context, intent, null)
|
||||
} catch (e: Exception) {
|
||||
if (e is CancellationException) throw e
|
||||
if (e.message != null) {
|
||||
onError(context.getString(R.string.no_wallet_found_with_error, e.message!!))
|
||||
} else {
|
||||
|
||||
@@ -30,6 +30,7 @@ import com.journeyapps.barcodescanner.ScanContract
|
||||
import com.journeyapps.barcodescanner.ScanOptions
|
||||
import com.vitorpamplona.amethyst.R
|
||||
import com.vitorpamplona.quartz.encoders.Nip19
|
||||
import kotlinx.coroutines.CancellationException
|
||||
|
||||
@Composable
|
||||
fun NIP19QrCodeScanner(onScan: (String?) -> Unit) {
|
||||
@@ -51,6 +52,7 @@ fun NIP19QrCodeScanner(onScan: (String?) -> Unit) {
|
||||
onScan(null)
|
||||
}
|
||||
} catch (e: Throwable) {
|
||||
if (e is CancellationException) throw e
|
||||
Log.e("NIP19 Scanner", "Error parsing $it", e)
|
||||
// QR can be anything, do not throw errors.
|
||||
onScan(null)
|
||||
|
||||
@@ -50,9 +50,9 @@ import com.vitorpamplona.amethyst.ui.MainActivity
|
||||
import com.vitorpamplona.amethyst.ui.components.getActivity
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.MainScreen
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedOff.LoginPage
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedOff.LoginOrSignupScreen
|
||||
import com.vitorpamplona.quartz.signers.NostrSignerExternal
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.CancellationException
|
||||
|
||||
@Composable
|
||||
fun AccountScreen(
|
||||
@@ -71,7 +71,7 @@ fun AccountScreen(
|
||||
LoadingAccounts()
|
||||
}
|
||||
is AccountState.LoggedOff -> {
|
||||
LoginPage(accountStateViewModel, isFirstLogin = true)
|
||||
LoginOrSignupScreen(accountStateViewModel, isFirstLogin = true)
|
||||
}
|
||||
is AccountState.LoggedIn -> {
|
||||
CompositionLocalProvider(
|
||||
@@ -144,6 +144,7 @@ fun LoggedInPage(
|
||||
activity.prepareToLaunchSigner()
|
||||
launcher.launch(it)
|
||||
} catch (e: Exception) {
|
||||
if (e is CancellationException) throw e
|
||||
Log.e("Signer", "Error opening Signer app", e)
|
||||
accountViewModel.toast(
|
||||
R.string.error_opening_external_signer,
|
||||
|
||||
@@ -28,7 +28,8 @@ import com.vitorpamplona.amethyst.AccountInfo
|
||||
import com.vitorpamplona.amethyst.LocalPreferences
|
||||
import com.vitorpamplona.amethyst.ServiceManager
|
||||
import com.vitorpamplona.amethyst.model.Account
|
||||
import com.vitorpamplona.amethyst.service.HttpClient
|
||||
import com.vitorpamplona.amethyst.service.HttpClientManager
|
||||
import com.vitorpamplona.amethyst.service.relays.Client
|
||||
import com.vitorpamplona.quartz.crypto.KeyPair
|
||||
import com.vitorpamplona.quartz.encoders.Hex
|
||||
import com.vitorpamplona.quartz.encoders.Nip19
|
||||
@@ -39,9 +40,11 @@ import com.vitorpamplona.quartz.encoders.toNpub
|
||||
import com.vitorpamplona.quartz.signers.ExternalSignerLauncher
|
||||
import com.vitorpamplona.quartz.signers.NostrSignerExternal
|
||||
import com.vitorpamplona.quartz.signers.NostrSignerInternal
|
||||
import kotlinx.coroutines.CancellationException
|
||||
import kotlinx.coroutines.DelicateCoroutinesApi
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.GlobalScope
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.asStateFlow
|
||||
import kotlinx.coroutines.flow.update
|
||||
@@ -65,8 +68,7 @@ class AccountStateViewModel() : ViewModel() {
|
||||
|
||||
private suspend fun tryLoginExistingAccount() =
|
||||
withContext(Dispatchers.IO) {
|
||||
LocalPreferences.loadCurrentAccountFromEncryptedStorage()?.let { startUI(it) }
|
||||
?: run { requestLoginUI() }
|
||||
LocalPreferences.loadCurrentAccountFromEncryptedStorage()?.let { startUI(it) } ?: run { requestLoginUI() }
|
||||
}
|
||||
|
||||
private suspend fun requestLoginUI() {
|
||||
@@ -84,7 +86,7 @@ class AccountStateViewModel() : ViewModel() {
|
||||
) = withContext(Dispatchers.IO) {
|
||||
val parsed = Nip19.uriToRoute(key)
|
||||
val pubKeyParsed = parsed?.hex?.hexToByteArray()
|
||||
val proxy = HttpClient.initProxy(useProxy, "127.0.0.1", proxyPort)
|
||||
val proxy = HttpClientManager.initProxy(useProxy, "127.0.0.1", proxyPort)
|
||||
|
||||
if (loginWithExternalSigner && pubKeyParsed == null) {
|
||||
throw Exception("Invalid key while trying to login with external signer")
|
||||
@@ -144,25 +146,33 @@ class AccountStateViewModel() : ViewModel() {
|
||||
startUI(account)
|
||||
}
|
||||
|
||||
suspend fun startUI(account: Account) =
|
||||
withContext(Dispatchers.Main) {
|
||||
if (account.isWriteable()) {
|
||||
_accountContent.update { AccountState.LoggedIn(account) }
|
||||
} else {
|
||||
_accountContent.update { AccountState.LoggedInViewOnly(account) }
|
||||
}
|
||||
|
||||
viewModelScope.launch(Dispatchers.IO) {
|
||||
withContext(Dispatchers.Main) {
|
||||
// Prepares livedata objects on the main user.
|
||||
account.userProfile().live()
|
||||
}
|
||||
serviceManager?.restartIfDifferentAccount(account)
|
||||
}
|
||||
|
||||
account.saveable.observeForever(saveListener)
|
||||
suspend fun startUI(
|
||||
account: Account,
|
||||
onServicesReady: (() -> Unit)? = null,
|
||||
) = withContext(Dispatchers.Main) {
|
||||
if (account.isWriteable()) {
|
||||
_accountContent.update { AccountState.LoggedIn(account) }
|
||||
} else {
|
||||
_accountContent.update { AccountState.LoggedInViewOnly(account) }
|
||||
}
|
||||
|
||||
viewModelScope.launch(Dispatchers.IO) {
|
||||
withContext(Dispatchers.Main) {
|
||||
// Prepares livedata objects on the main user.
|
||||
account.userProfile().live()
|
||||
}
|
||||
serviceManager?.restartIfDifferentAccount(account)
|
||||
|
||||
if (onServicesReady != null) {
|
||||
// waits for the connection to go through
|
||||
delay(1000)
|
||||
onServicesReady()
|
||||
}
|
||||
}
|
||||
|
||||
account.saveable.observeForever(saveListener)
|
||||
}
|
||||
|
||||
@OptIn(DelicateCoroutinesApi::class)
|
||||
private val saveListener: (com.vitorpamplona.amethyst.model.AccountState) -> Unit = {
|
||||
GlobalScope.launch(Dispatchers.IO) { LocalPreferences.saveToEncryptedStorage(it.account) }
|
||||
@@ -195,6 +205,7 @@ class AccountStateViewModel() : ViewModel() {
|
||||
try {
|
||||
loginAndStartUI(key, useProxy, proxyPort, loginWithExternalSigner, packageName)
|
||||
} catch (e: Exception) {
|
||||
if (e is CancellationException) throw e
|
||||
Log.e("Login", "Could not sign in", e)
|
||||
onError()
|
||||
}
|
||||
@@ -204,9 +215,10 @@ class AccountStateViewModel() : ViewModel() {
|
||||
fun newKey(
|
||||
useProxy: Boolean,
|
||||
proxyPort: Int,
|
||||
name: String? = null,
|
||||
) {
|
||||
viewModelScope.launch(Dispatchers.IO) {
|
||||
val proxy = HttpClient.initProxy(useProxy, "127.0.0.1", proxyPort)
|
||||
val proxy = HttpClientManager.initProxy(useProxy, "127.0.0.1", proxyPort)
|
||||
val keyPair = KeyPair()
|
||||
val account =
|
||||
Account(
|
||||
@@ -220,7 +232,10 @@ class AccountStateViewModel() : ViewModel() {
|
||||
|
||||
// saves to local preferences
|
||||
LocalPreferences.updatePrefsForLogin(account)
|
||||
startUI(account)
|
||||
startUI(account) {
|
||||
account.userProfile().latestContactList?.let { Client.send(it) }
|
||||
account.sendNewUserMetadata(name = name)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -49,7 +49,7 @@ import com.vitorpamplona.amethyst.model.User
|
||||
import com.vitorpamplona.amethyst.model.UserState
|
||||
import com.vitorpamplona.amethyst.service.CashuProcessor
|
||||
import com.vitorpamplona.amethyst.service.CashuToken
|
||||
import com.vitorpamplona.amethyst.service.HttpClient
|
||||
import com.vitorpamplona.amethyst.service.HttpClientManager
|
||||
import com.vitorpamplona.amethyst.service.Nip05NostrAddressVerifier
|
||||
import com.vitorpamplona.amethyst.service.Nip11CachedRetriever
|
||||
import com.vitorpamplona.amethyst.service.Nip11Retriever
|
||||
@@ -87,6 +87,7 @@ import kotlinx.collections.immutable.ImmutableSet
|
||||
import kotlinx.collections.immutable.persistentSetOf
|
||||
import kotlinx.collections.immutable.toImmutableList
|
||||
import kotlinx.collections.immutable.toImmutableSet
|
||||
import kotlinx.coroutines.CancellationException
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.channels.BufferOverflow
|
||||
@@ -1042,7 +1043,7 @@ class AccountViewModel(val account: Account, val settings: SettingsState) : View
|
||||
) {
|
||||
viewModelScope.launch(Dispatchers.IO) {
|
||||
account.proxyPort = portNumber.value.toInt()
|
||||
account.proxy = HttpClient.initProxy(checked, "127.0.0.1", account.proxyPort)
|
||||
account.proxy = HttpClientManager.initProxy(checked, "127.0.0.1", account.proxyPort)
|
||||
account.saveable.invalidateData()
|
||||
serviceManager?.forceRestart()
|
||||
}
|
||||
@@ -1102,6 +1103,7 @@ class AccountViewModel(val account: Account, val settings: SettingsState) : View
|
||||
val myCover = context.imageLoader.execute(request).drawable
|
||||
onReady(myCover)
|
||||
} catch (e: Exception) {
|
||||
if (e is CancellationException) throw e
|
||||
Log.e("VideoView", "Fail to load cover $thumbUri", e)
|
||||
onError(e.message)
|
||||
}
|
||||
|
||||
@@ -44,7 +44,6 @@ import com.vitorpamplona.amethyst.ui.screen.RefresheableFeedView
|
||||
import com.vitorpamplona.amethyst.ui.theme.TabRowHeight
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
@OptIn(ExperimentalFoundationApi::class)
|
||||
@Composable
|
||||
fun BookmarkListScreen(
|
||||
accountViewModel: AccountViewModel,
|
||||
@@ -52,13 +51,13 @@ fun BookmarkListScreen(
|
||||
) {
|
||||
val publicFeedViewModel: NostrBookmarkPublicFeedViewModel =
|
||||
viewModel(
|
||||
key = "NotificationViewModel",
|
||||
key = "NostrBookmarkPublicFeedViewModel",
|
||||
factory = NostrBookmarkPublicFeedViewModel.Factory(accountViewModel.account),
|
||||
)
|
||||
|
||||
val privateFeedViewModel: NostrBookmarkPrivateFeedViewModel =
|
||||
viewModel(
|
||||
key = "NotificationViewModel",
|
||||
key = "NostrBookmarkPrivateFeedViewModel",
|
||||
factory = NostrBookmarkPrivateFeedViewModel.Factory(accountViewModel.account),
|
||||
)
|
||||
|
||||
@@ -69,6 +68,17 @@ fun BookmarkListScreen(
|
||||
privateFeedViewModel.invalidateData()
|
||||
}
|
||||
|
||||
RenderBookmarkScreen(privateFeedViewModel, accountViewModel, nav, publicFeedViewModel)
|
||||
}
|
||||
|
||||
@Composable
|
||||
@OptIn(ExperimentalFoundationApi::class)
|
||||
private fun RenderBookmarkScreen(
|
||||
privateFeedViewModel: NostrBookmarkPrivateFeedViewModel,
|
||||
accountViewModel: AccountViewModel,
|
||||
nav: (String) -> Unit,
|
||||
publicFeedViewModel: NostrBookmarkPublicFeedViewModel,
|
||||
) {
|
||||
Column(Modifier.fillMaxHeight()) {
|
||||
val pagerState = rememberPagerState { 2 }
|
||||
val coroutineScope = rememberCoroutineScope()
|
||||
@@ -99,6 +109,7 @@ fun BookmarkListScreen(
|
||||
accountViewModel = accountViewModel,
|
||||
nav = nav,
|
||||
)
|
||||
|
||||
1 ->
|
||||
RefresheableFeedView(
|
||||
publicFeedViewModel,
|
||||
|
||||
@@ -225,6 +225,7 @@ fun ChatroomListScreenOnlyList(
|
||||
val observer =
|
||||
LifecycleEventObserver { _, event ->
|
||||
if (event == Lifecycle.Event.ON_RESUME) {
|
||||
NostrChatroomListDataSource.account = accountViewModel.account
|
||||
NostrChatroomListDataSource.start()
|
||||
}
|
||||
}
|
||||
@@ -303,6 +304,7 @@ fun WatchAccountForListScreen(
|
||||
) {
|
||||
LaunchedEffect(accountViewModel) {
|
||||
launch(Dispatchers.IO) {
|
||||
NostrChatroomListDataSource.account = accountViewModel.account
|
||||
NostrChatroomListDataSource.start()
|
||||
knownFeedViewModel.invalidateData(true)
|
||||
newFeedViewModel.invalidateData(true)
|
||||
|
||||
@@ -54,6 +54,7 @@ import com.vitorpamplona.amethyst.ui.actions.CloseButton
|
||||
import com.vitorpamplona.amethyst.ui.theme.ButtonBorder
|
||||
import com.vitorpamplona.amethyst.ui.theme.RichTextDefaults
|
||||
import com.vitorpamplona.amethyst.ui.theme.placeholderText
|
||||
import kotlinx.coroutines.CancellationException
|
||||
|
||||
@Composable
|
||||
fun ConnectOrbotDialog(
|
||||
@@ -83,7 +84,8 @@ fun ConnectOrbotDialog(
|
||||
onPost = {
|
||||
try {
|
||||
Integer.parseInt(portNumber.value)
|
||||
} catch (_: Exception) {
|
||||
} catch (e: Exception) {
|
||||
if (e is CancellationException) throw e
|
||||
onError(toastMessage)
|
||||
return@UseOrbotButton
|
||||
}
|
||||
|
||||
@@ -45,7 +45,6 @@ import com.vitorpamplona.amethyst.model.Note
|
||||
import com.vitorpamplona.amethyst.ui.navigation.Route
|
||||
import com.vitorpamplona.quartz.events.ChannelCreateEvent
|
||||
import com.vitorpamplona.quartz.events.ChatroomKeyable
|
||||
import com.vitorpamplona.quartz.events.Event
|
||||
import com.vitorpamplona.quartz.events.EventInterface
|
||||
import com.vitorpamplona.quartz.events.GiftWrapEvent
|
||||
import com.vitorpamplona.quartz.events.SealedGossipEvent
|
||||
|
||||
@@ -35,9 +35,7 @@ import androidx.compose.foundation.verticalScroll
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
|
||||
@@ -0,0 +1,51 @@
|
||||
/**
|
||||
* Copyright (c) 2023 Vitor Pamplona
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
* this software and associated documentation files (the "Software"), to deal in
|
||||
* the Software without restriction, including without limitation the rights to use,
|
||||
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
|
||||
* Software, and to permit persons to whom the Software is furnished to do so,
|
||||
* subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
|
||||
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
package com.vitorpamplona.amethyst.ui.screen.loggedOff
|
||||
|
||||
import androidx.compose.animation.Crossfade
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.setValue
|
||||
import com.vitorpamplona.amethyst.ui.screen.AccountStateViewModel
|
||||
|
||||
@Composable
|
||||
fun LoginOrSignupScreen(
|
||||
accountViewModel: AccountStateViewModel,
|
||||
isFirstLogin: Boolean,
|
||||
) {
|
||||
var wantsNewUser by remember {
|
||||
mutableStateOf(false)
|
||||
}
|
||||
|
||||
Crossfade(wantsNewUser, label = "LoginOrSignupScreen") {
|
||||
if (it) {
|
||||
SignUpPage(accountStateViewModel = accountViewModel) {
|
||||
wantsNewUser = false
|
||||
}
|
||||
} else {
|
||||
LoginPage(accountStateViewModel = accountViewModel, isFirstLogin = isFirstLogin) {
|
||||
wantsNewUser = true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -32,7 +32,6 @@ import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
@@ -50,6 +49,7 @@ import androidx.compose.material3.Checkbox
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.IconButton
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.OutlinedButton
|
||||
import androidx.compose.material3.OutlinedTextField
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
@@ -75,19 +75,17 @@ import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.platform.LocalUriHandler
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.AnnotatedString
|
||||
import androidx.compose.ui.text.SpanStyle
|
||||
import androidx.compose.ui.text.TextStyle
|
||||
import androidx.compose.ui.text.buildAnnotatedString
|
||||
import androidx.compose.ui.text.input.ImeAction
|
||||
import androidx.compose.ui.text.input.KeyboardType
|
||||
import androidx.compose.ui.text.input.PasswordVisualTransformation
|
||||
import androidx.compose.ui.text.input.TextFieldValue
|
||||
import androidx.compose.ui.text.input.VisualTransformation
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.text.style.TextDecoration
|
||||
import androidx.compose.ui.text.withStyle
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.lifecycle.viewmodel.compose.viewModel
|
||||
import com.vitorpamplona.amethyst.Amethyst
|
||||
import com.vitorpamplona.amethyst.R
|
||||
import com.vitorpamplona.amethyst.service.PackageUtils
|
||||
@@ -96,20 +94,39 @@ import com.vitorpamplona.amethyst.ui.components.getActivity
|
||||
import com.vitorpamplona.amethyst.ui.qrcode.SimpleQrCodeScanner
|
||||
import com.vitorpamplona.amethyst.ui.screen.AccountStateViewModel
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.ConnectOrbotDialog
|
||||
import com.vitorpamplona.amethyst.ui.theme.Font14SP
|
||||
import com.vitorpamplona.amethyst.ui.theme.Size20dp
|
||||
import com.vitorpamplona.amethyst.ui.theme.Size35dp
|
||||
import com.vitorpamplona.amethyst.ui.theme.Size40dp
|
||||
import com.vitorpamplona.amethyst.ui.theme.ThemeComparisonRow
|
||||
import com.vitorpamplona.amethyst.ui.theme.placeholderText
|
||||
import com.vitorpamplona.quartz.signers.ExternalSignerLauncher
|
||||
import com.vitorpamplona.quartz.signers.SignerType
|
||||
import kotlinx.coroutines.CancellationException
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import java.util.UUID
|
||||
|
||||
@Preview(device = "spec:width=2160px,height=2340px,dpi=440")
|
||||
@Composable
|
||||
fun LoginPage() {
|
||||
val accountViewModel: AccountStateViewModel = viewModel()
|
||||
|
||||
ThemeComparisonRow(
|
||||
onDark = {
|
||||
LoginPage(accountViewModel, true) {}
|
||||
},
|
||||
onLight = {
|
||||
LoginPage(accountViewModel, true) {}
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalComposeUiApi::class)
|
||||
@Composable
|
||||
fun LoginPage(
|
||||
accountViewModel: AccountStateViewModel,
|
||||
accountStateViewModel: AccountStateViewModel,
|
||||
isFirstLogin: Boolean,
|
||||
onWantsToLogin: () -> Unit,
|
||||
) {
|
||||
val key = remember { mutableStateOf(TextFieldValue("")) }
|
||||
var errorMessage by remember { mutableStateOf("") }
|
||||
@@ -157,6 +174,7 @@ fun LoginPage(
|
||||
activity.prepareToLaunchSigner()
|
||||
launcher.launch(it)
|
||||
} catch (e: Exception) {
|
||||
if (e is CancellationException) throw e
|
||||
Log.e("Signer", "Error opening Signer app", e)
|
||||
scope.launch(Dispatchers.Main) {
|
||||
Toast.makeText(
|
||||
@@ -193,7 +211,7 @@ fun LoginPage(
|
||||
}
|
||||
|
||||
if (acceptedTerms.value && key.value.text.isNotBlank()) {
|
||||
accountViewModel.login(
|
||||
accountStateViewModel.login(
|
||||
key.value.text,
|
||||
useProxy.value,
|
||||
proxyPort.value.toInt(),
|
||||
@@ -208,219 +226,103 @@ fun LoginPage(
|
||||
}
|
||||
|
||||
Column(
|
||||
modifier = Modifier.fillMaxSize().verticalScroll(rememberScrollState()),
|
||||
verticalArrangement = Arrangement.SpaceBetween,
|
||||
modifier =
|
||||
Modifier
|
||||
.fillMaxSize()
|
||||
.verticalScroll(rememberScrollState())
|
||||
.padding(Size20dp),
|
||||
verticalArrangement = Arrangement.Center,
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
) {
|
||||
// The first child is glued to the top.
|
||||
// Hence we have nothing at the top, an empty box is used.
|
||||
Box(modifier = Modifier.height(0.dp))
|
||||
Image(
|
||||
painterResource(id = R.drawable.amethyst),
|
||||
contentDescription = stringResource(R.string.app_logo),
|
||||
modifier = Modifier.size(150.dp),
|
||||
contentScale = ContentScale.Inside,
|
||||
)
|
||||
|
||||
// The second child, this column, is centered vertically.
|
||||
Column(
|
||||
modifier = Modifier.padding(20.dp).fillMaxSize(),
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
) {
|
||||
Image(
|
||||
painterResource(id = R.drawable.amethyst),
|
||||
contentDescription = stringResource(R.string.app_logo),
|
||||
modifier = Modifier.size(200.dp),
|
||||
contentScale = ContentScale.Inside,
|
||||
Spacer(modifier = Modifier.height(40.dp))
|
||||
|
||||
var showPassword by remember { mutableStateOf(false) }
|
||||
|
||||
val autofillNode =
|
||||
AutofillNode(
|
||||
autofillTypes = listOf(AutofillType.Password),
|
||||
onFill = { key.value = TextFieldValue(it) },
|
||||
)
|
||||
val autofill = LocalAutofill.current
|
||||
LocalAutofillTree.current += autofillNode
|
||||
|
||||
Spacer(modifier = Modifier.height(40.dp))
|
||||
|
||||
var showPassword by remember { mutableStateOf(false) }
|
||||
|
||||
val autofillNode =
|
||||
AutofillNode(
|
||||
autofillTypes = listOf(AutofillType.Password),
|
||||
onFill = { key.value = TextFieldValue(it) },
|
||||
)
|
||||
val autofill = LocalAutofill.current
|
||||
LocalAutofillTree.current += autofillNode
|
||||
|
||||
OutlinedTextField(
|
||||
modifier =
|
||||
Modifier.onGloballyPositioned { coordinates ->
|
||||
OutlinedTextField(
|
||||
modifier =
|
||||
Modifier
|
||||
.onGloballyPositioned { coordinates ->
|
||||
autofillNode.boundingBox = coordinates.boundsInWindow()
|
||||
}
|
||||
.onFocusChanged { focusState ->
|
||||
autofill?.run {
|
||||
if (focusState.isFocused) {
|
||||
requestAutofillForNode(autofillNode)
|
||||
} else {
|
||||
cancelAutofillForNode(autofillNode)
|
||||
}
|
||||
}
|
||||
},
|
||||
value = key.value,
|
||||
onValueChange = { key.value = it },
|
||||
keyboardOptions =
|
||||
KeyboardOptions(
|
||||
autoCorrect = false,
|
||||
keyboardType = KeyboardType.Password,
|
||||
imeAction = ImeAction.Go,
|
||||
),
|
||||
placeholder = {
|
||||
Text(
|
||||
text = stringResource(R.string.nsec_npub_hex_private_key),
|
||||
color = MaterialTheme.colorScheme.placeholderText,
|
||||
)
|
||||
},
|
||||
trailingIcon = {
|
||||
Row {
|
||||
IconButton(onClick = { showPassword = !showPassword }) {
|
||||
Icon(
|
||||
imageVector =
|
||||
if (showPassword) Icons.Outlined.VisibilityOff else Icons.Outlined.Visibility,
|
||||
contentDescription =
|
||||
if (showPassword) {
|
||||
stringResource(R.string.show_password)
|
||||
} else {
|
||||
stringResource(
|
||||
R.string.hide_password,
|
||||
)
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
},
|
||||
leadingIcon = {
|
||||
if (dialogOpen) {
|
||||
SimpleQrCodeScanner {
|
||||
dialogOpen = false
|
||||
if (!it.isNullOrEmpty()) {
|
||||
key.value = TextFieldValue(it)
|
||||
.onFocusChanged { focusState ->
|
||||
autofill?.run {
|
||||
if (focusState.isFocused) {
|
||||
requestAutofillForNode(autofillNode)
|
||||
} else {
|
||||
cancelAutofillForNode(autofillNode)
|
||||
}
|
||||
}
|
||||
}
|
||||
IconButton(onClick = { dialogOpen = true }) {
|
||||
},
|
||||
value = key.value,
|
||||
onValueChange = { key.value = it },
|
||||
keyboardOptions =
|
||||
KeyboardOptions(
|
||||
autoCorrect = false,
|
||||
keyboardType = KeyboardType.Password,
|
||||
imeAction = ImeAction.Go,
|
||||
),
|
||||
placeholder = {
|
||||
Text(
|
||||
text = stringResource(R.string.nsec_npub_hex_private_key),
|
||||
color = MaterialTheme.colorScheme.placeholderText,
|
||||
)
|
||||
},
|
||||
trailingIcon = {
|
||||
Row {
|
||||
IconButton(onClick = { showPassword = !showPassword }) {
|
||||
Icon(
|
||||
painter = painterResource(R.drawable.ic_qrcode),
|
||||
null,
|
||||
modifier = Modifier.size(24.dp),
|
||||
tint = MaterialTheme.colorScheme.primary,
|
||||
imageVector =
|
||||
if (showPassword) Icons.Outlined.VisibilityOff else Icons.Outlined.Visibility,
|
||||
contentDescription =
|
||||
if (showPassword) {
|
||||
stringResource(R.string.show_password)
|
||||
} else {
|
||||
stringResource(
|
||||
R.string.hide_password,
|
||||
)
|
||||
},
|
||||
)
|
||||
}
|
||||
},
|
||||
visualTransformation =
|
||||
if (showPassword) VisualTransformation.None else PasswordVisualTransformation(),
|
||||
keyboardActions =
|
||||
KeyboardActions(
|
||||
onGo = {
|
||||
if (!acceptedTerms.value) {
|
||||
termsAcceptanceIsRequired =
|
||||
context.getString(R.string.acceptance_of_terms_is_required)
|
||||
}
|
||||
|
||||
if (key.value.text.isBlank()) {
|
||||
errorMessage = context.getString(R.string.key_is_required)
|
||||
}
|
||||
|
||||
if (acceptedTerms.value && key.value.text.isNotBlank()) {
|
||||
accountViewModel.login(key.value.text, useProxy.value, proxyPort.value.toInt()) {
|
||||
errorMessage = context.getString(R.string.invalid_key)
|
||||
}
|
||||
}
|
||||
},
|
||||
),
|
||||
)
|
||||
if (errorMessage.isNotBlank()) {
|
||||
Text(
|
||||
text = errorMessage,
|
||||
color = MaterialTheme.colorScheme.error,
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
)
|
||||
}
|
||||
|
||||
Spacer(modifier = Modifier.height(20.dp))
|
||||
|
||||
if (isFirstLogin) {
|
||||
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||
Checkbox(
|
||||
checked = acceptedTerms.value,
|
||||
onCheckedChange = { acceptedTerms.value = it },
|
||||
)
|
||||
|
||||
val regularText = SpanStyle(color = MaterialTheme.colorScheme.onBackground)
|
||||
|
||||
val clickableTextStyle = SpanStyle(color = MaterialTheme.colorScheme.primary)
|
||||
|
||||
val annotatedTermsString =
|
||||
buildAnnotatedString {
|
||||
withStyle(regularText) { append(stringResource(R.string.i_accept_the)) }
|
||||
|
||||
withStyle(clickableTextStyle) {
|
||||
pushStringAnnotation("openTerms", "")
|
||||
append(stringResource(R.string.terms_of_use))
|
||||
}
|
||||
}
|
||||
|
||||
ClickableText(
|
||||
text = annotatedTermsString,
|
||||
) { spanOffset ->
|
||||
annotatedTermsString.getStringAnnotations(spanOffset, spanOffset).firstOrNull()?.also {
|
||||
span ->
|
||||
if (span.tag == "openTerms") {
|
||||
runCatching {
|
||||
uri.openUri("https://github.com/vitorpamplona/amethyst/blob/main/PRIVACY.md")
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
leadingIcon = {
|
||||
if (dialogOpen) {
|
||||
SimpleQrCodeScanner {
|
||||
dialogOpen = false
|
||||
if (!it.isNullOrEmpty()) {
|
||||
key.value = TextFieldValue(it)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (termsAcceptanceIsRequired.isNotBlank()) {
|
||||
Text(
|
||||
text = termsAcceptanceIsRequired,
|
||||
color = MaterialTheme.colorScheme.error,
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
IconButton(onClick = { dialogOpen = true }) {
|
||||
Icon(
|
||||
painter = painterResource(R.drawable.ic_qrcode),
|
||||
null,
|
||||
modifier = Modifier.size(24.dp),
|
||||
tint = MaterialTheme.colorScheme.primary,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
if (PackageUtils.isOrbotInstalled(context)) {
|
||||
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||
Checkbox(
|
||||
checked = useProxy.value,
|
||||
onCheckedChange = {
|
||||
if (it) {
|
||||
connectOrbotDialogOpen = true
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
Text(stringResource(R.string.connect_via_tor))
|
||||
}
|
||||
|
||||
if (connectOrbotDialogOpen) {
|
||||
ConnectOrbotDialog(
|
||||
onClose = { connectOrbotDialogOpen = false },
|
||||
onPost = {
|
||||
connectOrbotDialogOpen = false
|
||||
useProxy.value = true
|
||||
},
|
||||
onError = {
|
||||
scope.launch {
|
||||
Toast.makeText(
|
||||
context,
|
||||
it,
|
||||
Toast.LENGTH_LONG,
|
||||
)
|
||||
.show()
|
||||
}
|
||||
},
|
||||
proxyPort,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
Spacer(modifier = Modifier.height(20.dp))
|
||||
|
||||
Box(modifier = Modifier.padding(40.dp, 0.dp, 40.dp, 0.dp)) {
|
||||
Button(
|
||||
enabled = acceptedTerms.value,
|
||||
onClick = {
|
||||
},
|
||||
visualTransformation =
|
||||
if (showPassword) VisualTransformation.None else PasswordVisualTransformation(),
|
||||
keyboardActions =
|
||||
KeyboardActions(
|
||||
onGo = {
|
||||
if (!acceptedTerms.value) {
|
||||
termsAcceptanceIsRequired =
|
||||
context.getString(R.string.acceptance_of_terms_is_required)
|
||||
@@ -431,65 +333,176 @@ fun LoginPage(
|
||||
}
|
||||
|
||||
if (acceptedTerms.value && key.value.text.isNotBlank()) {
|
||||
accountViewModel.login(key.value.text, useProxy.value, proxyPort.value.toInt()) {
|
||||
accountStateViewModel.login(key.value.text, useProxy.value, proxyPort.value.toInt()) {
|
||||
errorMessage = context.getString(R.string.invalid_key)
|
||||
}
|
||||
}
|
||||
},
|
||||
),
|
||||
)
|
||||
if (errorMessage.isNotBlank()) {
|
||||
Text(
|
||||
text = errorMessage,
|
||||
color = MaterialTheme.colorScheme.error,
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
)
|
||||
}
|
||||
|
||||
Spacer(modifier = Modifier.height(10.dp))
|
||||
|
||||
if (PackageUtils.isOrbotInstalled(context)) {
|
||||
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||
Checkbox(
|
||||
checked = useProxy.value,
|
||||
onCheckedChange = {
|
||||
if (it) {
|
||||
connectOrbotDialogOpen = true
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
Text(stringResource(R.string.connect_via_tor))
|
||||
}
|
||||
|
||||
if (connectOrbotDialogOpen) {
|
||||
ConnectOrbotDialog(
|
||||
onClose = { connectOrbotDialogOpen = false },
|
||||
onPost = {
|
||||
connectOrbotDialogOpen = false
|
||||
useProxy.value = true
|
||||
},
|
||||
onError = {
|
||||
scope.launch {
|
||||
Toast.makeText(
|
||||
context,
|
||||
it,
|
||||
Toast.LENGTH_LONG,
|
||||
)
|
||||
.show()
|
||||
}
|
||||
},
|
||||
proxyPort,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
if (isFirstLogin) {
|
||||
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||
Checkbox(
|
||||
checked = acceptedTerms.value,
|
||||
onCheckedChange = { acceptedTerms.value = it },
|
||||
)
|
||||
|
||||
val regularText = SpanStyle(color = MaterialTheme.colorScheme.onBackground)
|
||||
|
||||
val clickableTextStyle = SpanStyle(color = MaterialTheme.colorScheme.primary)
|
||||
|
||||
val annotatedTermsString =
|
||||
buildAnnotatedString {
|
||||
withStyle(regularText) { append(stringResource(R.string.i_accept_the)) }
|
||||
|
||||
withStyle(clickableTextStyle) {
|
||||
pushStringAnnotation("openTerms", "")
|
||||
append(stringResource(R.string.terms_of_use))
|
||||
}
|
||||
}
|
||||
|
||||
ClickableText(
|
||||
text = annotatedTermsString,
|
||||
) { spanOffset ->
|
||||
annotatedTermsString.getStringAnnotations(spanOffset, spanOffset).firstOrNull()?.also {
|
||||
span ->
|
||||
if (span.tag == "openTerms") {
|
||||
runCatching {
|
||||
uri.openUri("https://github.com/vitorpamplona/amethyst/blob/main/PRIVACY.md")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (termsAcceptanceIsRequired.isNotBlank()) {
|
||||
Text(
|
||||
text = termsAcceptanceIsRequired,
|
||||
color = MaterialTheme.colorScheme.error,
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
Spacer(modifier = Modifier.height(10.dp))
|
||||
|
||||
Box(modifier = Modifier.padding(40.dp, 0.dp, 40.dp, 0.dp)) {
|
||||
Button(
|
||||
enabled = acceptedTerms.value,
|
||||
onClick = {
|
||||
if (!acceptedTerms.value) {
|
||||
termsAcceptanceIsRequired =
|
||||
context.getString(R.string.acceptance_of_terms_is_required)
|
||||
}
|
||||
|
||||
if (key.value.text.isBlank()) {
|
||||
errorMessage = context.getString(R.string.key_is_required)
|
||||
}
|
||||
|
||||
if (acceptedTerms.value && key.value.text.isNotBlank()) {
|
||||
accountStateViewModel.login(key.value.text, useProxy.value, proxyPort.value.toInt()) {
|
||||
errorMessage = context.getString(R.string.invalid_key)
|
||||
}
|
||||
}
|
||||
},
|
||||
shape = RoundedCornerShape(Size35dp),
|
||||
modifier = Modifier.height(50.dp),
|
||||
) {
|
||||
Text(
|
||||
text = stringResource(R.string.login),
|
||||
modifier = Modifier.padding(horizontal = 40.dp),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
if (PackageUtils.isExternalSignerInstalled(context)) {
|
||||
Box(modifier = Modifier.padding(40.dp, 20.dp, 40.dp, 0.dp)) {
|
||||
Button(
|
||||
enabled = acceptedTerms.value,
|
||||
onClick = {
|
||||
if (!acceptedTerms.value) {
|
||||
termsAcceptanceIsRequired =
|
||||
context.getString(R.string.acceptance_of_terms_is_required)
|
||||
return@Button
|
||||
}
|
||||
|
||||
loginWithExternalSigner = true
|
||||
return@Button
|
||||
},
|
||||
shape = RoundedCornerShape(Size35dp),
|
||||
modifier = Modifier.height(50.dp),
|
||||
) {
|
||||
Text(
|
||||
text = stringResource(R.string.login),
|
||||
text = stringResource(R.string.login_with_external_signer),
|
||||
modifier = Modifier.padding(horizontal = 40.dp),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
if (PackageUtils.isAmberInstalled(context)) {
|
||||
Box(modifier = Modifier.padding(40.dp, 40.dp, 40.dp, 0.dp)) {
|
||||
Button(
|
||||
enabled = acceptedTerms.value,
|
||||
onClick = {
|
||||
if (!acceptedTerms.value) {
|
||||
termsAcceptanceIsRequired =
|
||||
context.getString(R.string.acceptance_of_terms_is_required)
|
||||
return@Button
|
||||
}
|
||||
|
||||
loginWithExternalSigner = true
|
||||
return@Button
|
||||
},
|
||||
shape = RoundedCornerShape(Size35dp),
|
||||
modifier = Modifier.height(50.dp),
|
||||
) {
|
||||
Text(
|
||||
text = stringResource(R.string.login_with_external_signer),
|
||||
modifier = Modifier.padding(horizontal = 40.dp),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// The last child is glued to the bottom.
|
||||
ClickableText(
|
||||
text = AnnotatedString(stringResource(R.string.generate_a_new_key)),
|
||||
modifier = Modifier.padding(20.dp).fillMaxWidth(),
|
||||
onClick = {
|
||||
if (acceptedTerms.value) {
|
||||
accountViewModel.newKey(useProxy.value, proxyPort.value.toInt())
|
||||
} else {
|
||||
termsAcceptanceIsRequired = context.getString(R.string.acceptance_of_terms_is_required)
|
||||
}
|
||||
},
|
||||
style =
|
||||
TextStyle(
|
||||
fontSize = Font14SP,
|
||||
textDecoration = TextDecoration.Underline,
|
||||
color = MaterialTheme.colorScheme.primary,
|
||||
textAlign = TextAlign.Center,
|
||||
),
|
||||
)
|
||||
Spacer(modifier = Modifier.height(Size40dp))
|
||||
|
||||
Text(text = stringResource(R.string.don_t_have_an_account))
|
||||
|
||||
Spacer(modifier = Modifier.height(Size20dp))
|
||||
|
||||
Box(modifier = Modifier.padding(Size40dp, 0.dp, Size40dp, 0.dp)) {
|
||||
OutlinedButton(
|
||||
onClick = onWantsToLogin,
|
||||
shape = RoundedCornerShape(Size35dp),
|
||||
modifier = Modifier.height(50.dp),
|
||||
) {
|
||||
Text(
|
||||
text = stringResource(R.string.sign_up),
|
||||
modifier = Modifier.padding(horizontal = Size40dp),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,308 @@
|
||||
/**
|
||||
* Copyright (c) 2023 Vitor Pamplona
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
* this software and associated documentation files (the "Software"), to deal in
|
||||
* the Software without restriction, including without limitation the rights to use,
|
||||
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
|
||||
* Software, and to permit persons to whom the Software is furnished to do so,
|
||||
* subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
|
||||
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
package com.vitorpamplona.amethyst.ui.screen.loggedOff
|
||||
|
||||
import android.widget.Toast
|
||||
import androidx.compose.foundation.Image
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.rememberScrollState
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.foundation.text.ClickableText
|
||||
import androidx.compose.foundation.text.KeyboardActions
|
||||
import androidx.compose.foundation.text.KeyboardOptions
|
||||
import androidx.compose.foundation.verticalScroll
|
||||
import androidx.compose.material3.Button
|
||||
import androidx.compose.material3.Checkbox
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.OutlinedButton
|
||||
import androidx.compose.material3.OutlinedTextField
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.rememberCoroutineScope
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.layout.ContentScale
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.platform.LocalUriHandler
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.SpanStyle
|
||||
import androidx.compose.ui.text.buildAnnotatedString
|
||||
import androidx.compose.ui.text.input.ImeAction
|
||||
import androidx.compose.ui.text.input.KeyboardType
|
||||
import androidx.compose.ui.text.input.TextFieldValue
|
||||
import androidx.compose.ui.text.withStyle
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.lifecycle.viewmodel.compose.viewModel
|
||||
import com.vitorpamplona.amethyst.R
|
||||
import com.vitorpamplona.amethyst.service.PackageUtils
|
||||
import com.vitorpamplona.amethyst.ui.screen.AccountStateViewModel
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.ConnectOrbotDialog
|
||||
import com.vitorpamplona.amethyst.ui.theme.Size10dp
|
||||
import com.vitorpamplona.amethyst.ui.theme.Size20dp
|
||||
import com.vitorpamplona.amethyst.ui.theme.Size35dp
|
||||
import com.vitorpamplona.amethyst.ui.theme.Size40dp
|
||||
import com.vitorpamplona.amethyst.ui.theme.ThemeComparisonRow
|
||||
import com.vitorpamplona.amethyst.ui.theme.placeholderText
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
@Preview(device = "spec:width=2160px,height=2340px,dpi=440")
|
||||
@Composable
|
||||
fun SignUpPage() {
|
||||
val accountViewModel: AccountStateViewModel = viewModel()
|
||||
|
||||
ThemeComparisonRow(
|
||||
onDark = {
|
||||
SignUpPage(accountViewModel) {}
|
||||
},
|
||||
onLight = {
|
||||
SignUpPage(accountViewModel) {}
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun SignUpPage(
|
||||
accountStateViewModel: AccountStateViewModel,
|
||||
onWantsToLogin: () -> Unit,
|
||||
) {
|
||||
val displayName = remember { mutableStateOf(TextFieldValue("")) }
|
||||
var errorMessage by remember { mutableStateOf("") }
|
||||
val acceptedTerms = remember { mutableStateOf(false) }
|
||||
var termsAcceptanceIsRequired by remember { mutableStateOf("") }
|
||||
|
||||
val uri = LocalUriHandler.current
|
||||
val context = LocalContext.current
|
||||
val useProxy = remember { mutableStateOf(false) }
|
||||
val proxyPort = remember { mutableStateOf("9050") }
|
||||
var connectOrbotDialogOpen by remember { mutableStateOf(false) }
|
||||
val scope = rememberCoroutineScope()
|
||||
|
||||
Column(
|
||||
modifier =
|
||||
Modifier
|
||||
.fillMaxSize()
|
||||
.verticalScroll(rememberScrollState())
|
||||
.padding(Size20dp),
|
||||
verticalArrangement = Arrangement.Center,
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
) {
|
||||
Image(
|
||||
painterResource(id = R.drawable.amethyst),
|
||||
contentDescription = stringResource(R.string.app_logo),
|
||||
modifier = Modifier.size(150.dp),
|
||||
contentScale = ContentScale.Inside,
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.height(Size40dp))
|
||||
|
||||
Text(text = stringResource(R.string.welcome), style = MaterialTheme.typography.titleLarge)
|
||||
|
||||
Spacer(modifier = Modifier.height(Size20dp))
|
||||
|
||||
Text(text = stringResource(R.string.how_should_we_call_you), style = MaterialTheme.typography.titleMedium)
|
||||
|
||||
Spacer(modifier = Modifier.height(Size20dp))
|
||||
|
||||
OutlinedTextField(
|
||||
value = displayName.value,
|
||||
onValueChange = { displayName.value = it },
|
||||
keyboardOptions =
|
||||
KeyboardOptions(
|
||||
autoCorrect = false,
|
||||
keyboardType = KeyboardType.Text,
|
||||
imeAction = ImeAction.Go,
|
||||
),
|
||||
placeholder = {
|
||||
Text(
|
||||
text = stringResource(R.string.my_awesome_name),
|
||||
color = MaterialTheme.colorScheme.placeholderText,
|
||||
)
|
||||
},
|
||||
keyboardActions =
|
||||
KeyboardActions(
|
||||
onGo = {
|
||||
if (!acceptedTerms.value) {
|
||||
termsAcceptanceIsRequired =
|
||||
context.getString(R.string.acceptance_of_terms_is_required)
|
||||
}
|
||||
|
||||
if (displayName.value.text.isBlank()) {
|
||||
errorMessage = context.getString(R.string.name_is_required)
|
||||
}
|
||||
|
||||
if (acceptedTerms.value && displayName.value.text.isNotBlank()) {
|
||||
accountStateViewModel.login(displayName.value.text, useProxy.value, proxyPort.value.toInt()) {
|
||||
errorMessage = context.getString(R.string.invalid_key)
|
||||
}
|
||||
}
|
||||
},
|
||||
),
|
||||
)
|
||||
if (errorMessage.isNotBlank()) {
|
||||
Text(
|
||||
text = errorMessage,
|
||||
color = MaterialTheme.colorScheme.error,
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
)
|
||||
}
|
||||
|
||||
Spacer(modifier = Modifier.height(10.dp))
|
||||
|
||||
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||
Checkbox(
|
||||
checked = acceptedTerms.value,
|
||||
onCheckedChange = { acceptedTerms.value = it },
|
||||
)
|
||||
|
||||
val regularText = SpanStyle(color = MaterialTheme.colorScheme.onBackground)
|
||||
|
||||
val clickableTextStyle = SpanStyle(color = MaterialTheme.colorScheme.primary)
|
||||
|
||||
val annotatedTermsString =
|
||||
buildAnnotatedString {
|
||||
withStyle(regularText) { append(stringResource(R.string.i_accept_the)) }
|
||||
|
||||
withStyle(clickableTextStyle) {
|
||||
pushStringAnnotation("openTerms", "")
|
||||
append(stringResource(R.string.terms_of_use))
|
||||
}
|
||||
}
|
||||
|
||||
ClickableText(
|
||||
text = annotatedTermsString,
|
||||
) { spanOffset ->
|
||||
annotatedTermsString.getStringAnnotations(spanOffset, spanOffset).firstOrNull()?.also {
|
||||
span ->
|
||||
if (span.tag == "openTerms") {
|
||||
runCatching {
|
||||
uri.openUri("https://github.com/vitorpamplona/amethyst/blob/main/PRIVACY.md")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (termsAcceptanceIsRequired.isNotBlank()) {
|
||||
Text(
|
||||
text = termsAcceptanceIsRequired,
|
||||
color = MaterialTheme.colorScheme.error,
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
)
|
||||
}
|
||||
|
||||
if (PackageUtils.isOrbotInstalled(context)) {
|
||||
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||
Checkbox(
|
||||
checked = useProxy.value,
|
||||
onCheckedChange = {
|
||||
if (it) {
|
||||
connectOrbotDialogOpen = true
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
Text(stringResource(R.string.connect_via_tor))
|
||||
}
|
||||
|
||||
if (connectOrbotDialogOpen) {
|
||||
ConnectOrbotDialog(
|
||||
onClose = { connectOrbotDialogOpen = false },
|
||||
onPost = {
|
||||
connectOrbotDialogOpen = false
|
||||
useProxy.value = true
|
||||
},
|
||||
onError = {
|
||||
scope.launch {
|
||||
Toast.makeText(
|
||||
context,
|
||||
it,
|
||||
Toast.LENGTH_LONG,
|
||||
)
|
||||
.show()
|
||||
}
|
||||
},
|
||||
proxyPort,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
Spacer(modifier = Modifier.height(Size10dp))
|
||||
|
||||
Box(modifier = Modifier.padding(Size40dp, 0.dp, Size40dp, 0.dp)) {
|
||||
Button(
|
||||
enabled = acceptedTerms.value,
|
||||
onClick = {
|
||||
if (!acceptedTerms.value) {
|
||||
termsAcceptanceIsRequired = context.getString(R.string.acceptance_of_terms_is_required)
|
||||
}
|
||||
|
||||
if (displayName.value.text.isBlank()) {
|
||||
errorMessage = context.getString(R.string.key_is_required)
|
||||
}
|
||||
|
||||
if (acceptedTerms.value && displayName.value.text.isNotBlank()) {
|
||||
accountStateViewModel.newKey(useProxy.value, proxyPort.value.toInt(), displayName.value.text)
|
||||
}
|
||||
},
|
||||
shape = RoundedCornerShape(Size35dp),
|
||||
modifier = Modifier.height(50.dp),
|
||||
) {
|
||||
Text(
|
||||
text = stringResource(R.string.create_account),
|
||||
modifier = Modifier.padding(horizontal = Size40dp),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
Spacer(modifier = Modifier.height(Size40dp))
|
||||
|
||||
Text(text = stringResource(R.string.already_have_an_account))
|
||||
|
||||
Spacer(modifier = Modifier.height(Size20dp))
|
||||
|
||||
Box(modifier = Modifier.padding(Size40dp, 0.dp, Size40dp, 0.dp)) {
|
||||
OutlinedButton(
|
||||
onClick = onWantsToLogin,
|
||||
shape = RoundedCornerShape(Size35dp),
|
||||
modifier = Modifier.height(50.dp),
|
||||
) {
|
||||
Text(
|
||||
text = stringResource(R.string.login),
|
||||
modifier = Modifier.padding(horizontal = Size40dp),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -24,7 +24,9 @@ import android.app.Activity
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.border
|
||||
import androidx.compose.foundation.isSystemInDarkTheme
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
@@ -128,62 +130,92 @@ val RepostPictureBorderDark = Modifier.border(2.dp, DarkColorPalette.background,
|
||||
val RepostPictureBorderLight = Modifier.border(2.dp, LightColorPalette.background, CircleShape)
|
||||
|
||||
val DarkImageModifier =
|
||||
Modifier.fillMaxWidth().clip(shape = QuoteBorder).border(1.dp, DarkSubtleBorder, QuoteBorder)
|
||||
Modifier
|
||||
.fillMaxWidth()
|
||||
.clip(shape = QuoteBorder)
|
||||
.border(1.dp, DarkSubtleBorder, QuoteBorder)
|
||||
|
||||
val LightImageModifier =
|
||||
Modifier.fillMaxWidth().clip(shape = QuoteBorder).border(1.dp, LightSubtleBorder, QuoteBorder)
|
||||
Modifier
|
||||
.fillMaxWidth()
|
||||
.clip(shape = QuoteBorder)
|
||||
.border(1.dp, LightSubtleBorder, QuoteBorder)
|
||||
|
||||
val DarkProfile35dpModifier = Modifier.size(Size35dp).clip(shape = CircleShape)
|
||||
val DarkProfile35dpModifier =
|
||||
Modifier
|
||||
.size(Size35dp)
|
||||
.clip(shape = CircleShape)
|
||||
|
||||
val LightProfile35dpModifier = Modifier.fillMaxWidth().clip(shape = CircleShape)
|
||||
val LightProfile35dpModifier =
|
||||
Modifier
|
||||
.fillMaxWidth()
|
||||
.clip(shape = CircleShape)
|
||||
|
||||
val DarkReplyBorderModifier =
|
||||
Modifier.padding(top = 5.dp)
|
||||
Modifier
|
||||
.padding(top = 5.dp)
|
||||
.fillMaxWidth()
|
||||
.clip(shape = QuoteBorder)
|
||||
.border(1.dp, DarkSubtleBorder, QuoteBorder)
|
||||
|
||||
val LightReplyBorderModifier =
|
||||
Modifier.padding(top = 2.dp, bottom = 0.dp, start = 0.dp, end = 0.dp)
|
||||
Modifier
|
||||
.padding(top = 2.dp, bottom = 0.dp, start = 0.dp, end = 0.dp)
|
||||
.fillMaxWidth()
|
||||
.clip(shape = QuoteBorder)
|
||||
.border(1.dp, LightSubtleBorder, QuoteBorder)
|
||||
|
||||
val DarkInnerPostBorderModifier =
|
||||
Modifier.padding(top = 5.dp)
|
||||
Modifier
|
||||
.padding(top = 5.dp)
|
||||
.fillMaxWidth()
|
||||
.clip(shape = QuoteBorder)
|
||||
.border(1.dp, DarkSubtleBorder, QuoteBorder)
|
||||
|
||||
val LightInnerPostBorderModifier =
|
||||
Modifier.padding(top = 5.dp)
|
||||
Modifier
|
||||
.padding(top = 5.dp)
|
||||
.fillMaxWidth()
|
||||
.clip(shape = QuoteBorder)
|
||||
.border(1.dp, LightSubtleBorder, QuoteBorder)
|
||||
|
||||
val DarkChannelNotePictureModifier =
|
||||
Modifier.size(30.dp)
|
||||
Modifier
|
||||
.size(30.dp)
|
||||
.clip(shape = CircleShape)
|
||||
.background(DarkColorPalette.background)
|
||||
.border(2.dp, DarkColorPalette.background, CircleShape)
|
||||
|
||||
val LightChannelNotePictureModifier =
|
||||
Modifier.size(30.dp)
|
||||
Modifier
|
||||
.size(30.dp)
|
||||
.clip(shape = CircleShape)
|
||||
.background(LightColorPalette.background)
|
||||
.border(2.dp, LightColorPalette.background, CircleShape)
|
||||
|
||||
val LightRelayIconModifier =
|
||||
Modifier.size(Size13dp).clip(shape = CircleShape).background(LightColorPalette.background)
|
||||
Modifier
|
||||
.size(Size13dp)
|
||||
.clip(shape = CircleShape)
|
||||
.background(LightColorPalette.background)
|
||||
|
||||
val DarkRelayIconModifier =
|
||||
Modifier.size(Size13dp).clip(shape = CircleShape).background(DarkColorPalette.background)
|
||||
Modifier
|
||||
.size(Size13dp)
|
||||
.clip(shape = CircleShape)
|
||||
.background(DarkColorPalette.background)
|
||||
|
||||
val LightLargeRelayIconModifier =
|
||||
Modifier.size(Size55dp).clip(shape = CircleShape).background(LightColorPalette.background)
|
||||
Modifier
|
||||
.size(Size55dp)
|
||||
.clip(shape = CircleShape)
|
||||
.background(LightColorPalette.background)
|
||||
|
||||
val DarkLargeRelayIconModifier =
|
||||
Modifier.size(Size55dp).clip(shape = CircleShape).background(DarkColorPalette.background)
|
||||
Modifier
|
||||
.size(Size55dp)
|
||||
.clip(shape = CircleShape)
|
||||
.background(DarkColorPalette.background)
|
||||
|
||||
val RichTextDefaults = RichTextStyle().resolveDefaults()
|
||||
|
||||
@@ -203,7 +235,8 @@ val MarkDownStyleOnDark =
|
||||
fontSize = Font14SP,
|
||||
),
|
||||
modifier =
|
||||
Modifier.padding(0.dp)
|
||||
Modifier
|
||||
.padding(0.dp)
|
||||
.fillMaxWidth()
|
||||
.clip(shape = QuoteBorder)
|
||||
.border(1.dp, DarkSubtleBorder, QuoteBorder)
|
||||
@@ -240,7 +273,8 @@ val MarkDownStyleOnLight =
|
||||
fontSize = Font14SP,
|
||||
),
|
||||
modifier =
|
||||
Modifier.padding(0.dp)
|
||||
Modifier
|
||||
.padding(0.dp)
|
||||
.fillMaxWidth()
|
||||
.clip(shape = QuoteBorder)
|
||||
.border(1.dp, LightSubtleBorder, QuoteBorder)
|
||||
@@ -402,19 +436,49 @@ fun AmethystTheme(
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun ThemeComparison(
|
||||
fun ThemeComparisonColumn(
|
||||
onDark: @Composable () -> Unit,
|
||||
onLight: @Composable () -> Unit,
|
||||
) {
|
||||
Column {
|
||||
val darkTheme: SharedPreferencesViewModel = viewModel()
|
||||
darkTheme.updateTheme(ThemeType.DARK)
|
||||
AmethystTheme(darkTheme) { Surface(color = MaterialTheme.colorScheme.background) { onDark() } }
|
||||
Box {
|
||||
val darkTheme: SharedPreferencesViewModel = viewModel()
|
||||
darkTheme.updateTheme(ThemeType.DARK)
|
||||
AmethystTheme(darkTheme) {
|
||||
Surface(color = MaterialTheme.colorScheme.background) { onDark() }
|
||||
}
|
||||
}
|
||||
|
||||
val lightTheme: SharedPreferencesViewModel = viewModel()
|
||||
lightTheme.updateTheme(ThemeType.LIGHT)
|
||||
AmethystTheme(lightTheme) {
|
||||
Surface(color = MaterialTheme.colorScheme.background) { onLight() }
|
||||
Box {
|
||||
val lightTheme: SharedPreferencesViewModel = viewModel()
|
||||
lightTheme.updateTheme(ThemeType.LIGHT)
|
||||
AmethystTheme(lightTheme) {
|
||||
Surface(color = MaterialTheme.colorScheme.background) { onLight() }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun ThemeComparisonRow(
|
||||
onDark: @Composable () -> Unit,
|
||||
onLight: @Composable () -> Unit,
|
||||
) {
|
||||
Row {
|
||||
Box(modifier = Modifier.weight(1f)) {
|
||||
val darkTheme: SharedPreferencesViewModel = viewModel()
|
||||
darkTheme.updateTheme(ThemeType.DARK)
|
||||
AmethystTheme(darkTheme) {
|
||||
Surface(color = MaterialTheme.colorScheme.background) { onDark() }
|
||||
}
|
||||
}
|
||||
|
||||
Box(modifier = Modifier.weight(1f)) {
|
||||
val lightTheme: SharedPreferencesViewModel = viewModel()
|
||||
lightTheme.updateTheme(ThemeType.LIGHT)
|
||||
AmethystTheme(lightTheme) {
|
||||
Surface(color = MaterialTheme.colorScheme.background) { onLight() }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,14 +8,12 @@
|
||||
android:fillColor="#7F2EFF"/>
|
||||
<path
|
||||
android:pathData="M15.1,24l-0.5,-0.2c-0.2,-0.1 -4.5,-2 -6.8,-6.4c-0.3,-0.6 -0.1,-1.4 0.6,-1.7c0.6,-0.3 1.4,-0.1 1.7,0.6c1.4,2.8 3.9,4.4 5,4.9c1.1,-0.6 3.6,-2.2 5,-4.9c0.3,-0.6 1.1,-0.9 1.7,-0.6c0.6,0.3 0.9,1.1 0.6,1.7c-2.2,4.4 -6.6,6.3 -6.8,6.4L15.1,24z"
|
||||
android:fillColor="#FFFFFF"/>
|
||||
<path
|
||||
android:pathData="M15.1,24.3l-0.6,-0.3c-0.2,-0.1 -4.6,-2 -6.9,-6.5c-0.2,-0.4 -0.2,-0.8 -0.1,-1.2c0.1,-0.4 0.4,-0.7 0.8,-0.9c0.4,-0.2 0.8,-0.2 1.2,-0.1c0.4,0.1 0.7,0.4 0.9,0.8c1.3,2.6 3.7,4.2 4.8,4.8c1.1,-0.6 3.5,-2.2 4.8,-4.8c0.2,-0.4 0.5,-0.6 0.9,-0.8c0.4,-0.1 0.8,-0.1 1.2,0.1c0.4,0.2 0.6,0.5 0.8,0.9c0.1,0.4 0.1,0.8 -0.1,1.2c-2.3,4.5 -6.7,6.4 -6.9,6.5L15.1,24.3zM8.9,15.8c-0.2,0 -0.3,0 -0.5,0.1C8.2,16 8,16.3 7.9,16.5c-0.1,0.3 -0.1,0.5 0.1,0.8c2.2,4.3 6.5,6.2 6.7,6.3l0.4,0.2l0.4,-0.2c0.2,-0.1 4.5,-2 6.7,-6.3c0.1,-0.2 0.1,-0.5 0.1,-0.8c-0.1,-0.3 -0.3,-0.5 -0.5,-0.6c-0.2,-0.1 -0.5,-0.1 -0.8,-0.1c-0.3,0.1 -0.5,0.3 -0.6,0.5c-1.4,2.8 -4,4.5 -5.1,5.1l-0.1,0.1l-0.1,-0.1c-1,-0.6 -3.6,-2.2 -5.1,-5.1c-0.1,-0.2 -0.3,-0.4 -0.6,-0.5C9.1,15.8 9,15.8 8.9,15.8z"
|
||||
android:fillColor="#7F2EFF"/>
|
||||
android:fillColor="#FFFFFF"
|
||||
android:strokeColor="#7F2EFF"
|
||||
android:strokeWidth="0.5"/>
|
||||
<path
|
||||
android:pathData="M15,15c-2.9,0 -5.2,-2.3 -5.2,-5.2s2.3,-5.2 5.2,-5.2s5.2,2.3 5.2,5.2S17.8,15 15,15zM15,7.2c-1.4,0 -2.6,1.2 -2.6,2.6s1.2,2.6 2.6,2.6s2.6,-1.2 2.6,-2.6S16.4,7.2 15,7.2z"
|
||||
android:fillColor="#FFFFFF"/>
|
||||
<path
|
||||
android:pathData="M15,15.2c-3,0 -5.4,-2.4 -5.4,-5.4S12,4.4 15,4.4s5.4,2.4 5.4,5.4S18,15.2 15,15.2zM15,4.9c-2.7,0 -4.9,2.2 -4.9,4.9s2.2,4.9 4.9,4.9c2.7,0 4.9,-2.2 4.9,-4.9S17.7,4.9 15,4.9zM15,12.6c-1.6,0 -2.9,-1.3 -2.9,-2.9s1.3,-2.9 2.9,-2.9c1.6,0 2.9,1.3 2.9,2.9S16.5,12.6 15,12.6zM15,7.4c-1.3,0 -2.4,1.1 -2.4,2.4s1.1,2.4 2.4,2.4c1.3,0 2.4,-1.1 2.4,-2.4S16.3,7.4 15,7.4z"
|
||||
android:fillColor="#7F2EFF"/>
|
||||
android:fillColor="#FFFFFF"
|
||||
android:strokeColor="#7F2EFF"
|
||||
android:strokeWidth="0.5"/>
|
||||
</vector>
|
||||
|
||||
@@ -90,6 +90,8 @@
|
||||
<string name="add_a_relay">Přidat přeposílání</string>
|
||||
<string name="display_name">Zobrazované jméno</string>
|
||||
<string name="my_display_name">Moje zobrazované jméno</string>
|
||||
<string name="my_awesome_name">Ostrich McAwesome</string>
|
||||
<string name="welcome">Vítejte Ostrich!</string>
|
||||
<string name="username">Uživatelské jméno</string>
|
||||
<string name="my_username">Moje uživatelské jméno</string>
|
||||
<string name="about_me">O mně</string>
|
||||
@@ -143,7 +145,14 @@
|
||||
<string name="terms_of_use">podmínkami použití</string>
|
||||
<string name="acceptance_of_terms_is_required">Je vyžadováno přijetí podmínek</string>
|
||||
<string name="key_is_required">Klíč je povinný</string>
|
||||
<string name="name_is_required">Jméno je povinné</string>
|
||||
<string name="login">Přihlásit se</string>
|
||||
<string name="sign_up">Registrovat se</string>
|
||||
<string name="create_account">Vytvořit účet</string>
|
||||
<string name="how_should_we_call_you">Jak bychom vám měli říkat?</string>
|
||||
<string name="don_t_have_an_account">Nemáte Nostr účet?</string>
|
||||
<string name="already_have_an_account">Máte již Nostr účet?</string>
|
||||
<string name="create_a_new_account">Vytvořit nový účet</string>
|
||||
<string name="generate_a_new_key">Vygenerovat nový klíč</string>
|
||||
<string name="loading_feed">Načítání zdroje</string>
|
||||
<string name="loading_account">Načítání účtu</string>
|
||||
@@ -523,6 +532,7 @@
|
||||
<string name="could_not_assemble_lnurl_from_lightning_address_check_the_user_s_setup">Nepodařilo se sestavit LNUrl z Lightning adresy \"%1$s\". Zkontrolujte nastavení uživatele</string>
|
||||
<string name="the_receiver_s_lightning_service_at_is_not_available_it_was_calculated_from_the_lightning_address_error_check_if_the_server_is_up_and_if_the_lightning_address_is_correct">Služba blesku příjemce v %1$s není k dispozici. Byla vypočtena z adresy blesku \"%2$s\". Chyba: %3$s. Zkontrolujte, zda je server aktivní a zda je adresa blesku správná</string>
|
||||
<string name="could_not_resolve_check_if_you_are_connected_if_the_server_is_up_and_if_the_lightning_address_is_correct">Nepodařilo se vyřešit %1$s. Zkontrolujte, zda jste připojeni, zda je server aktivní a zda je adresa %2$s správná</string>
|
||||
<string name="could_not_resolve_check_if_you_are_connected_if_the_server_is_up_and_if_the_lightning_address_is_correct_exception">Nepodařilo se vyřešit %1$s. Zkontrolujte, zda jste připojeni, zda je server aktivní a zda je adresa %2$s správná.\n\nVýjimka byla: %3$s</string>
|
||||
<string name="could_not_fetch_invoice_from">Nepodařilo se načíst fakturu z %1$s</string>
|
||||
<string name="error_parsing_json_from_lightning_address_check_the_user_s_lightning_setup">Chyba při analýze JSON z Lightning adresy. Zkontrolujte uživatelovo bleskové nastavení</string>
|
||||
<string name="callback_url_not_found_in_the_user_s_lightning_address_server_configuration">URL adresa zpětného volání nebyla nalezena v konfiguraci serveru pro bleskovou adresu uživatele</string>
|
||||
|
||||
@@ -90,6 +90,8 @@
|
||||
<string name="add_a_relay">Relay hinzufügen</string>
|
||||
<string name="display_name">Anzeigename</string>
|
||||
<string name="my_display_name">Mein Anzeigename</string>
|
||||
<string name="my_awesome_name">Ostrich McAwesome</string>
|
||||
<string name="welcome">Willkommen Ostrich!</string>
|
||||
<string name="username">Benutzername</string>
|
||||
<string name="my_username">Mein Benutzername</string>
|
||||
<string name="about_me">Über mich</string>
|
||||
@@ -147,7 +149,14 @@ erie gespeichert</string>
|
||||
|
||||
anz der Bedingungen ist erforderlich</string>
|
||||
<string name="key_is_required">Schlüssel ist erforderlich</string>
|
||||
<string name="name_is_required">Ein Name wird benötigt</string>
|
||||
<string name="login">Anmeldung</string>
|
||||
<string name="sign_up">Anmelden</string>
|
||||
<string name="create_account">Konto erstellen</string>
|
||||
<string name="how_should_we_call_you">Wie sollen wir Sie nennen?</string>
|
||||
<string name="don_t_have_an_account">Sie haben kein Nostr-Konto?</string>
|
||||
<string name="already_have_an_account">Haben Sie schon ein Nostr-Konto?</string>
|
||||
<string name="create_a_new_account">Neues Konto anlegen</string>
|
||||
<string name="generate_a_new_key">Neuen Schlüssel generieren</string>
|
||||
<string name="loading_feed">Feed wird geladen</string>
|
||||
<string name="loading_account">Konto wird geladen</string>
|
||||
@@ -527,6 +536,7 @@ anz der Bedingungen ist erforderlich</string>
|
||||
<string name="could_not_assemble_lnurl_from_lightning_address_check_the_user_s_setup">Konnte keine LNUrl aus der Lightning-Adresse \"%1$s\" zusammenstellen. Überprüfen Sie die Konfiguration des Benutzers</string>
|
||||
<string name="the_receiver_s_lightning_service_at_is_not_available_it_was_calculated_from_the_lightning_address_error_check_if_the_server_is_up_and_if_the_lightning_address_is_correct">Der Lightning-Service des Empfängers unter %1$s ist nicht verfügbar. Er wurde aus der Lightning-Adresse \"%2$s\" berechnet. Fehler: %3$s. Überprüfen Sie, ob der Server online ist und ob die Lightning-Adresse korrekt ist</string>
|
||||
<string name="could_not_resolve_check_if_you_are_connected_if_the_server_is_up_and_if_the_lightning_address_is_correct">Konnte %1$s nicht auflösen. Überprüfen Sie, ob Sie verbunden sind, ob der Server online ist und ob die Lightning-Adresse %2$s korrekt ist</string>
|
||||
<string name="could_not_resolve_check_if_you_are_connected_if_the_server_is_up_and_if_the_lightning_address_is_correct_exception">Konnte %1$s nicht auflösen. Überprüfen Sie, ob Sie verbunden sind, ob der Server online ist und ob die Lightning-Adresse %2$s korrekt ist.\n\nAusnahme war: %3$s</string>
|
||||
<string name="could_not_fetch_invoice_from">Konnte Rechnung nicht von %1$s abholen</string>
|
||||
<string name="error_parsing_json_from_lightning_address_check_the_user_s_lightning_setup">Fehler beim analysieren von JSON aus der Lightning-Adresse. Überprüfen Sie die Lightning-Konfiguration des Benutzers</string>
|
||||
<string name="callback_url_not_found_in_the_user_s_lightning_address_server_configuration">Callback-URL nicht in der Serverkonfiguration der Lightning-Adresse des Benutzers gefunden</string>
|
||||
|
||||
@@ -90,6 +90,8 @@
|
||||
<string name="add_a_relay">Agregar un relé</string>
|
||||
<string name="display_name">Nombre para mostrar</string>
|
||||
<string name="my_display_name">Mi nombre para mostrar</string>
|
||||
<string name="my_awesome_name">Avestruz Maravillosa</string>
|
||||
<string name="welcome">¡Te damos la bienvenida, Avestruz!</string>
|
||||
<string name="username">Nombre de usuario</string>
|
||||
<string name="my_username">Mi nombre de usuario</string>
|
||||
<string name="about_me">Quién soy</string>
|
||||
@@ -143,7 +145,14 @@
|
||||
<string name="terms_of_use">condiciones de uso</string>
|
||||
<string name="acceptance_of_terms_is_required">Es obligatorio aceptar las condiciones</string>
|
||||
<string name="key_is_required">Se requiere la clave</string>
|
||||
<string name="name_is_required">Se requiere un nombre</string>
|
||||
<string name="login">Iniciar sesión</string>
|
||||
<string name="sign_up">Registrarse</string>
|
||||
<string name="create_account">Crear cuenta</string>
|
||||
<string name="how_should_we_call_you">¿Cómo debemos llamarte?</string>
|
||||
<string name="don_t_have_an_account">¿No tienes una cuenta de Nostr?</string>
|
||||
<string name="already_have_an_account">¿Ya tienes una cuenta de Nostr?</string>
|
||||
<string name="create_a_new_account">Crear una cuenta nueva</string>
|
||||
<string name="generate_a_new_key">Generar una clave nueva</string>
|
||||
<string name="loading_feed">Cargando feed</string>
|
||||
<string name="loading_account">Cargando cuenta</string>
|
||||
|
||||
@@ -90,6 +90,8 @@
|
||||
<string name="add_a_relay">Agregar un relé</string>
|
||||
<string name="display_name">Nombre para mostrar</string>
|
||||
<string name="my_display_name">Mi nombre para mostrar</string>
|
||||
<string name="my_awesome_name">Avestruz Maravillosa</string>
|
||||
<string name="welcome">¡Te damos la bienvenida, Avestruz!</string>
|
||||
<string name="username">Nombre de usuario</string>
|
||||
<string name="my_username">Mi nombre de usuario</string>
|
||||
<string name="about_me">Quién soy</string>
|
||||
@@ -143,7 +145,14 @@
|
||||
<string name="terms_of_use">condiciones de uso</string>
|
||||
<string name="acceptance_of_terms_is_required">Es obligatorio aceptar las condiciones</string>
|
||||
<string name="key_is_required">Se requiere la clave</string>
|
||||
<string name="name_is_required">Se requiere un nombre</string>
|
||||
<string name="login">Iniciar sesión</string>
|
||||
<string name="sign_up">Registrarse</string>
|
||||
<string name="create_account">Crear cuenta</string>
|
||||
<string name="how_should_we_call_you">¿Cómo debemos llamarte?</string>
|
||||
<string name="don_t_have_an_account">¿No tienes una cuenta de Nostr?</string>
|
||||
<string name="already_have_an_account">¿Ya tienes una cuenta de Nostr?</string>
|
||||
<string name="create_a_new_account">Crear una cuenta nueva</string>
|
||||
<string name="generate_a_new_key">Generar una clave nueva</string>
|
||||
<string name="loading_feed">Cargando feed</string>
|
||||
<string name="loading_account">Cargando cuenta</string>
|
||||
|
||||
@@ -143,7 +143,14 @@
|
||||
<string name="terms_of_use">conditions d\'utilisation</string>
|
||||
<string name="acceptance_of_terms_is_required">L\'acceptation des conditions est requise</string>
|
||||
<string name="key_is_required">La clé est requise</string>
|
||||
<string name="name_is_required">Un nom est requis</string>
|
||||
<string name="login">Connexion</string>
|
||||
<string name="sign_up">Inscription</string>
|
||||
<string name="create_account">Créer un Compte</string>
|
||||
<string name="how_should_we_call_you">Comment devrions-nous vous appeler ?</string>
|
||||
<string name="don_t_have_an_account">Vous n\'avez pas de compte Nostr ?</string>
|
||||
<string name="already_have_an_account">Vous avez déjà un compte Nostr ?</string>
|
||||
<string name="create_a_new_account">Créer un nouveau compte</string>
|
||||
<string name="generate_a_new_key">Générer une nouvelle clé</string>
|
||||
<string name="loading_feed">Chargement du flux</string>
|
||||
<string name="loading_account">Chargement du compte</string>
|
||||
@@ -571,4 +578,11 @@
|
||||
<string name="classifieds_category_food">Nourriture</string>
|
||||
<string name="classifieds_category_misc">Divers</string>
|
||||
<string name="classifieds_category_other">Autres</string>
|
||||
<string name="failed_to_upload_media_no_details">Échec du téléversement du fichier</string>
|
||||
<string name="could_not_open_the_compressed_file">Impossible d\'ouvrir le fichier compressé</string>
|
||||
<string name="error_when_compressing_media">Erreur lors de la compression du média : %1$s</string>
|
||||
<string name="failed_to_upload_media">Erreur de téléversement : %1$s</string>
|
||||
<string name="server_did_not_provide_a_url_after_uploading">Le serveur n\'a pas fourni d\'URL après le téléversement</string>
|
||||
<string name="could_not_download_from_the_server">Impossible de télécharger le média depuis le serveur</string>
|
||||
<string name="could_not_prepare_local_file_to_upload">Impossible de préparer le fichier local à téléverser: %1$s</string>
|
||||
</resources>
|
||||
|
||||
@@ -90,6 +90,8 @@
|
||||
<string name="add_a_relay">Csomópont hozzáadása</string>
|
||||
<string name="display_name">Megjelenítendő Név</string>
|
||||
<string name="my_display_name">Az én megjelenített névem</string>
|
||||
<string name="my_awesome_name">Király McStrucc</string>
|
||||
<string name="welcome">Üdvözlünk Strucc!</string>
|
||||
<string name="username">Felhasználónév</string>
|
||||
<string name="my_username">Felhasználónevem</string>
|
||||
<string name="about_me">Rólam</string>
|
||||
@@ -143,7 +145,14 @@
|
||||
<string name="terms_of_use">használati feltételeket</string>
|
||||
<string name="acceptance_of_terms_is_required">A feltételek elfogadása szükséges</string>
|
||||
<string name="key_is_required">Kulcs szükséges</string>
|
||||
<string name="name_is_required">Név megadása kötelező</string>
|
||||
<string name="login">Bejelentkezés</string>
|
||||
<string name="sign_up">Regisztráció</string>
|
||||
<string name="create_account">Fiók létrehozása</string>
|
||||
<string name="how_should_we_call_you">Hogyan szólíthatunk?</string>
|
||||
<string name="don_t_have_an_account">Nincs Nostr fiókod?</string>
|
||||
<string name="already_have_an_account">Van már Nostr-fiókod?</string>
|
||||
<string name="create_a_new_account">Új fiók létrehozása</string>
|
||||
<string name="generate_a_new_key">Új kulcs létrehozása</string>
|
||||
<string name="loading_feed">Lista betöltése</string>
|
||||
<string name="loading_account">Fiók betöltése</string>
|
||||
|
||||
@@ -90,6 +90,8 @@
|
||||
<string name="add_a_relay">Voeg relay toe</string>
|
||||
<string name="display_name">Naam</string>
|
||||
<string name="my_display_name">Mijn naam</string>
|
||||
<string name="my_awesome_name">Struisvogel McAwesome</string>
|
||||
<string name="welcome">Welkom Struisvogel!</string>
|
||||
<string name="username">Gebruikersnaam</string>
|
||||
<string name="my_username">Mijn gebruikersnaam</string>
|
||||
<string name="about_me">Over mij</string>
|
||||
@@ -143,7 +145,14 @@
|
||||
<string name="terms_of_use">gebruiksvoorwaarden</string>
|
||||
<string name="acceptance_of_terms_is_required">Accepteren van de voorwaarden is vereist</string>
|
||||
<string name="key_is_required">Sleutel is vereist</string>
|
||||
<string name="name_is_required">Een naam is verplicht</string>
|
||||
<string name="login">Inloggen</string>
|
||||
<string name="sign_up">Aanmelden</string>
|
||||
<string name="create_account">Account aanmaken</string>
|
||||
<string name="how_should_we_call_you">Hoe moeten we je noemen?</string>
|
||||
<string name="don_t_have_an_account">Heb je geen Nostr account?</string>
|
||||
<string name="already_have_an_account">Heb je al een Nostr account?</string>
|
||||
<string name="create_a_new_account">Een nieuw account aanmaken</string>
|
||||
<string name="generate_a_new_key">Genereer een nieuwe sleutel</string>
|
||||
<string name="loading_feed">Feed laden…</string>
|
||||
<string name="loading_account">Account wordt geladen</string>
|
||||
|
||||
@@ -90,6 +90,8 @@
|
||||
<string name="add_a_relay">Adicionar um Relay</string>
|
||||
<string name="display_name">Nome de Exibição</string>
|
||||
<string name="my_display_name">Meu nome de exibição</string>
|
||||
<string name="my_awesome_name">McAwesome Ostrich</string>
|
||||
<string name="welcome">Bem-vindo, Ostrich!</string>
|
||||
<string name="username">Usuário</string>
|
||||
<string name="my_username">Meu usuário</string>
|
||||
<string name="about_me">Sobre mim</string>
|
||||
@@ -143,7 +145,14 @@
|
||||
<string name="terms_of_use">termos de uso</string>
|
||||
<string name="acceptance_of_terms_is_required">É necessário aceitar os termos de uso</string>
|
||||
<string name="key_is_required">Chave é obrigatória</string>
|
||||
<string name="name_is_required">Um nome é necessário</string>
|
||||
<string name="login">Entrar</string>
|
||||
<string name="sign_up">Enviar</string>
|
||||
<string name="create_account">Criar Conta</string>
|
||||
<string name="how_should_we_call_you">Como deveríamos chamar você?</string>
|
||||
<string name="don_t_have_an_account">Não tem uma conta Nostr?</string>
|
||||
<string name="already_have_an_account">Já tem uma conta Nostr?</string>
|
||||
<string name="create_a_new_account">Criar uma nova conta</string>
|
||||
<string name="generate_a_new_key">Gerar uma nova chave</string>
|
||||
<string name="loading_feed">Carregando feed</string>
|
||||
<string name="loading_account">Carregando conta</string>
|
||||
@@ -523,6 +532,7 @@
|
||||
<string name="could_not_assemble_lnurl_from_lightning_address_check_the_user_s_setup">Não foi possível montar o LNUrl a partir do Endereço Lightning \"%1$s\". Verifique a configuração do usuário</string>
|
||||
<string name="the_receiver_s_lightning_service_at_is_not_available_it_was_calculated_from_the_lightning_address_error_check_if_the_server_is_up_and_if_the_lightning_address_is_correct">O serviço Lightning do destinatário em %1$s não está disponível. Foi calculado a partir do endereço Lightning \"%2$s\". Erro: %3$s. Verifique se o servidor está funcionando e se o endereço Lightning está correto</string>
|
||||
<string name="could_not_resolve_check_if_you_are_connected_if_the_server_is_up_and_if_the_lightning_address_is_correct">Não foi possível resolver %1$s. Verifique se você está conectado, se o servidor está funcionando e se o endereço Lightning %2$s está correto</string>
|
||||
<string name="could_not_resolve_check_if_you_are_connected_if_the_server_is_up_and_if_the_lightning_address_is_correct_exception">Não foi possível resolver %1$s. Verifique se você está conectado, se o servidor está funcionando e se o endereço Lightning %2$s está correto.\n\nExceção foi: %3$s</string>
|
||||
<string name="could_not_fetch_invoice_from">Não foi possível obter a fatura de %1$s</string>
|
||||
<string name="error_parsing_json_from_lightning_address_check_the_user_s_lightning_setup">Erro ao analisar JSON do Endereço Lightning. Verifique a configuração de Lightning do usuário</string>
|
||||
<string name="callback_url_not_found_in_the_user_s_lightning_address_server_configuration">URL de retorno não encontrada na configuração do servidor de endereço Lightning do usuário</string>
|
||||
|
||||
@@ -2,4 +2,13 @@
|
||||
<resources xmlns:tools="http://schemas.android.com/tools">
|
||||
<string name="point_to_the_qr_code">Покажите на КР код</string>
|
||||
<string name="show_qr">Прикажи КР</string>
|
||||
<string name="profile_image">слика профила</string>
|
||||
<string name="scan_qr">Скенирај QR</string>
|
||||
<string name="show_anyway">Схов Аниваи</string>
|
||||
<string name="post_was_flagged_as_inappropriate_by">Објаву је игнорисао или је пријавио</string>
|
||||
<string name="post_not_found">Догађај се учитава или се не може пронаћи на вашој листи релеја</string>
|
||||
<string name="channel_image">Слика канала</string>
|
||||
<string name="referenced_event_not_found">Референтни догађај није пронађен</string>
|
||||
<string name="could_not_decrypt_the_message">Није могуће дешифровати поруку</string>
|
||||
<string name="group_picture">Групна слика</string>
|
||||
</resources>
|
||||
|
||||
@@ -90,6 +90,8 @@
|
||||
<string name="add_a_relay">Lägg till Relä</string>
|
||||
<string name="display_name">Visningsnamn</string>
|
||||
<string name="my_display_name">Mitt visningsnamn</string>
|
||||
<string name="my_awesome_name">Struts McAwesome</string>
|
||||
<string name="welcome">Välkommen struts!</string>
|
||||
<string name="username">Användarnamn</string>
|
||||
<string name="my_username">Mitt användarnamn</string>
|
||||
<string name="about_me">Om mig</string>
|
||||
@@ -143,7 +145,14 @@
|
||||
<string name="terms_of_use">villkor</string>
|
||||
<string name="acceptance_of_terms_is_required">Godkännande av villkor krävs</string>
|
||||
<string name="key_is_required">Nyckel krävs</string>
|
||||
<string name="name_is_required">Namn är obligatoriskt</string>
|
||||
<string name="login">Inloggning</string>
|
||||
<string name="sign_up">Registrera dig</string>
|
||||
<string name="create_account">Skapa ett konto</string>
|
||||
<string name="how_should_we_call_you">Hur ska vi kalla dig?</string>
|
||||
<string name="don_t_have_an_account">Har du inget Nostr konto?</string>
|
||||
<string name="already_have_an_account">Har du redan ett Nostr konto?</string>
|
||||
<string name="create_a_new_account">Skapa ett nytt konto</string>
|
||||
<string name="generate_a_new_key">Skapa en ny nyckel</string>
|
||||
<string name="loading_feed">Ladda flöde</string>
|
||||
<string name="loading_account">Laddar kontot</string>
|
||||
@@ -521,6 +530,7 @@
|
||||
<string name="could_not_assemble_lnurl_from_lightning_address_check_the_user_s_setup">Kunde inte montera LNUrl från Lightning Address \"%1$s\". Kontrollera användarens konfiguration</string>
|
||||
<string name="the_receiver_s_lightning_service_at_is_not_available_it_was_calculated_from_the_lightning_address_error_check_if_the_server_is_up_and_if_the_lightning_address_is_correct">Mottagarens Lightning-tjänst på %1$s är inte tillgänglig. Den beräknades från Lightning Address \"%2$s\". Fel: %3$s. Kontrollera om servern är uppe och om Lightning Addressen är korrekt</string>
|
||||
<string name="could_not_resolve_check_if_you_are_connected_if_the_server_is_up_and_if_the_lightning_address_is_correct">Kunde inte lösa %1$s. Kontrollera om du är ansluten, om servern är uppe och om Lightning Addressen %2$s är korrekt</string>
|
||||
<string name="could_not_resolve_check_if_you_are_connected_if_the_server_is_up_and_if_the_lightning_address_is_correct_exception">Kunde inte lösa %1$s. Kontrollera om du är ansluten, om servern är uppe och om Lightning Addressen %2$s är korrekt.\n\nUndantag var: %3$s</string>
|
||||
<string name="could_not_fetch_invoice_from">Kunde inte hämta fakturan från %1$s</string>
|
||||
<string name="error_parsing_json_from_lightning_address_check_the_user_s_lightning_setup">Fel vid tolkning av JSON från Lightning Address. Kontrollera användarens Lightning-konfiguration</string>
|
||||
<string name="callback_url_not_found_in_the_user_s_lightning_address_server_configuration">Återuppringnings-URL hittades inte i användarens konfiguration för Lightning Address-servern</string>
|
||||
|
||||
@@ -93,6 +93,8 @@
|
||||
<string name="add_a_relay">Add a Relay</string>
|
||||
<string name="display_name">Display Name</string>
|
||||
<string name="my_display_name">My display name</string>
|
||||
<string name="my_awesome_name">Ostrich McAwesome</string>
|
||||
<string name="welcome">Welcome Ostrich!</string>
|
||||
<string name="username">Username</string>
|
||||
<string name="my_username">My username</string>
|
||||
<string name="about_me">About me</string>
|
||||
@@ -146,7 +148,14 @@
|
||||
<string name="terms_of_use">terms of use</string>
|
||||
<string name="acceptance_of_terms_is_required">Acceptance of terms is required</string>
|
||||
<string name="key_is_required">Key is required</string>
|
||||
<string name="name_is_required">A name is required</string>
|
||||
<string name="login">Login</string>
|
||||
<string name="sign_up">Sign Up</string>
|
||||
<string name="create_account">Create Account</string>
|
||||
<string name="how_should_we_call_you">How should we call you?</string>
|
||||
<string name="don_t_have_an_account">Don\'t have a Nostr account?</string>
|
||||
<string name="already_have_an_account">Already have a Nostr account?</string>
|
||||
<string name="create_a_new_account">Create a new account</string>
|
||||
<string name="generate_a_new_key">Generate a new key</string>
|
||||
<string name="loading_feed">Loading feed</string>
|
||||
<string name="loading_account">Loading account</string>
|
||||
@@ -525,7 +534,7 @@
|
||||
<string name="messages_new_subject_message_placeholder">Changing the name for the new goals.</string>
|
||||
|
||||
<string name="paste_from_clipboard">Paste from clipboard</string>
|
||||
|
||||
|
||||
<string name="language_description">For the App\'s Interface</string>
|
||||
<string name="theme_description">Dark, Light or System theme</string>
|
||||
<string name="automatically_load_images_gifs_description">Automatically load images and GIFs</string>
|
||||
@@ -616,6 +625,7 @@
|
||||
<string name="could_not_assemble_lnurl_from_lightning_address_check_the_user_s_setup">Could not assemble LNUrl from Lightning Address \"%1$s\". Check the user\'s setup</string>
|
||||
<string name="the_receiver_s_lightning_service_at_is_not_available_it_was_calculated_from_the_lightning_address_error_check_if_the_server_is_up_and_if_the_lightning_address_is_correct">The receiver\'s lightning service at %1$s is not available. It was calculated from the lightning address \"%2$s\". Error: %3$s. Check if the server is up and if the lightning address is correct</string>
|
||||
<string name="could_not_resolve_check_if_you_are_connected_if_the_server_is_up_and_if_the_lightning_address_is_correct">Could not resolve %1$s. Check if you are connected, if the server is up and if the lightning address %2$s is correct</string>
|
||||
<string name="could_not_resolve_check_if_you_are_connected_if_the_server_is_up_and_if_the_lightning_address_is_correct_exception">Could not resolve %1$s. Check if you are connected, if the server is up and if the lightning address %2$s is correct.\n\nException was: %3$s</string>
|
||||
<string name="could_not_fetch_invoice_from">Could not fetch invoice from %1$s</string>
|
||||
<string name="error_parsing_json_from_lightning_address_check_the_user_s_lightning_setup">Error Parsing JSON from Lightning Address. Check the user\'s lightning setup</string>
|
||||
<string name="callback_url_not_found_in_the_user_s_lightning_address_server_configuration">Callback URL not found in the User\'s lightning address server configuration</string>
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
<locale android:name="cy-GB"/>
|
||||
<locale android:name="da-DK"/>
|
||||
<locale android:name="de"/>
|
||||
<locale android:name="el-GB"/>
|
||||
<locale android:name="el-GR"/>
|
||||
<locale android:name="en"/>
|
||||
<locale android:name="en-GB"/>
|
||||
<locale android:name="eo"/>
|
||||
|
||||
@@ -33,6 +33,7 @@ import com.google.mlkit.nl.translate.TranslatorOptions
|
||||
import com.linkedin.urls.detection.UrlDetector
|
||||
import com.linkedin.urls.detection.UrlDetectorOptions
|
||||
import com.vitorpamplona.amethyst.service.checkNotInMainThread
|
||||
import kotlinx.coroutines.CancellationException
|
||||
import java.util.concurrent.Executors
|
||||
import java.util.regex.Pattern
|
||||
|
||||
@@ -161,7 +162,8 @@ object LanguageTranslatorService {
|
||||
val short = "A$counter"
|
||||
counter++
|
||||
returningList.put(short, tag)
|
||||
} catch (_: Exception) {
|
||||
} catch (e: Exception) {
|
||||
if (e is CancellationException) throw e
|
||||
}
|
||||
}
|
||||
return returningList
|
||||
@@ -177,7 +179,8 @@ object LanguageTranslatorService {
|
||||
val short = "A$counter"
|
||||
counter++
|
||||
returningList.put(short, lnInvoice)
|
||||
} catch (_: Exception) {
|
||||
} catch (e: Exception) {
|
||||
if (e is CancellationException) throw e
|
||||
}
|
||||
}
|
||||
return returningList
|
||||
|
||||
@@ -23,6 +23,7 @@ package com.vitorpamplona.amethyst.service.notifications
|
||||
import android.util.Log
|
||||
import com.google.firebase.messaging.FirebaseMessaging
|
||||
import com.vitorpamplona.amethyst.AccountInfo
|
||||
import kotlinx.coroutines.CancellationException
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.tasks.await
|
||||
|
||||
@@ -38,6 +39,7 @@ object PushNotificationUtils {
|
||||
try {
|
||||
RegisterAccounts(accounts).go(FirebaseMessaging.getInstance().token.await())
|
||||
} catch (e: Exception) {
|
||||
if (e is CancellationException) throw e
|
||||
Log.e("Firebase token", "failed to get firebase token", e)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,14 +3,14 @@ import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
|
||||
buildscript {
|
||||
ext {
|
||||
fragment_version = "1.6.2"
|
||||
lifecycle_version = '2.6.2'
|
||||
compose_ui_version = '1.5.4'
|
||||
lifecycle_version = '2.7.0'
|
||||
compose_ui_version = '1.6.0'
|
||||
nav_version = '2.7.6'
|
||||
room_version = "2.4.3"
|
||||
accompanist_version = '0.32.0'
|
||||
accompanist_version = '0.34.0'
|
||||
coil_version = '2.5.0'
|
||||
vico_version = '1.13.1'
|
||||
media3_version = '1.2.0'
|
||||
media3_version = '1.2.1'
|
||||
core_ktx_version = '1.12.0'
|
||||
material3_version = '1.1.2'
|
||||
}
|
||||
|
||||
@@ -43,7 +43,7 @@ dependencies {
|
||||
implementation "androidx.compose.runtime:runtime:$compose_ui_version"
|
||||
|
||||
// Bitcoin secp256k1 bindings to Android
|
||||
api 'fr.acinq.secp256k1:secp256k1-kmp-jni-android:0.12.0'
|
||||
api 'fr.acinq.secp256k1:secp256k1-kmp-jni-android:0.13.0'
|
||||
|
||||
// LibSodium for ChaCha encryption
|
||||
implementation "com.goterl:lazysodium-android:5.1.0@aar"
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<manifest>
|
||||
|
||||
</manifest>
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user