Compare commits

...

61 Commits

Author SHA1 Message Date
Vitor Pamplona
145b0d8f0a v0.83.8 2024-01-31 14:26:58 -05:00
Vitor Pamplona
eae07e2fe0 Making sure Cancellation of coroutines stops long processes. 2024-01-30 17:33:44 -05:00
Vitor Pamplona
6b60b1434f Name refactoring 2024-01-30 13:52:02 -05:00
Vitor Pamplona
3ff4db264a Merge branch 'main' of https://github.com/vitorpamplona/amethyst 2024-01-30 13:40:08 -05:00
Vitor Pamplona
21f042fa95 Fixes secondary buttons theme in the login and signup screens. 2024-01-30 13:38:41 -05:00
Vitor Pamplona
e26bbc2e61 Merge pull request #755 from vitorpamplona/l10n_crowdin_translations
New Crowdin Translations
2024-01-30 13:16:43 -05:00
Crowdin Bot
5135026e6a New Crowdin translations by GitHub Action 2024-01-30 18:12:06 +00:00
Vitor Pamplona
a72f5ae01f Removing unnecessary logs 2024-01-30 13:09:43 -05:00
Vitor Pamplona
f2ed2d94c4 Update README.md
Fixing Markdown
2024-01-30 12:01:23 -05:00
Vitor Pamplona
75d703ac0a Testing GitStr with a Readme patch 2024-01-30 11:54:01 -05:00
Vitor Pamplona
277bf0a603 Fixes misalignment of some links to users in the middle of the note. 2024-01-30 11:51:02 -05:00
Vitor Pamplona
1ac990b67c Set account to avoid lateinit property account has not been initialized 2024-01-30 11:20:25 -05:00
Vitor Pamplona
e59f676d47 updates secp256k1 2024-01-30 11:00:33 -05:00
Vitor Pamplona
312d82fdf6 moving the order or contributing 2024-01-30 09:47:31 -05:00
Vitor Pamplona
08659738bd Merge branch 'main' of https://github.com/vitorpamplona/amethyst 2024-01-30 09:46:06 -05:00
Vitor Pamplona
f1b0958903 Merge pull request #754 from davotoula/fix_gr_locale
Correct GR locale config
2024-01-30 09:45:36 -05:00
Vitor Pamplona
998aa2aed1 Adding GitStr 2024-01-30 09:44:48 -05:00
David Kaspar
370a18f716 Correct GR locale config 2024-01-30 13:30:25 +00:00
Vitor Pamplona
6638defbb9 Fixes content sensitivity for NIP-54 images. 2024-01-29 18:26:25 -05:00
Vitor Pamplona
d88eb014b7 Adding the exception to the Zap error message. 2024-01-29 17:14:31 -05:00
Vitor Pamplona
14e9a91d40 Updates dependencies 2024-01-29 16:28:53 -05:00
Vitor Pamplona
3ed45f282c Fixing NIP number for wraps/seals 2024-01-29 11:19:56 -05:00
Vitor Pamplona
2ff090dac1 New NIP available 2024-01-29 11:03:15 -05:00
Vitor Pamplona
281d86b4bf Small refactoring on the HTTP client 2024-01-27 11:58:39 -05:00
Vitor Pamplona
504e55a93f Merge pull request #753 from greenart7c3/proxy_fix
Recreate http client when changing the proxy or timeout
2024-01-27 11:39:29 -05:00
greenart7c3
c7b2c20119 recreate httpclient when changing the proxy or timeout 2024-01-24 17:18:25 -03:00
Vitor Pamplona
3e102c064b Merge pull request #750 from greenart7c3/queries
remove the need for application package name in the androidManifest for the external signer
2024-01-23 09:22:58 -05:00
greenart7c3
e0ec8a741c remove the need for application package name in the androidManifest for the external signer 2024-01-22 11:29:10 -03:00
Vitor Pamplona
f1388b46d5 Merge pull request #746 from greenart7c3/main
add a copy url to the url preview card
2024-01-19 06:07:42 -05:00
greenart7c3
0f24e458d8 add a copy url to the url preview card 2024-01-19 07:18:25 -03:00
Vitor Pamplona
b8f3625954 Fixes remember of the wrong hashtag because it's a different post. 2024-01-18 15:57:21 -03:00
Vitor Pamplona
0babc46f7c Merge branch 'main' of https://github.com/vitorpamplona/amethyst 2024-01-18 13:36:24 -03:00
Vitor Pamplona
6d368148cc makes the benchmark module profileable 2024-01-18 13:35:53 -03:00
Vitor Pamplona
3241f0ada1 formatting 2024-01-18 13:35:38 -03:00
Vitor Pamplona
49ad972af8 activates nostr.mom and nos.lol in the default relay set for global because of the Video feed. 2024-01-18 13:34:30 -03:00
Vitor Pamplona
e061575454 Removes unused imports 2024-01-18 13:33:48 -03:00
Vitor Pamplona
694642b42b Merge pull request #745 from vitorpamplona/l10n_crowdin_translations
New Crowdin Translations
2024-01-17 07:27:51 -05:00
Crowdin Bot
f4684ed078 New Crowdin translations by GitHub Action 2024-01-17 12:24:41 +00:00
Vitor Pamplona
db12ff0532 Merge pull request #744 from greenart7c3/reject_permission_amber
add support for always rejected permissions from external signer
2024-01-17 07:23:13 -05:00
greenart7c3
13d9c1ab10 add support for always rejected permissions from external signer 2024-01-17 09:05:24 -03:00
Vitor Pamplona
ac8f0e8600 v0.83.7 Testing new build tools 2024-01-11 12:33:55 -05:00
Vitor Pamplona
21472daab5 Uses new build tools to sign releases 2024-01-11 12:32:58 -05:00
Vitor Pamplona
c6a00386c8 adds distribution to github actions 2024-01-11 12:23:36 -05:00
Vitor Pamplona
9e3aa31c25 Updates Github actions scripts. 2024-01-11 12:06:45 -05:00
Vitor Pamplona
47987fac0f Merge pull request #743 from vitorpamplona/l10n_crowdin_translations
New Crowdin Translations
2024-01-11 10:52:13 -05:00
Crowdin Bot
46c782cf37 New Crowdin translations by GitHub Action 2024-01-11 15:51:24 +00:00
Vitor Pamplona
fd67f4875d Merge pull request #742 from vitorpamplona/l10n_crowdin_translations
New Crowdin Translations
2024-01-11 10:50:28 -05:00
Vitor Pamplona
4e2ff591e6 v0.83.6 2024-01-11 10:49:37 -05:00
Crowdin Bot
0e46145e2d New Crowdin translations by GitHub Action 2024-01-11 15:45:38 +00:00
Vitor Pamplona
383d859544 Fixes the BookmarkScreen update after adding and removing a bookmark to the list 2024-01-11 10:43:51 -05:00
Vitor Pamplona
81400eb863 Solving viewModel creation bug for Public Bookmarks 2024-01-11 10:36:52 -05:00
Vitor Pamplona
422446f53e Fixes bug when creating bookmarks for the first time. 2024-01-11 10:09:41 -05:00
Vitor Pamplona
f1e721f323 Fixes bug in checking bookmarks without running the onReady. 2024-01-11 10:09:19 -05:00
Vitor Pamplona
0fc72eb84d updates mockk 2024-01-11 09:43:11 -05:00
Vitor Pamplona
9234f4b7a8 Improves previews 2024-01-09 17:47:34 -05:00
Vitor Pamplona
0198d56099 Merge branch 'main' of https://github.com/vitorpamplona/amethyst 2024-01-09 16:35:04 -05:00
Vitor Pamplona
8f31c60cd1 Fixing some of the margins in the login screen. 2024-01-09 16:33:50 -05:00
Vitor Pamplona
7cb17954d9 Merge pull request #741 from vitorpamplona/l10n_crowdin_translations
New Crowdin Translations
2024-01-09 16:26:37 -05:00
Crowdin Bot
baf35b202f New Crowdin translations by GitHub Action 2024-01-09 21:25:47 +00:00
Vitor Pamplona
7b7e3624ac New Signup screen 2024-01-09 16:23:17 -05:00
Vitor Pamplona
d27b9ae5a8 Reducing the size of the following icon 2024-01-06 19:16:15 -05:00
105 changed files with 1652 additions and 656 deletions

View File

@@ -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*') }}

View File

@@ -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

View File

@@ -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
View File

@@ -9,6 +9,7 @@
/.idea/deploymentTargetDropDown.xml
/.idea/appInsightsSettings.xml
/.idea/ktlint-plugin.xml
/.idea/ktfmt.xml
.DS_Store
/build
/captures

View File

@@ -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
|-------------------------------------------|----------------------------------------------|-------------------------------------------------|--------------------------------------------------------|
| ![Home Feed](./docs/screenshots/home.png) | ![Messages](./docs/screenshots/messages.png) | ![Live Streams](./docs/screenshots/replies.png) | ![Notifications](./docs/screenshots/notifications.png) |
# 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">

View File

@@ -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"

View File

@@ -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}")
}
}

View File

@@ -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.")
}
}

View File

@@ -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 -->

View File

@@ -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)}",

View File

@@ -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
}
}

View File

@@ -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) {

View File

@@ -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()
}
}

View File

@@ -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
}

View File

@@ -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)
}

View File

@@ -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),

View File

@@ -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)
}

View File

@@ -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,

View File

@@ -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
}

View File

@@ -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
}

View File

@@ -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)
}

View File

@@ -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()
}
}

View File

@@ -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")
}

View File

@@ -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
}

View File

@@ -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) {

View File

@@ -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,

View File

@@ -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
}

View File

@@ -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

View File

@@ -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
}
}

View File

@@ -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(

View File

@@ -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"

View File

@@ -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)
}
}

View File

@@ -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()

View File

@@ -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)
}
}

View File

@@ -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()

View File

@@ -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),

View File

@@ -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")) }
}

View File

@@ -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
}
}

View File

@@ -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

View File

@@ -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
}

View File

@@ -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

View File

@@ -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,

View File

@@ -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()
}

View File

@@ -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
}
}

View File

@@ -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
}
}

View File

@@ -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),

View File

@@ -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

View File

@@ -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")

View File

@@ -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 ")

View File

@@ -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))
}
},

View File

@@ -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
}
}

View File

@@ -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,
),

View File

@@ -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
}

View File

@@ -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)
}

View File

@@ -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)
}

View File

@@ -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) {

View File

@@ -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 {

View File

@@ -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,

View File

@@ -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
},
)

View File

@@ -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
}
}

View File

@@ -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 {

View File

@@ -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 }

View File

@@ -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) },
)

View File

@@ -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() },
)

View File

@@ -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
}
}

View File

@@ -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

View File

@@ -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)
}

View File

@@ -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
}

View File

@@ -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 {

View File

@@ -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)

View File

@@ -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,

View File

@@ -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)
}
}
}

View File

@@ -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)
}

View File

@@ -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,

View File

@@ -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)

View File

@@ -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
}

View File

@@ -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

View File

@@ -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

View File

@@ -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
}
}
}
}

View File

@@ -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),
)
}
}
}
}

View File

@@ -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),
)
}
}
}
}

View File

@@ -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() }
}
}
}
}

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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"/>

View File

@@ -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

View File

@@ -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)
}
}

View File

@@ -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'
}

View File

@@ -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"

View File

@@ -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