Merge branch 'main' into claude/strip-file-metadata-sVIEd
This commit is contained in:
@@ -119,7 +119,7 @@ Create 8 hybrid domain skills combining general expertise with AmethystMultiplat
|
|||||||
**Focus:** iosMain patterns, Swift/KMP interop, XCFramework generation
|
**Focus:** iosMain patterns, Swift/KMP interop, XCFramework generation
|
||||||
|
|
||||||
**SKILL.md sections:**
|
**SKILL.md sections:**
|
||||||
- iOS source sets: iosMain, iosX64Main, iosArm64Main
|
- iOS source sets: iosMain, iosArm64Main
|
||||||
- Swift interop: type mapping, nullability
|
- Swift interop: type mapping, nullability
|
||||||
- expect/actual iOS: 10+ examples from quartz/iosMain
|
- expect/actual iOS: 10+ examples from quartz/iosMain
|
||||||
- XCFramework setup: baseName = "quartz-kmpKit"
|
- XCFramework setup: baseName = "quartz-kmpKit"
|
||||||
|
|||||||
Regular → Executable
@@ -9,18 +9,18 @@
|
|||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
]
|
],
|
||||||
"Stop": [
|
"Stop": [
|
||||||
{
|
{
|
||||||
"matcher": "",
|
"matcher": "",
|
||||||
"hooks": [
|
"hooks": [
|
||||||
{
|
{
|
||||||
"type": "command",
|
"type": "command",
|
||||||
"command": "./gradlew spotlessApply",
|
"command": "./gradlew spotlessApply 2>/dev/null || spotless-apply",
|
||||||
"timeout": 120
|
"timeout": 120
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -47,7 +47,7 @@
|
|||||||
|
|
||||||
### :quartz (KMP Nostr Library)
|
### :quartz (KMP Nostr Library)
|
||||||
**Type:** Kotlin Multiplatform Library
|
**Type:** Kotlin Multiplatform Library
|
||||||
**Targets:** JVM, Android, iOS (iosX64, iosArm64, iosSimulatorArm64)
|
**Targets:** JVM, Android, iOS (iosArm64, iosSimulatorArm64)
|
||||||
**Dependencies:**
|
**Dependencies:**
|
||||||
- External: secp256k1, jackson, okhttp, kotlinx.coroutines, kotlinx.collections.immutable
|
- External: secp256k1, jackson, okhttp, kotlinx.coroutines, kotlinx.collections.immutable
|
||||||
- Source sets: commonMain → jvmAndroid → {androidMain, jvmMain}, iosMain
|
- Source sets: commonMain → jvmAndroid → {androidMain, jvmMain}, iosMain
|
||||||
@@ -127,7 +127,6 @@ commonMain (base)
|
|||||||
│ ├─ androidMain (Android platform)
|
│ ├─ androidMain (Android platform)
|
||||||
│ └─ jvmMain (Desktop platform)
|
│ └─ jvmMain (Desktop platform)
|
||||||
└─ iosMain (iOS platform)
|
└─ iosMain (iOS platform)
|
||||||
├─ iosX64Main
|
|
||||||
├─ iosArm64Main
|
├─ iosArm64Main
|
||||||
└─ iosSimulatorArm64Main
|
└─ iosSimulatorArm64Main
|
||||||
```
|
```
|
||||||
|
|||||||
@@ -110,8 +110,8 @@ Think of source sets as a dependency graph, not folders.
|
|||||||
│ - Jackson │ │ │
|
│ - Jackson │ │ │
|
||||||
│ - OkHttp │ └────┬─────────────┘
|
│ - OkHttp │ └────┬─────────────┘
|
||||||
└───┬───────────┬───┘ │
|
└───┬───────────┬───┘ │
|
||||||
│ │ ├─→ iosX64Main
|
│ │ │
|
||||||
▼ ▼ ├─→ iosArm64Main
|
▼ ▼ ├─→ iosArm64Main
|
||||||
┌─────────┐ ┌──────────┐ └─→ iosSimulatorArm64Main
|
┌─────────┐ ┌──────────┐ └─→ iosSimulatorArm64Main
|
||||||
│android │ │jvmMain │
|
│android │ │jvmMain │
|
||||||
│Main │ │(Desktop) │
|
│Main │ │(Desktop) │
|
||||||
@@ -252,7 +252,7 @@ expect fun currentTimeSeconds(): Long
|
|||||||
|
|
||||||
**iOS (iosMain):**
|
**iOS (iosMain):**
|
||||||
- Active development, framework configured
|
- Active development, framework configured
|
||||||
- Architecture targets: iosX64Main, iosArm64Main, iosSimulatorArm64Main
|
- Architecture targets: macosArm64Main, iosArm64Main, iosSimulatorArm64Main
|
||||||
- Platform APIs via platform.posix, Security framework
|
- Platform APIs via platform.posix, Security framework
|
||||||
|
|
||||||
### Web, wasm - Future Targets
|
### Web, wasm - Future Targets
|
||||||
|
|||||||
@@ -29,7 +29,7 @@ Visual guide to source set organization with concrete examples from the codebase
|
|||||||
│ - Jackson │ │ - Platform libs │
|
│ - Jackson │ │ - Platform libs │
|
||||||
│ - OkHttp │ └───────┬───────────┘
|
│ - OkHttp │ └───────┬───────────┘
|
||||||
└────┬─────────┬───┘ │
|
└────┬─────────┬───┘ │
|
||||||
│ │ ├─→ iosX64Main (simulator Intel)
|
│ │ │
|
||||||
│ │ ├─→ iosArm64Main (device ARM64)
|
│ │ ├─→ iosArm64Main (device ARM64)
|
||||||
│ │ └─→ iosSimulatorArm64Main (Apple Silicon)
|
│ │ └─→ iosSimulatorArm64Main (Apple Silicon)
|
||||||
▼ ▼
|
▼ ▼
|
||||||
@@ -238,7 +238,6 @@ iosMain {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
val iosX64Main by getting { dependsOn(iosMain.get()) }
|
|
||||||
val iosArm64Main by getting { dependsOn(iosMain.get()) }
|
val iosArm64Main by getting { dependsOn(iosMain.get()) }
|
||||||
val iosSimulatorArm64Main by getting { dependsOn(iosMain.get()) }
|
val iosSimulatorArm64Main by getting { dependsOn(iosMain.get()) }
|
||||||
```
|
```
|
||||||
@@ -249,7 +248,6 @@ val iosSimulatorArm64Main by getting { dependsOn(iosMain.get()) }
|
|||||||
- Different from Android/Desktop
|
- Different from Android/Desktop
|
||||||
|
|
||||||
**Architecture targets:**
|
**Architecture targets:**
|
||||||
- iosX64Main: Intel simulator
|
|
||||||
- iosArm64Main: Device (iPhone, iPad)
|
- iosArm64Main: Device (iPhone, iPad)
|
||||||
- iosSimulatorArm64Main: Apple Silicon simulator
|
- iosSimulatorArm64Main: Apple Silicon simulator
|
||||||
|
|
||||||
@@ -326,7 +324,7 @@ commonMain
|
|||||||
| androidMain | jvmAndroid | Android framework | Activity, ViewModel |
|
| androidMain | jvmAndroid | Android framework | Activity, ViewModel |
|
||||||
| jvmMain | jvmAndroid | JVM + Compose Desktop | Window, MenuBar |
|
| jvmMain | jvmAndroid | JVM + Compose Desktop | Window, MenuBar |
|
||||||
| iosMain | commonMain | iOS platform | Security framework |
|
| iosMain | commonMain | iOS platform | Security framework |
|
||||||
| iosX64Main | iosMain | Simulator (Intel) | Architecture-specific |
|
| iosMain | Simulator (Intel) | Architecture-specific |
|
||||||
| iosArm64Main | iosMain | Device (ARM64) | Architecture-specific |
|
| iosArm64Main | iosMain | Device (ARM64) | Architecture-specific |
|
||||||
| jsMain | commonMain | JS/DOM | Web (future) |
|
| jsMain | commonMain | JS/DOM | Web (future) |
|
||||||
| wasmMain | commonMain | wasm APIs | WebAssembly (future) |
|
| wasmMain | commonMain | wasm APIs | WebAssembly (future) |
|
||||||
|
|||||||
@@ -76,7 +76,6 @@ fun main() = application {
|
|||||||
|
|
||||||
**Source sets:**
|
**Source sets:**
|
||||||
- iosMain (common iOS code)
|
- iosMain (common iOS code)
|
||||||
- iosX64Main (Intel simulator)
|
|
||||||
- iosArm64Main (device - iPhone/iPad)
|
- iosArm64Main (device - iPhone/iPad)
|
||||||
- iosSimulatorArm64Main (Apple Silicon simulator)
|
- iosSimulatorArm64Main (Apple Silicon simulator)
|
||||||
|
|
||||||
@@ -110,7 +109,7 @@ actual object Secp256k1Instance {
|
|||||||
```kotlin
|
```kotlin
|
||||||
// quartz/build.gradle.kts
|
// quartz/build.gradle.kts
|
||||||
kotlin {
|
kotlin {
|
||||||
listOf(iosX64(), iosArm64(), iosSimulatorArm64())
|
listOf(macosArm64(), iosArm64(), iosSimulatorArm64())
|
||||||
.forEach { target ->
|
.forEach { target ->
|
||||||
target.binaries.framework {
|
target.binaries.framework {
|
||||||
baseName = "quartz-kmpKit"
|
baseName = "quartz-kmpKit"
|
||||||
@@ -310,7 +309,7 @@ fun parseJson(json: String): Event {
|
|||||||
- Manual desktop app testing
|
- Manual desktop app testing
|
||||||
|
|
||||||
**iOS:**
|
**iOS:**
|
||||||
- Unit tests: iosTest (iosX64Test, iosArm64Test, etc.)
|
- Unit tests: iosTest (iosArm64Test, etc.)
|
||||||
- Simulator/device testing
|
- Simulator/device testing
|
||||||
|
|
||||||
**Web (future):**
|
**Web (future):**
|
||||||
|
|||||||
+1
-1
@@ -12,7 +12,7 @@ echo "$JAVA_HOME"
|
|||||||
echo "$(java -version)"
|
echo "$(java -version)"
|
||||||
echo "Running test... "
|
echo "Running test... "
|
||||||
|
|
||||||
./gradlew test
|
./gradlew test --quiet
|
||||||
|
|
||||||
status=$?
|
status=$?
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,69 @@
|
|||||||
|
name: Build APK For Claude
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- 'claude/**'
|
||||||
|
|
||||||
|
concurrency:
|
||||||
|
group: ${{ github.workflow }}-${{ github.ref }}
|
||||||
|
cancel-in-progress: true
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build-benchmark:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
timeout-minutes: 30
|
||||||
|
steps:
|
||||||
|
- name: Checkout code
|
||||||
|
uses: actions/checkout@v6
|
||||||
|
|
||||||
|
- name: Set up JDK 21
|
||||||
|
uses: actions/setup-java@v5
|
||||||
|
with:
|
||||||
|
distribution: 'zulu'
|
||||||
|
java-version: 21
|
||||||
|
|
||||||
|
- name: Cache gradle
|
||||||
|
uses: actions/cache@v5
|
||||||
|
with:
|
||||||
|
path: |
|
||||||
|
~/.gradle/caches
|
||||||
|
~/.gradle/wrapper
|
||||||
|
key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle*', '**/gradle-wrapper.properties') }}
|
||||||
|
restore-keys: |
|
||||||
|
${{ runner.os }}-gradle-
|
||||||
|
|
||||||
|
- name: Build Benchmark APK
|
||||||
|
run: ./gradlew assemblePlayBenchmark
|
||||||
|
|
||||||
|
- name: Upload Play Benchmark APK
|
||||||
|
id: upload
|
||||||
|
uses: actions/upload-artifact@v6
|
||||||
|
with:
|
||||||
|
name: Play Benchmark APK
|
||||||
|
path: amethyst/build/outputs/apk/play/benchmark/amethyst-play-universal-benchmark.apk
|
||||||
|
|
||||||
|
- name: Comment on PR with APK link
|
||||||
|
uses: actions/github-script@v7
|
||||||
|
with:
|
||||||
|
script: |
|
||||||
|
const artifactId = `${{ steps.upload.outputs.artifact-id }}`;
|
||||||
|
const downloadUrl = `${context.serverUrl}/${context.repo.owner}/${context.repo.repo}/actions/runs/${context.runId}/artifacts/${artifactId}`;
|
||||||
|
const body = `📦 **Benchmark APK ready!**\n\nDownload: [Play Benchmark APK](${downloadUrl})`;
|
||||||
|
|
||||||
|
const branch = context.ref.replace('refs/heads/', '');
|
||||||
|
const { data: prs } = await github.rest.pulls.list({
|
||||||
|
owner: context.repo.owner,
|
||||||
|
repo: context.repo.repo,
|
||||||
|
head: `${context.repo.owner}:${branch}`,
|
||||||
|
state: 'open'
|
||||||
|
});
|
||||||
|
|
||||||
|
for (const pr of prs) {
|
||||||
|
await github.rest.issues.createComment({
|
||||||
|
owner: context.repo.owner,
|
||||||
|
repo: context.repo.repo,
|
||||||
|
issue_number: pr.number,
|
||||||
|
body
|
||||||
|
});
|
||||||
|
}
|
||||||
+134
-22
@@ -1,4 +1,4 @@
|
|||||||
name: Test/Build Android
|
name: Test/Build
|
||||||
|
|
||||||
on:
|
on:
|
||||||
pull_request:
|
pull_request:
|
||||||
@@ -6,78 +6,190 @@ on:
|
|||||||
push:
|
push:
|
||||||
branches: [main]
|
branches: [main]
|
||||||
|
|
||||||
jobs:
|
concurrency:
|
||||||
build:
|
group: ${{ github.workflow }}-${{ github.ref }}
|
||||||
runs-on: ubuntu-latest
|
cancel-in-progress: true
|
||||||
timeout-minutes: 30
|
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
lint:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
timeout-minutes: 15
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout code
|
- name: Checkout code
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v6
|
||||||
|
|
||||||
- name: Set up JDK 21
|
- name: Set up JDK 21
|
||||||
uses: actions/setup-java@v4
|
uses: actions/setup-java@v5
|
||||||
with:
|
with:
|
||||||
distribution: 'zulu'
|
distribution: 'zulu'
|
||||||
java-version: 21
|
java-version: 21
|
||||||
|
|
||||||
- name: Cache gradle
|
- name: Cache gradle
|
||||||
uses: actions/cache@v4
|
uses: actions/cache@v5
|
||||||
with:
|
with:
|
||||||
path: ~/.gradle/caches
|
path: |
|
||||||
key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle*') }}
|
~/.gradle/caches
|
||||||
|
~/.gradle/wrapper
|
||||||
|
key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle*', '**/gradle-wrapper.properties') }}
|
||||||
restore-keys: |
|
restore-keys: |
|
||||||
${{ runner.os }}-gradle-
|
${{ runner.os }}-gradle-
|
||||||
|
|
||||||
- name: Linter (gradle)
|
- name: Linter (gradle)
|
||||||
run: ./gradlew spotlessCheck
|
run: ./gradlew spotlessCheck
|
||||||
|
|
||||||
|
test:
|
||||||
|
needs: lint
|
||||||
|
strategy:
|
||||||
|
fail-fast: false
|
||||||
|
matrix:
|
||||||
|
os: [ubuntu-latest, macos-latest, windows-latest]
|
||||||
|
runs-on: ${{ matrix.os }}
|
||||||
|
timeout-minutes: 30
|
||||||
|
defaults:
|
||||||
|
run:
|
||||||
|
shell: bash
|
||||||
|
steps:
|
||||||
|
- name: Checkout code
|
||||||
|
uses: actions/checkout@v6
|
||||||
|
|
||||||
|
- name: Set up JDK 21
|
||||||
|
uses: actions/setup-java@v5
|
||||||
|
with:
|
||||||
|
distribution: 'zulu'
|
||||||
|
java-version: 21
|
||||||
|
|
||||||
|
- name: Cache gradle
|
||||||
|
uses: actions/cache@v5
|
||||||
|
with:
|
||||||
|
path: |
|
||||||
|
~/.gradle/caches
|
||||||
|
~/.gradle/wrapper
|
||||||
|
key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle*', '**/gradle-wrapper.properties') }}
|
||||||
|
restore-keys: |
|
||||||
|
${{ runner.os }}-gradle-
|
||||||
|
|
||||||
- name: Test (gradle)
|
- name: Test (gradle)
|
||||||
run: ./gradlew test --no-daemon
|
run: ./gradlew test --no-daemon
|
||||||
|
|
||||||
- name: Android Test Report
|
- name: Android Test Report
|
||||||
uses: asadmansr/android-test-report-action@v1.2.0
|
uses: asadmansr/android-test-report-action@v1.2.0
|
||||||
if: ${{ always() }} # IMPORTANT: run Android Test Report regardless
|
if: ${{ always() && matrix.os == 'ubuntu-latest' }}
|
||||||
|
|
||||||
|
- name: Upload Test Results
|
||||||
|
uses: actions/upload-artifact@v6
|
||||||
|
if: ${{ always() && matrix.os == 'ubuntu-latest' }}
|
||||||
|
with:
|
||||||
|
name: Test Reports
|
||||||
|
path: amethyst/build/reports
|
||||||
|
|
||||||
|
build-android:
|
||||||
|
needs: test
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
timeout-minutes: 30
|
||||||
|
steps:
|
||||||
|
- name: Checkout code
|
||||||
|
uses: actions/checkout@v6
|
||||||
|
|
||||||
|
- name: Set up JDK 21
|
||||||
|
uses: actions/setup-java@v5
|
||||||
|
with:
|
||||||
|
distribution: 'zulu'
|
||||||
|
java-version: 21
|
||||||
|
|
||||||
|
- name: Cache gradle
|
||||||
|
uses: actions/cache@v5
|
||||||
|
with:
|
||||||
|
path: |
|
||||||
|
~/.gradle/caches
|
||||||
|
~/.gradle/wrapper
|
||||||
|
key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle*', '**/gradle-wrapper.properties') }}
|
||||||
|
restore-keys: |
|
||||||
|
${{ runner.os }}-gradle-
|
||||||
|
|
||||||
- name: Build APK (gradle)
|
- name: Build APK (gradle)
|
||||||
run: ./gradlew assembleDebug
|
run: ./gradlew assembleDebug
|
||||||
|
|
||||||
- name: Upload Play APK
|
- name: Upload Play APK
|
||||||
uses: actions/upload-artifact@v4
|
uses: actions/upload-artifact@v6
|
||||||
with:
|
with:
|
||||||
name: Play Debug APK
|
name: Play Debug APK
|
||||||
path: amethyst/build/outputs/apk/play/debug/amethyst-play-universal-debug.apk
|
path: amethyst/build/outputs/apk/play/debug/amethyst-play-universal-debug.apk
|
||||||
|
|
||||||
- name: Upload FDroid APK
|
- name: Upload FDroid APK
|
||||||
uses: actions/upload-artifact@v4
|
uses: actions/upload-artifact@v6
|
||||||
with:
|
with:
|
||||||
name: FDroid Debug APK
|
name: FDroid Debug APK
|
||||||
path: amethyst/build/outputs/apk/fdroid/debug/amethyst-fdroid-universal-debug.apk
|
path: amethyst/build/outputs/apk/fdroid/debug/amethyst-fdroid-universal-debug.apk
|
||||||
|
|
||||||
- name: Build APK (gradle)
|
- name: Build Benchmark APK (gradle)
|
||||||
run: ./gradlew assembleBenchmark
|
run: ./gradlew assembleBenchmark
|
||||||
|
|
||||||
- name: Upload Play APK Benchmark
|
- name: Upload Play APK Benchmark
|
||||||
uses: actions/upload-artifact@v4
|
uses: actions/upload-artifact@v6
|
||||||
with:
|
with:
|
||||||
name: Play Benchmark APK
|
name: Play Benchmark APK
|
||||||
path: amethyst/build/outputs/apk/play/benchmark/amethyst-play-universal-benchmark.apk
|
path: amethyst/build/outputs/apk/play/benchmark/amethyst-play-universal-benchmark.apk
|
||||||
|
|
||||||
- name: Upload FDroid APK Benchmark
|
- name: Upload FDroid APK Benchmark
|
||||||
uses: actions/upload-artifact@v4
|
uses: actions/upload-artifact@v6
|
||||||
with:
|
with:
|
||||||
name: FDroid Benchmark APK
|
name: FDroid Benchmark APK
|
||||||
path: amethyst/build/outputs/apk/fdroid/benchmark/amethyst-fdroid-universal-benchmark.apk
|
path: amethyst/build/outputs/apk/fdroid/benchmark/amethyst-fdroid-universal-benchmark.apk
|
||||||
|
|
||||||
- name: Upload Compose Reports
|
- name: Upload Compose Reports
|
||||||
uses: actions/upload-artifact@v4
|
uses: actions/upload-artifact@v6
|
||||||
with:
|
with:
|
||||||
name: Compose Reports
|
name: Compose Reports
|
||||||
path: amethyst/build/compose_compiler
|
path: amethyst/build/compose_compiler
|
||||||
|
|
||||||
- name: Upload Test Results
|
build-desktop:
|
||||||
uses: actions/upload-artifact@v4
|
needs: test
|
||||||
with:
|
strategy:
|
||||||
name: Test Reports
|
fail-fast: false
|
||||||
path: amethyst/build/reports
|
matrix:
|
||||||
|
include:
|
||||||
|
- os: ubuntu-latest
|
||||||
|
task: packageDeb
|
||||||
|
artifact-name: Desktop Linux DEB
|
||||||
|
artifact-path: desktopApp/build/compose/binaries/main/deb/*.deb
|
||||||
|
- os: macos-latest
|
||||||
|
task: packageDmg
|
||||||
|
artifact-name: Desktop macOS DMG
|
||||||
|
artifact-path: desktopApp/build/compose/binaries/main/dmg/*.dmg
|
||||||
|
- os: windows-latest
|
||||||
|
task: packageMsi
|
||||||
|
artifact-name: Desktop Windows MSI
|
||||||
|
artifact-path: desktopApp/build/compose/binaries/main/msi/*.msi
|
||||||
|
runs-on: ${{ matrix.os }}
|
||||||
|
timeout-minutes: 30
|
||||||
|
defaults:
|
||||||
|
run:
|
||||||
|
shell: bash
|
||||||
|
steps:
|
||||||
|
- name: Checkout code
|
||||||
|
uses: actions/checkout@v6
|
||||||
|
|
||||||
|
- name: Set up JDK 21
|
||||||
|
uses: actions/setup-java@v5
|
||||||
|
with:
|
||||||
|
distribution: 'zulu'
|
||||||
|
java-version: 21
|
||||||
|
|
||||||
|
- name: Cache gradle
|
||||||
|
uses: actions/cache@v5
|
||||||
|
with:
|
||||||
|
path: |
|
||||||
|
~/.gradle/caches
|
||||||
|
~/.gradle/wrapper
|
||||||
|
key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle*', '**/gradle-wrapper.properties') }}
|
||||||
|
restore-keys: |
|
||||||
|
${{ runner.os }}-gradle-
|
||||||
|
|
||||||
|
- name: Build Desktop Distribution
|
||||||
|
run: ./gradlew :desktopApp:${{ matrix.task }}
|
||||||
|
|
||||||
|
- name: Upload Desktop Distribution
|
||||||
|
uses: actions/upload-artifact@v6
|
||||||
|
with:
|
||||||
|
name: ${{ matrix.artifact-name }}
|
||||||
|
path: ${{ matrix.artifact-path }}
|
||||||
|
|||||||
@@ -6,23 +6,42 @@ on:
|
|||||||
- 'v*' # Push events to matching v*, i.e. v1.0, v20.15.10
|
- 'v*' # Push events to matching v*, i.e. v1.0, v20.15.10
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
deploy:
|
create-release:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
outputs:
|
||||||
|
upload_url: ${{ steps.create_release.outputs.upload_url }}
|
||||||
|
steps:
|
||||||
|
- name: Create Release
|
||||||
|
id: create_release
|
||||||
|
uses: actions/create-release@v1
|
||||||
|
env:
|
||||||
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
with:
|
||||||
|
tag_name: ${{ github.ref }}
|
||||||
|
release_name: Release ${{ github.ref }}
|
||||||
|
draft: false
|
||||||
|
prerelease: true
|
||||||
|
|
||||||
|
deploy-android:
|
||||||
|
needs: create-release
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout code
|
- name: Checkout code
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v6
|
||||||
|
|
||||||
- name: Set up JDK 21
|
- name: Set up JDK 21
|
||||||
uses: actions/setup-java@v4
|
uses: actions/setup-java@v5
|
||||||
with:
|
with:
|
||||||
distribution: 'zulu'
|
distribution: 'zulu'
|
||||||
java-version: 21
|
java-version: 21
|
||||||
|
|
||||||
- name: Cache gradle
|
- name: Cache gradle
|
||||||
uses: actions/cache@v4
|
uses: actions/cache@v5
|
||||||
with:
|
with:
|
||||||
path: ~/.gradle/caches
|
path: |
|
||||||
key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle*') }}
|
~/.gradle/caches
|
||||||
|
~/.gradle/wrapper
|
||||||
|
key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle*', '**/gradle-wrapper.properties') }}
|
||||||
restore-keys: |
|
restore-keys: |
|
||||||
${{ runner.os }}-gradle-
|
${{ runner.os }}-gradle-
|
||||||
|
|
||||||
@@ -38,7 +57,6 @@ jobs:
|
|||||||
keyStorePassword: ${{ secrets.KEY_STORE_PASSWORD }}
|
keyStorePassword: ${{ secrets.KEY_STORE_PASSWORD }}
|
||||||
keyPassword: ${{ secrets.KEY_PASSWORD }}
|
keyPassword: ${{ secrets.KEY_PASSWORD }}
|
||||||
env:
|
env:
|
||||||
# override default build-tools version (29.0.3) -- optional
|
|
||||||
BUILD_TOOLS_VERSION: "36.0.0"
|
BUILD_TOOLS_VERSION: "36.0.0"
|
||||||
|
|
||||||
- name: Sign AAB (F-Droid)
|
- name: Sign AAB (F-Droid)
|
||||||
@@ -50,7 +68,6 @@ jobs:
|
|||||||
keyStorePassword: ${{ secrets.KEY_STORE_PASSWORD }}
|
keyStorePassword: ${{ secrets.KEY_STORE_PASSWORD }}
|
||||||
keyPassword: ${{ secrets.KEY_PASSWORD }}
|
keyPassword: ${{ secrets.KEY_PASSWORD }}
|
||||||
env:
|
env:
|
||||||
# override default build-tools version (29.0.3) -- optional
|
|
||||||
BUILD_TOOLS_VERSION: "36.0.0"
|
BUILD_TOOLS_VERSION: "36.0.0"
|
||||||
|
|
||||||
- name: Build APK
|
- name: Build APK
|
||||||
@@ -65,7 +82,6 @@ jobs:
|
|||||||
keyStorePassword: ${{ secrets.KEY_STORE_PASSWORD }}
|
keyStorePassword: ${{ secrets.KEY_STORE_PASSWORD }}
|
||||||
keyPassword: ${{ secrets.KEY_PASSWORD }}
|
keyPassword: ${{ secrets.KEY_PASSWORD }}
|
||||||
env:
|
env:
|
||||||
# override default build-tools version (29.0.3) -- optional
|
|
||||||
BUILD_TOOLS_VERSION: "36.0.0"
|
BUILD_TOOLS_VERSION: "36.0.0"
|
||||||
|
|
||||||
- name: Sign APK (F-Droid)
|
- name: Sign APK (F-Droid)
|
||||||
@@ -77,20 +93,8 @@ jobs:
|
|||||||
keyStorePassword: ${{ secrets.KEY_STORE_PASSWORD }}
|
keyStorePassword: ${{ secrets.KEY_STORE_PASSWORD }}
|
||||||
keyPassword: ${{ secrets.KEY_PASSWORD }}
|
keyPassword: ${{ secrets.KEY_PASSWORD }}
|
||||||
env:
|
env:
|
||||||
# override default build-tools version (29.0.3) -- optional
|
|
||||||
BUILD_TOOLS_VERSION: "36.0.0"
|
BUILD_TOOLS_VERSION: "36.0.0"
|
||||||
|
|
||||||
- name: Create Release
|
|
||||||
id: create_release
|
|
||||||
uses: actions/create-release@v1
|
|
||||||
env:
|
|
||||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
|
||||||
with:
|
|
||||||
tag_name: ${{ github.ref }}
|
|
||||||
release_name: Release ${{ github.ref }}
|
|
||||||
draft: false
|
|
||||||
prerelease: true
|
|
||||||
|
|
||||||
# Google Play APK
|
# Google Play APK
|
||||||
- name: Upload Play APK Universal Asset
|
- name: Upload Play APK Universal Asset
|
||||||
id: upload-release-asset-play-universal-apk
|
id: upload-release-asset-play-universal-apk
|
||||||
@@ -98,7 +102,7 @@ jobs:
|
|||||||
env:
|
env:
|
||||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
with:
|
with:
|
||||||
upload_url: ${{ steps.create_release.outputs.upload_url }}
|
upload_url: ${{ needs.create-release.outputs.upload_url }}
|
||||||
asset_path: amethyst/build/outputs/apk/play/release/amethyst-play-universal-release-unsigned-signed.apk
|
asset_path: amethyst/build/outputs/apk/play/release/amethyst-play-universal-release-unsigned-signed.apk
|
||||||
asset_name: amethyst-googleplay-universal-${{ github.ref_name }}.apk
|
asset_name: amethyst-googleplay-universal-${{ github.ref_name }}.apk
|
||||||
asset_content_type: application/zip
|
asset_content_type: application/zip
|
||||||
@@ -109,7 +113,7 @@ jobs:
|
|||||||
env:
|
env:
|
||||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
with:
|
with:
|
||||||
upload_url: ${{ steps.create_release.outputs.upload_url }}
|
upload_url: ${{ needs.create-release.outputs.upload_url }}
|
||||||
asset_path: amethyst/build/outputs/apk/play/release/amethyst-play-x86-release-unsigned-signed.apk
|
asset_path: amethyst/build/outputs/apk/play/release/amethyst-play-x86-release-unsigned-signed.apk
|
||||||
asset_name: amethyst-googleplay-x86-${{ github.ref_name }}.apk
|
asset_name: amethyst-googleplay-x86-${{ github.ref_name }}.apk
|
||||||
asset_content_type: application/zip
|
asset_content_type: application/zip
|
||||||
@@ -120,7 +124,7 @@ jobs:
|
|||||||
env:
|
env:
|
||||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
with:
|
with:
|
||||||
upload_url: ${{ steps.create_release.outputs.upload_url }}
|
upload_url: ${{ needs.create-release.outputs.upload_url }}
|
||||||
asset_path: amethyst/build/outputs/apk/play/release/amethyst-play-x86_64-release-unsigned-signed.apk
|
asset_path: amethyst/build/outputs/apk/play/release/amethyst-play-x86_64-release-unsigned-signed.apk
|
||||||
asset_name: amethyst-googleplay-x86_64-${{ github.ref_name }}.apk
|
asset_name: amethyst-googleplay-x86_64-${{ github.ref_name }}.apk
|
||||||
asset_content_type: application/zip
|
asset_content_type: application/zip
|
||||||
@@ -131,7 +135,7 @@ jobs:
|
|||||||
env:
|
env:
|
||||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
with:
|
with:
|
||||||
upload_url: ${{ steps.create_release.outputs.upload_url }}
|
upload_url: ${{ needs.create-release.outputs.upload_url }}
|
||||||
asset_path: amethyst/build/outputs/apk/play/release/amethyst-play-arm64-v8a-release-unsigned-signed.apk
|
asset_path: amethyst/build/outputs/apk/play/release/amethyst-play-arm64-v8a-release-unsigned-signed.apk
|
||||||
asset_name: amethyst-googleplay-arm64-v8a-${{ github.ref_name }}.apk
|
asset_name: amethyst-googleplay-arm64-v8a-${{ github.ref_name }}.apk
|
||||||
asset_content_type: application/zip
|
asset_content_type: application/zip
|
||||||
@@ -142,7 +146,7 @@ jobs:
|
|||||||
env:
|
env:
|
||||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
with:
|
with:
|
||||||
upload_url: ${{ steps.create_release.outputs.upload_url }}
|
upload_url: ${{ needs.create-release.outputs.upload_url }}
|
||||||
asset_path: amethyst/build/outputs/apk/play/release/amethyst-play-armeabi-v7a-release-unsigned-signed.apk
|
asset_path: amethyst/build/outputs/apk/play/release/amethyst-play-armeabi-v7a-release-unsigned-signed.apk
|
||||||
asset_name: amethyst-googleplay-armeabi-v7a-${{ github.ref_name }}.apk
|
asset_name: amethyst-googleplay-armeabi-v7a-${{ github.ref_name }}.apk
|
||||||
asset_content_type: application/zip
|
asset_content_type: application/zip
|
||||||
@@ -154,7 +158,7 @@ jobs:
|
|||||||
env:
|
env:
|
||||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
with:
|
with:
|
||||||
upload_url: ${{ steps.create_release.outputs.upload_url }}
|
upload_url: ${{ needs.create-release.outputs.upload_url }}
|
||||||
asset_path: amethyst/build/outputs/apk/fdroid/release/amethyst-fdroid-universal-release-unsigned-signed.apk
|
asset_path: amethyst/build/outputs/apk/fdroid/release/amethyst-fdroid-universal-release-unsigned-signed.apk
|
||||||
asset_name: amethyst-fdroid-universal-${{ github.ref_name }}.apk
|
asset_name: amethyst-fdroid-universal-${{ github.ref_name }}.apk
|
||||||
asset_content_type: application/zip
|
asset_content_type: application/zip
|
||||||
@@ -165,7 +169,7 @@ jobs:
|
|||||||
env:
|
env:
|
||||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
with:
|
with:
|
||||||
upload_url: ${{ steps.create_release.outputs.upload_url }}
|
upload_url: ${{ needs.create-release.outputs.upload_url }}
|
||||||
asset_path: amethyst/build/outputs/apk/fdroid/release/amethyst-fdroid-x86-release-unsigned-signed.apk
|
asset_path: amethyst/build/outputs/apk/fdroid/release/amethyst-fdroid-x86-release-unsigned-signed.apk
|
||||||
asset_name: amethyst-fdroid-x86-${{ github.ref_name }}.apk
|
asset_name: amethyst-fdroid-x86-${{ github.ref_name }}.apk
|
||||||
asset_content_type: application/zip
|
asset_content_type: application/zip
|
||||||
@@ -176,7 +180,7 @@ jobs:
|
|||||||
env:
|
env:
|
||||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
with:
|
with:
|
||||||
upload_url: ${{ steps.create_release.outputs.upload_url }}
|
upload_url: ${{ needs.create-release.outputs.upload_url }}
|
||||||
asset_path: amethyst/build/outputs/apk/fdroid/release/amethyst-fdroid-x86_64-release-unsigned-signed.apk
|
asset_path: amethyst/build/outputs/apk/fdroid/release/amethyst-fdroid-x86_64-release-unsigned-signed.apk
|
||||||
asset_name: amethyst-fdroid-x86_64-${{ github.ref_name }}.apk
|
asset_name: amethyst-fdroid-x86_64-${{ github.ref_name }}.apk
|
||||||
asset_content_type: application/zip
|
asset_content_type: application/zip
|
||||||
@@ -187,7 +191,7 @@ jobs:
|
|||||||
env:
|
env:
|
||||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
with:
|
with:
|
||||||
upload_url: ${{ steps.create_release.outputs.upload_url }}
|
upload_url: ${{ needs.create-release.outputs.upload_url }}
|
||||||
asset_path: amethyst/build/outputs/apk/fdroid/release/amethyst-fdroid-arm64-v8a-release-unsigned-signed.apk
|
asset_path: amethyst/build/outputs/apk/fdroid/release/amethyst-fdroid-arm64-v8a-release-unsigned-signed.apk
|
||||||
asset_name: amethyst-fdroid-arm64-v8a-${{ github.ref_name }}.apk
|
asset_name: amethyst-fdroid-arm64-v8a-${{ github.ref_name }}.apk
|
||||||
asset_content_type: application/zip
|
asset_content_type: application/zip
|
||||||
@@ -198,13 +202,11 @@ jobs:
|
|||||||
env:
|
env:
|
||||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
with:
|
with:
|
||||||
upload_url: ${{ steps.create_release.outputs.upload_url }}
|
upload_url: ${{ needs.create-release.outputs.upload_url }}
|
||||||
asset_path: amethyst/build/outputs/apk/fdroid/release/amethyst-fdroid-armeabi-v7a-release-unsigned-signed.apk
|
asset_path: amethyst/build/outputs/apk/fdroid/release/amethyst-fdroid-armeabi-v7a-release-unsigned-signed.apk
|
||||||
asset_name: amethyst-fdroid-armeabi-v7a-${{ github.ref_name }}.apk
|
asset_name: amethyst-fdroid-armeabi-v7a-${{ github.ref_name }}.apk
|
||||||
asset_content_type: application/zip
|
asset_content_type: application/zip
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
# Google Play AAB
|
# Google Play AAB
|
||||||
- name: Upload Google Play AAB Asset
|
- name: Upload Google Play AAB Asset
|
||||||
id: upload-release-asset-play-aab
|
id: upload-release-asset-play-aab
|
||||||
@@ -212,7 +214,7 @@ jobs:
|
|||||||
env:
|
env:
|
||||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
with:
|
with:
|
||||||
upload_url: ${{ steps.create_release.outputs.upload_url }}
|
upload_url: ${{ needs.create-release.outputs.upload_url }}
|
||||||
asset_path: amethyst/build/outputs/bundle/playRelease/amethyst-play-release.aab
|
asset_path: amethyst/build/outputs/bundle/playRelease/amethyst-play-release.aab
|
||||||
asset_name: amethyst-googleplay-${{ github.ref_name }}.aab
|
asset_name: amethyst-googleplay-${{ github.ref_name }}.aab
|
||||||
asset_content_type: application/zip
|
asset_content_type: application/zip
|
||||||
@@ -224,7 +226,7 @@ jobs:
|
|||||||
env:
|
env:
|
||||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
with:
|
with:
|
||||||
upload_url: ${{ steps.create_release.outputs.upload_url }}
|
upload_url: ${{ needs.create-release.outputs.upload_url }}
|
||||||
asset_path: amethyst/build/outputs/bundle/fdroidRelease/amethyst-fdroid-release.aab
|
asset_path: amethyst/build/outputs/bundle/fdroidRelease/amethyst-fdroid-release.aab
|
||||||
asset_name: amethyst-fdroid-${{ github.ref_name }}.aab
|
asset_name: amethyst-fdroid-${{ github.ref_name }}.aab
|
||||||
asset_content_type: application/zip
|
asset_content_type: application/zip
|
||||||
@@ -236,3 +238,65 @@ jobs:
|
|||||||
ORG_GRADLE_PROJECT_mavenCentralPassword: ${{ secrets.SONATYPE_PASSWORD }}
|
ORG_GRADLE_PROJECT_mavenCentralPassword: ${{ secrets.SONATYPE_PASSWORD }}
|
||||||
ORG_GRADLE_PROJECT_signingInMemoryKey: ${{ secrets.SIGNING_PRIVATE_KEY }}
|
ORG_GRADLE_PROJECT_signingInMemoryKey: ${{ secrets.SIGNING_PRIVATE_KEY }}
|
||||||
ORG_GRADLE_PROJECT_signingInMemoryKeyPassword: ${{ secrets.SIGNING_PASSWORD }}
|
ORG_GRADLE_PROJECT_signingInMemoryKeyPassword: ${{ secrets.SIGNING_PASSWORD }}
|
||||||
|
|
||||||
|
deploy-desktop:
|
||||||
|
needs: create-release
|
||||||
|
strategy:
|
||||||
|
fail-fast: false
|
||||||
|
matrix:
|
||||||
|
include:
|
||||||
|
- os: ubuntu-latest
|
||||||
|
task: packageDeb
|
||||||
|
format: deb
|
||||||
|
platform: linux
|
||||||
|
- os: macos-latest
|
||||||
|
task: packageDmg
|
||||||
|
format: dmg
|
||||||
|
platform: macos
|
||||||
|
- os: windows-latest
|
||||||
|
task: packageMsi
|
||||||
|
format: msi
|
||||||
|
platform: windows
|
||||||
|
runs-on: ${{ matrix.os }}
|
||||||
|
defaults:
|
||||||
|
run:
|
||||||
|
shell: bash
|
||||||
|
steps:
|
||||||
|
- name: Checkout code
|
||||||
|
uses: actions/checkout@v6
|
||||||
|
|
||||||
|
- name: Set up JDK 21
|
||||||
|
uses: actions/setup-java@v5
|
||||||
|
with:
|
||||||
|
distribution: 'zulu'
|
||||||
|
java-version: 21
|
||||||
|
|
||||||
|
- name: Cache gradle
|
||||||
|
uses: actions/cache@v5
|
||||||
|
with:
|
||||||
|
path: |
|
||||||
|
~/.gradle/caches
|
||||||
|
~/.gradle/wrapper
|
||||||
|
key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle*', '**/gradle-wrapper.properties') }}
|
||||||
|
restore-keys: |
|
||||||
|
${{ runner.os }}-gradle-
|
||||||
|
|
||||||
|
- name: Build Desktop Distribution
|
||||||
|
run: ./gradlew :desktopApp:${{ matrix.task }}
|
||||||
|
|
||||||
|
- name: Find distribution file
|
||||||
|
id: find-dist
|
||||||
|
run: |
|
||||||
|
DIST_FILE=$(find desktopApp/build/compose/binaries/main/${{ matrix.format }} -type f \( -name "*.deb" -o -name "*.dmg" -o -name "*.msi" \) | head -1)
|
||||||
|
echo "path=$DIST_FILE" >> $GITHUB_OUTPUT
|
||||||
|
echo "name=$(basename $DIST_FILE)" >> $GITHUB_OUTPUT
|
||||||
|
|
||||||
|
- name: Upload Desktop Distribution to Release
|
||||||
|
uses: actions/upload-release-asset@v1
|
||||||
|
env:
|
||||||
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
with:
|
||||||
|
upload_url: ${{ needs.create-release.outputs.upload_url }}
|
||||||
|
asset_path: ${{ steps.find-dist.outputs.path }}
|
||||||
|
asset_name: amethyst-desktop-${{ matrix.platform }}-${{ github.ref_name }}.${{ matrix.format }}
|
||||||
|
asset_content_type: application/octet-stream
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ jobs:
|
|||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout code
|
- name: Checkout code
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v6
|
||||||
|
|
||||||
- name: crowdin action
|
- name: crowdin action
|
||||||
uses: crowdin/github-action@v2
|
uses: crowdin/github-action@v2
|
||||||
|
|||||||
@@ -22,6 +22,7 @@
|
|||||||
/.idea/AndroidProjectSystem.xml
|
/.idea/AndroidProjectSystem.xml
|
||||||
/.idea/deviceManager.xml
|
/.idea/deviceManager.xml
|
||||||
/.idea/inspectionProfiles/
|
/.idea/inspectionProfiles/
|
||||||
|
/.idea/migrations.xml
|
||||||
/commons/.idea/gradle.xml
|
/commons/.idea/gradle.xml
|
||||||
/commons/.idea/misc.xml
|
/commons/.idea/misc.xml
|
||||||
/commons/.idea/workspace.xml
|
/commons/.idea/workspace.xml
|
||||||
@@ -149,3 +150,6 @@ lint/tmp/
|
|||||||
|
|
||||||
# Local task tracking
|
# Local task tracking
|
||||||
TASKS.md
|
TASKS.md
|
||||||
|
|
||||||
|
# Claude Code local settings
|
||||||
|
.claude/settings.local.json
|
||||||
|
|||||||
+3
-1
@@ -15,7 +15,7 @@ Redesigns Media Player
|
|||||||
- Turn video controller creation into a flow to fix playback lifecycle issues
|
- Turn video controller creation into a flow to fix playback lifecycle issues
|
||||||
- Adds support for uploading audio
|
- Adds support for uploading audio
|
||||||
|
|
||||||
Adds support for NIP events (kind 30817)
|
Adds support for NIP-47 Wallets
|
||||||
|
|
||||||
Adds support for NIP-52 Calendar appointments
|
Adds support for NIP-52 Calendar appointments
|
||||||
|
|
||||||
@@ -23,6 +23,8 @@ Adds support for NIP-39 External Identities with kind 10011
|
|||||||
|
|
||||||
Adds support for NIP-C0 Code Snippets
|
Adds support for NIP-C0 Code Snippets
|
||||||
|
|
||||||
|
Adds support for NIPs on Nostr (event kind 30817)
|
||||||
|
|
||||||
Adds support for NIP-A3 Payment targets (PayTo: 10133) by @npub1w4uswmv6lu9yel005l3qgheysmr7tk9uvwluddznju3nuxalevvs2d0jr5
|
Adds support for NIP-A3 Payment targets (PayTo: 10133) by @npub1w4uswmv6lu9yel005l3qgheysmr7tk9uvwluddznju3nuxalevvs2d0jr5
|
||||||
|
|
||||||
Adds support for BUD-10 "Blossom:" URIs in images, audios, videos, and documents.
|
Adds support for BUD-10 "Blossom:" URIs in images, audios, videos, and documents.
|
||||||
|
|||||||
@@ -218,6 +218,13 @@ Build and run the Desktop app (requires Java 21+):
|
|||||||
```bash
|
```bash
|
||||||
./gradlew :desktopApp:run
|
./gradlew :desktopApp:run
|
||||||
```
|
```
|
||||||
|
Full build (including tests)
|
||||||
|
```bash
|
||||||
|
./gradlew build
|
||||||
|
```
|
||||||
|
Requirements:
|
||||||
|
- Xcode and iOS simulator
|
||||||
|
- libsodium installed (e.g. via brew: `brew install libsodium`
|
||||||
|
|
||||||
## Testing
|
## Testing
|
||||||
```bash
|
```bash
|
||||||
|
|||||||
+11
-2
@@ -36,6 +36,17 @@ def generateVersionName(String baseVersion) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Workaround: stability.analyzer plugin doesn't declare task dependencies properly for Gradle 9.x
|
||||||
|
afterEvaluate {
|
||||||
|
def stabilityNames = tasks.names.findAll { it.contains("StabilityCheck") }
|
||||||
|
def compileNames = tasks.names.findAll { it.matches("compile.*UnitTestKotlin") }
|
||||||
|
stabilityNames.each { scName ->
|
||||||
|
compileNames.each { ctName ->
|
||||||
|
tasks.named(scName).configure { mustRunAfter(tasks.named(ctName)) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
android {
|
android {
|
||||||
namespace = 'com.vitorpamplona.amethyst'
|
namespace = 'com.vitorpamplona.amethyst'
|
||||||
compileSdk = libs.versions.android.compileSdk.get().toInteger()
|
compileSdk = libs.versions.android.compileSdk.get().toInteger()
|
||||||
@@ -336,9 +347,7 @@ dependencies {
|
|||||||
fdroidImplementation libs.unifiedpush
|
fdroidImplementation libs.unifiedpush
|
||||||
|
|
||||||
// Charts
|
// Charts
|
||||||
implementation libs.vico.charts.core
|
|
||||||
implementation libs.vico.charts.compose
|
implementation libs.vico.charts.compose
|
||||||
implementation libs.vico.charts.views
|
|
||||||
implementation libs.vico.charts.m3
|
implementation libs.vico.charts.m3
|
||||||
|
|
||||||
// GeoHash
|
// GeoHash
|
||||||
|
|||||||
@@ -171,6 +171,7 @@ import com.vitorpamplona.quartz.nip37Drafts.DraftEventCache
|
|||||||
import com.vitorpamplona.quartz.nip37Drafts.DraftWrapEvent
|
import com.vitorpamplona.quartz.nip37Drafts.DraftWrapEvent
|
||||||
import com.vitorpamplona.quartz.nip42RelayAuth.RelayAuthEvent
|
import com.vitorpamplona.quartz.nip42RelayAuth.RelayAuthEvent
|
||||||
import com.vitorpamplona.quartz.nip47WalletConnect.Nip47WalletConnect
|
import com.vitorpamplona.quartz.nip47WalletConnect.Nip47WalletConnect
|
||||||
|
import com.vitorpamplona.quartz.nip47WalletConnect.Request
|
||||||
import com.vitorpamplona.quartz.nip47WalletConnect.Response
|
import com.vitorpamplona.quartz.nip47WalletConnect.Response
|
||||||
import com.vitorpamplona.quartz.nip56Reports.ReportType
|
import com.vitorpamplona.quartz.nip56Reports.ReportType
|
||||||
import com.vitorpamplona.quartz.nip57Zaps.LnZapEvent
|
import com.vitorpamplona.quartz.nip57Zaps.LnZapEvent
|
||||||
@@ -225,7 +226,6 @@ import kotlinx.coroutines.flow.flowOn
|
|||||||
import kotlinx.coroutines.flow.stateIn
|
import kotlinx.coroutines.flow.stateIn
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import java.math.BigDecimal
|
import java.math.BigDecimal
|
||||||
import java.util.Locale
|
|
||||||
import kotlin.coroutines.cancellation.CancellationException
|
import kotlin.coroutines.cancellation.CancellationException
|
||||||
|
|
||||||
@OptIn(DelicateCoroutinesApi::class)
|
@OptIn(DelicateCoroutinesApi::class)
|
||||||
@@ -507,7 +507,7 @@ class Account(
|
|||||||
sendNewAppSpecificData()
|
sendNewAppSpecificData()
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun updateTranslateTo(languageCode: Locale) {
|
suspend fun updateTranslateTo(languageCode: String) {
|
||||||
if (settings.updateTranslateTo(languageCode)) {
|
if (settings.updateTranslateTo(languageCode)) {
|
||||||
sendNewAppSpecificData()
|
sendNewAppSpecificData()
|
||||||
}
|
}
|
||||||
@@ -591,6 +591,14 @@ class Account(
|
|||||||
|
|
||||||
suspend fun calculateZappedAmount(zappedNote: Note): BigDecimal = zappedNote.zappedAmountWithNWCPayments(nip47SignerState)
|
suspend fun calculateZappedAmount(zappedNote: Note): BigDecimal = zappedNote.zappedAmountWithNWCPayments(nip47SignerState)
|
||||||
|
|
||||||
|
suspend fun sendNwcRequest(
|
||||||
|
request: Request,
|
||||||
|
onResponse: (Response?) -> Unit,
|
||||||
|
) {
|
||||||
|
val (event, relay) = nip47SignerState.sendNwcRequest(request, onResponse)
|
||||||
|
client.send(event, setOf(relay))
|
||||||
|
}
|
||||||
|
|
||||||
suspend fun sendZapPaymentRequestFor(
|
suspend fun sendZapPaymentRequestFor(
|
||||||
bolt11: String,
|
bolt11: String,
|
||||||
zappedNote: Note?,
|
zappedNote: Note?,
|
||||||
@@ -2008,6 +2016,7 @@ class Account(
|
|||||||
}
|
}
|
||||||
|
|
||||||
scope.launch(Dispatchers.IO) {
|
scope.launch(Dispatchers.IO) {
|
||||||
|
@OptIn(kotlinx.coroutines.FlowPreview::class)
|
||||||
settings.saveable.debounce(1000).collect {
|
settings.saveable.debounce(1000).collect {
|
||||||
if (it.accountSettings != null) {
|
if (it.accountSettings != null) {
|
||||||
LocalPreferences.saveToEncryptedStorage(it.accountSettings)
|
LocalPreferences.saveToEncryptedStorage(it.accountSettings)
|
||||||
|
|||||||
@@ -63,7 +63,6 @@ import kotlinx.coroutines.flow.StateFlow
|
|||||||
import kotlinx.coroutines.flow.map
|
import kotlinx.coroutines.flow.map
|
||||||
import kotlinx.coroutines.flow.update
|
import kotlinx.coroutines.flow.update
|
||||||
import kotlinx.serialization.Serializable
|
import kotlinx.serialization.Serializable
|
||||||
import java.util.Locale
|
|
||||||
|
|
||||||
val DefaultChannels =
|
val DefaultChannels =
|
||||||
listOf(
|
listOf(
|
||||||
@@ -340,11 +339,11 @@ class AccountSettings(
|
|||||||
saveAccountSettings()
|
saveAccountSettings()
|
||||||
}
|
}
|
||||||
|
|
||||||
fun translateToContains(languageCode: Locale) =
|
fun translateToContains(languageCode: String) =
|
||||||
syncedSettings.languages.translateTo.value
|
syncedSettings.languages.translateTo.value
|
||||||
.contains(languageCode.language)
|
.contains(languageCode)
|
||||||
|
|
||||||
fun updateTranslateTo(languageCode: Locale): Boolean {
|
fun updateTranslateTo(languageCode: String): Boolean {
|
||||||
if (syncedSettings.languages.updateTranslateTo(languageCode)) {
|
if (syncedSettings.languages.updateTranslateTo(languageCode)) {
|
||||||
saveAccountSettings()
|
saveAccountSettings()
|
||||||
return true
|
return true
|
||||||
|
|||||||
@@ -27,7 +27,6 @@ import kotlinx.collections.immutable.ImmutableList
|
|||||||
import kotlinx.collections.immutable.toImmutableList
|
import kotlinx.collections.immutable.toImmutableList
|
||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
import kotlinx.coroutines.flow.update
|
import kotlinx.coroutines.flow.update
|
||||||
import java.util.Locale
|
|
||||||
|
|
||||||
@Stable
|
@Stable
|
||||||
class AccountSyncedSettings(
|
class AccountSyncedSettings(
|
||||||
@@ -165,11 +164,11 @@ class AccountLanguagePreferences(
|
|||||||
dontTranslateFrom.update { it - languageCode }
|
dontTranslateFrom.update { it - languageCode }
|
||||||
}
|
}
|
||||||
|
|
||||||
fun translateToContains(languageCode: Locale) = translateTo.value.contains(languageCode.language)
|
fun translateToContains(languageCode: String) = translateTo.value.contains(languageCode)
|
||||||
|
|
||||||
fun updateTranslateTo(languageCode: Locale): Boolean {
|
fun updateTranslateTo(languageCode: String): Boolean {
|
||||||
if (translateTo.value != languageCode.language) {
|
if (translateTo.value != languageCode) {
|
||||||
translateTo.tryEmit(languageCode.language)
|
translateTo.tryEmit(languageCode)
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
return false
|
return false
|
||||||
|
|||||||
@@ -358,19 +358,19 @@ object LocalCache : ILocalCache, ICacheProvider {
|
|||||||
|
|
||||||
fun load(keys: Set<String>): Set<User> = keys.mapNotNullTo(mutableSetOf(), ::checkGetOrCreateUser)
|
fun load(keys: Set<String>): Set<User> = keys.mapNotNullTo(mutableSetOf(), ::checkGetOrCreateUser)
|
||||||
|
|
||||||
override fun getOrCreateUser(key: HexKey): User {
|
override fun getOrCreateUser(pubkey: HexKey): User {
|
||||||
require(isValidHex(key = key)) { "$key is not a valid hex" }
|
require(isValidHex(key = pubkey)) { "$pubkey is not a valid hex" }
|
||||||
|
|
||||||
return users.getOrCreate(key) {
|
return users.getOrCreate(pubkey) {
|
||||||
val nip65RelayListNote = getOrCreateAddressableNoteInternal(AdvertisedRelayListEvent.createAddress(key))
|
val nip65RelayListNote = getOrCreateAddressableNoteInternal(AdvertisedRelayListEvent.createAddress(pubkey))
|
||||||
val dmRelayListNote = getOrCreateAddressableNoteInternal(ChatMessageRelayListEvent.createAddress(key))
|
val dmRelayListNote = getOrCreateAddressableNoteInternal(ChatMessageRelayListEvent.createAddress(pubkey))
|
||||||
User(it, nip65RelayListNote, dmRelayListNote)
|
User(it, nip65RelayListNote, dmRelayListNote)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun getUserIfExists(key: String): User? {
|
override fun getUserIfExists(pubkey: String): User? {
|
||||||
if (key.isEmpty()) return null
|
if (pubkey.isEmpty()) return null
|
||||||
return users.get(key)
|
return users.get(pubkey)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun countUsers(predicate: (String, User) -> Boolean): Int {
|
override fun countUsers(predicate: (String, User) -> Boolean): Int {
|
||||||
@@ -394,7 +394,7 @@ object LocalCache : ILocalCache, ICacheProvider {
|
|||||||
|
|
||||||
fun getAddressableNoteIfExists(address: Address): AddressableNote? = addressables.get(address)
|
fun getAddressableNoteIfExists(address: Address): AddressableNote? = addressables.get(address)
|
||||||
|
|
||||||
override fun getNoteIfExists(key: String): Note? = if (key.length == 64) notes.get(key) else Address.parse(key)?.let { addressables.get(it) }
|
override fun getNoteIfExists(hexKey: String): Note? = if (hexKey.length == 64) notes.get(hexKey) else Address.parse(hexKey)?.let { addressables.get(it) }
|
||||||
|
|
||||||
fun getNoteIfExists(key: ETag): Note? = notes.get(key.eventId)
|
fun getNoteIfExists(key: ETag): Note? = notes.get(key.eventId)
|
||||||
|
|
||||||
@@ -2250,6 +2250,7 @@ object LocalCache : ILocalCache, ICacheProvider {
|
|||||||
|
|
||||||
requestNote?.let { request -> zappedNote?.addZapPayment(request, note) }
|
requestNote?.let { request -> zappedNote?.addZapPayment(request, note) }
|
||||||
|
|
||||||
|
@OptIn(kotlinx.coroutines.DelicateCoroutinesApi::class)
|
||||||
GlobalScope.launch(Dispatchers.IO) {
|
GlobalScope.launch(Dispatchers.IO) {
|
||||||
responseCallback(event)
|
responseCallback(event)
|
||||||
}
|
}
|
||||||
|
|||||||
+40
-1
@@ -114,7 +114,7 @@ class NwcSignerState(
|
|||||||
|
|
||||||
fun hasWalletConnectSetup(): Boolean = nip47Setup.value != null
|
fun hasWalletConnectSetup(): Boolean = nip47Setup.value != null
|
||||||
|
|
||||||
override fun isNIP47Author(pubkey: HexKey?): Boolean = nip47Signer.value.pubKey == pubkey
|
override fun isNIP47Author(pubKey: HexKey?): Boolean = nip47Signer.value.pubKey == pubKey
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Decrypts a NIP-47 payment request using the current signer.
|
* Decrypts a NIP-47 payment request using the current signer.
|
||||||
@@ -138,6 +138,45 @@ class NwcSignerState(
|
|||||||
return zapPaymentResponseDecryptionCache.value.decryptResponse(event)
|
return zapPaymentResponseDecryptionCache.value.decryptResponse(event)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sends a generic NIP-47 request to the connected wallet.
|
||||||
|
* Subscribes to responses and waits up to 60s for a reply.
|
||||||
|
*
|
||||||
|
* @param request the NIP-47 request to send
|
||||||
|
* @param onResponse callback to handle the response from the wallet
|
||||||
|
* @return a pair containing the request event and target relay URL
|
||||||
|
* @throws IllegalArgumentException if no NIP-47 wallet is set up
|
||||||
|
*/
|
||||||
|
suspend fun sendNwcRequest(
|
||||||
|
request: Request,
|
||||||
|
onResponse: (Response?) -> Unit,
|
||||||
|
): Pair<LnZapPaymentRequestEvent, NormalizedRelayUrl> {
|
||||||
|
val walletService = nip47Setup.value ?: throw IllegalArgumentException("No NIP47 setup")
|
||||||
|
|
||||||
|
val event = LnZapPaymentRequestEvent.createRequest(request, walletService.pubKeyHex, nip47Signer.value)
|
||||||
|
|
||||||
|
val filter =
|
||||||
|
NWCPaymentQueryState(
|
||||||
|
fromServiceHex = walletService.pubKeyHex,
|
||||||
|
toUserHex = event.pubKey,
|
||||||
|
replyingToHex = event.id,
|
||||||
|
relay = walletService.relayUri,
|
||||||
|
)
|
||||||
|
|
||||||
|
nwcFilterAssembler.subscribe(filter)
|
||||||
|
|
||||||
|
scope.launch(Dispatchers.IO) {
|
||||||
|
delay(60000)
|
||||||
|
nwcFilterAssembler.unsubscribe(filter)
|
||||||
|
}
|
||||||
|
|
||||||
|
cache.consume(event, null, true, walletService.relayUri) {
|
||||||
|
onResponse(decryptResponse(it))
|
||||||
|
}
|
||||||
|
|
||||||
|
return Pair(event, walletService.relayUri)
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Sends a zap payment request to a connected Lightning wallet.
|
* Sends a zap payment request to a connected Lightning wallet.
|
||||||
* Subscribes to responses and waits up to 60s for a reply.
|
* Subscribes to responses and waits up to 60s for a reply.
|
||||||
|
|||||||
+2
-2
@@ -36,7 +36,7 @@ import com.vitorpamplona.quartz.nip51Lists.bookmarkList.tags.BookmarkIdTag
|
|||||||
import com.vitorpamplona.quartz.nip51Lists.labeledBookmarkList.LabeledBookmarkListEvent
|
import com.vitorpamplona.quartz.nip51Lists.labeledBookmarkList.LabeledBookmarkListEvent
|
||||||
import com.vitorpamplona.quartz.nip51Lists.labeledBookmarkList.description
|
import com.vitorpamplona.quartz.nip51Lists.labeledBookmarkList.description
|
||||||
import com.vitorpamplona.quartz.nip51Lists.labeledBookmarkList.image
|
import com.vitorpamplona.quartz.nip51Lists.labeledBookmarkList.image
|
||||||
import com.vitorpamplona.quartz.nip51Lists.labeledBookmarkList.name
|
import com.vitorpamplona.quartz.nip51Lists.labeledBookmarkList.title
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||||
@@ -181,7 +181,7 @@ class LabeledBookmarkListsState(
|
|||||||
|
|
||||||
val template =
|
val template =
|
||||||
listEvent.update {
|
listEvent.update {
|
||||||
if (listName != null) name(listName)
|
if (listName != null) title(listName)
|
||||||
if (listDescription != null) description(listDescription)
|
if (listDescription != null) description(listDescription)
|
||||||
if (listImage != null) image(listImage)
|
if (listImage != null) image(listImage)
|
||||||
}
|
}
|
||||||
|
|||||||
+2
-2
@@ -38,7 +38,7 @@ import com.vitorpamplona.quartz.nip51Lists.muteList.tags.UserTag
|
|||||||
import com.vitorpamplona.quartz.nip51Lists.peopleList.PeopleListEvent
|
import com.vitorpamplona.quartz.nip51Lists.peopleList.PeopleListEvent
|
||||||
import com.vitorpamplona.quartz.nip51Lists.peopleList.description
|
import com.vitorpamplona.quartz.nip51Lists.peopleList.description
|
||||||
import com.vitorpamplona.quartz.nip51Lists.peopleList.image
|
import com.vitorpamplona.quartz.nip51Lists.peopleList.image
|
||||||
import com.vitorpamplona.quartz.nip51Lists.peopleList.name
|
import com.vitorpamplona.quartz.nip51Lists.peopleList.title
|
||||||
import com.vitorpamplona.quartz.utils.flattenToSet
|
import com.vitorpamplona.quartz.utils.flattenToSet
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
@@ -226,7 +226,7 @@ class PeopleListsState(
|
|||||||
|
|
||||||
val template =
|
val template =
|
||||||
listEvent.update {
|
listEvent.update {
|
||||||
if (listName != null) name(listName)
|
if (listName != null) title(listName)
|
||||||
if (listDescription != null) description(listDescription)
|
if (listDescription != null) description(listDescription)
|
||||||
if (listImage != null) image(listImage)
|
if (listImage != null) image(listImage)
|
||||||
}
|
}
|
||||||
|
|||||||
+1
@@ -79,6 +79,7 @@ class MergedFollowListsState(
|
|||||||
communities = community.mapTo(mutableSetOf()) { it.address.toValue() },
|
communities = community.mapTo(mutableSetOf()) { it.address.toValue() },
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@OptIn(kotlinx.coroutines.FlowPreview::class)
|
||||||
val flow: StateFlow<AllFollows> =
|
val flow: StateFlow<AllFollows> =
|
||||||
combine(
|
combine(
|
||||||
listOf(
|
listOf(
|
||||||
|
|||||||
@@ -20,6 +20,7 @@
|
|||||||
*/
|
*/
|
||||||
package com.vitorpamplona.amethyst.service.cashu.v4
|
package com.vitorpamplona.amethyst.service.cashu.v4
|
||||||
|
|
||||||
|
import kotlinx.serialization.ExperimentalSerializationApi
|
||||||
import kotlinx.serialization.Serializable
|
import kotlinx.serialization.Serializable
|
||||||
import kotlinx.serialization.cbor.ByteString
|
import kotlinx.serialization.cbor.ByteString
|
||||||
|
|
||||||
@@ -34,6 +35,7 @@ class V4Token(
|
|||||||
val t: Array<V4T>?,
|
val t: Array<V4T>?,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@OptIn(ExperimentalSerializationApi::class)
|
||||||
@Serializable
|
@Serializable
|
||||||
class V4T(
|
class V4T(
|
||||||
// identifier
|
// identifier
|
||||||
@@ -42,6 +44,7 @@ class V4T(
|
|||||||
val p: Array<V4Proof>,
|
val p: Array<V4Proof>,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@OptIn(ExperimentalSerializationApi::class)
|
||||||
@Serializable
|
@Serializable
|
||||||
class V4Proof(
|
class V4Proof(
|
||||||
// amount
|
// amount
|
||||||
@@ -57,6 +60,7 @@ class V4Proof(
|
|||||||
val w: String? = null,
|
val w: String? = null,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@OptIn(ExperimentalSerializationApi::class)
|
||||||
@Serializable
|
@Serializable
|
||||||
class V4DleqProof(
|
class V4DleqProof(
|
||||||
@ByteString
|
@ByteString
|
||||||
|
|||||||
+3
-3
@@ -51,14 +51,14 @@ class EventWatcherSubAssembler(
|
|||||||
}
|
}
|
||||||
|
|
||||||
override fun updateFilter(
|
override fun updateFilter(
|
||||||
key: List<EventFinderQueryState>,
|
keys: List<EventFinderQueryState>,
|
||||||
since: SincePerRelayMap?,
|
since: SincePerRelayMap?,
|
||||||
): List<RelayBasedFilter>? {
|
): List<RelayBasedFilter>? {
|
||||||
if (key.isEmpty()) {
|
if (keys.isEmpty()) {
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
lastNotesOnFilter = key.map { it.note }
|
lastNotesOnFilter = keys.map { it.note }
|
||||||
|
|
||||||
return groupByRelayPresence(lastNotesOnFilter, latestEOSEs)
|
return groupByRelayPresence(lastNotesOnFilter, latestEOSEs)
|
||||||
.map { group ->
|
.map { group ->
|
||||||
|
|||||||
@@ -264,6 +264,7 @@ fun EditPostView(
|
|||||||
ImageVideoDescription(
|
ImageVideoDescription(
|
||||||
it,
|
it,
|
||||||
accountViewModel.account.settings.defaultFileServer,
|
accountViewModel.account.settings.defaultFileServer,
|
||||||
|
isUploading = postViewModel.mediaUploadTracker.isUploading,
|
||||||
onAdd = { alt, server, sensitiveContent, mediaQuality, _ ->
|
onAdd = { alt, server, sensitiveContent, mediaQuality, _ ->
|
||||||
postViewModel.upload(alt, sensitiveContent, mediaQuality, false, server, accountViewModel.toastManager::toast, context)
|
postViewModel.upload(alt, sensitiveContent, mediaQuality, false, server, accountViewModel.toastManager::toast, context)
|
||||||
accountViewModel.account.settings.changeDefaultFileServer(server)
|
accountViewModel.account.settings.changeDefaultFileServer(server)
|
||||||
@@ -372,6 +373,7 @@ private fun BottomRowActions(postViewModel: EditPostViewModel) {
|
|||||||
) {
|
) {
|
||||||
SelectFromGallery(
|
SelectFromGallery(
|
||||||
isUploading = postViewModel.isUploadingImage,
|
isUploading = postViewModel.isUploadingImage,
|
||||||
|
enabled = !postViewModel.isUploadingFile,
|
||||||
tint = MaterialTheme.colorScheme.onBackground,
|
tint = MaterialTheme.colorScheme.onBackground,
|
||||||
modifier = Modifier,
|
modifier = Modifier,
|
||||||
) {
|
) {
|
||||||
@@ -379,7 +381,8 @@ private fun BottomRowActions(postViewModel: EditPostViewModel) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
SelectFromFiles(
|
SelectFromFiles(
|
||||||
isUploading = postViewModel.isUploadingImage,
|
isUploading = postViewModel.isUploadingFile,
|
||||||
|
enabled = !postViewModel.isUploadingImage,
|
||||||
tint = MaterialTheme.colorScheme.onBackground,
|
tint = MaterialTheme.colorScheme.onBackground,
|
||||||
modifier = Modifier,
|
modifier = Modifier,
|
||||||
) {
|
) {
|
||||||
|
|||||||
@@ -39,6 +39,7 @@ import com.vitorpamplona.amethyst.service.uploads.MediaCompressor
|
|||||||
import com.vitorpamplona.amethyst.service.uploads.MultiOrchestrator
|
import com.vitorpamplona.amethyst.service.uploads.MultiOrchestrator
|
||||||
import com.vitorpamplona.amethyst.service.uploads.UploadOrchestrator
|
import com.vitorpamplona.amethyst.service.uploads.UploadOrchestrator
|
||||||
import com.vitorpamplona.amethyst.ui.actions.mediaServers.ServerName
|
import com.vitorpamplona.amethyst.ui.actions.mediaServers.ServerName
|
||||||
|
import com.vitorpamplona.amethyst.ui.actions.uploads.MediaUploadTracker
|
||||||
import com.vitorpamplona.amethyst.ui.actions.uploads.SelectedMedia
|
import com.vitorpamplona.amethyst.ui.actions.uploads.SelectedMedia
|
||||||
import com.vitorpamplona.amethyst.ui.actions.uploads.SelectedMediaProcessing
|
import com.vitorpamplona.amethyst.ui.actions.uploads.SelectedMediaProcessing
|
||||||
import com.vitorpamplona.amethyst.ui.note.creators.userSuggestions.UserSuggestionState
|
import com.vitorpamplona.amethyst.ui.note.creators.userSuggestions.UserSuggestionState
|
||||||
@@ -79,7 +80,9 @@ open class EditPostViewModel : ViewModel() {
|
|||||||
|
|
||||||
var message by mutableStateOf(TextFieldValue(""))
|
var message by mutableStateOf(TextFieldValue(""))
|
||||||
var urlPreview by mutableStateOf<String?>(null)
|
var urlPreview by mutableStateOf<String?>(null)
|
||||||
var isUploadingImage by mutableStateOf(false)
|
val mediaUploadTracker = MediaUploadTracker()
|
||||||
|
val isUploadingImage: Boolean get() = mediaUploadTracker.isUploadingImage
|
||||||
|
val isUploadingFile: Boolean get() = mediaUploadTracker.isUploadingFile
|
||||||
|
|
||||||
var userSuggestions: UserSuggestionState? = null
|
var userSuggestions: UserSuggestionState? = null
|
||||||
var userSuggestionsMainMessage: UserSuggestionAnchor? = null
|
var userSuggestionsMainMessage: UserSuggestionAnchor? = null
|
||||||
@@ -180,7 +183,7 @@ open class EditPostViewModel : ViewModel() {
|
|||||||
val myAccount = account
|
val myAccount = account
|
||||||
val myMultiOrchestrator = multiOrchestrator ?: return@launch
|
val myMultiOrchestrator = multiOrchestrator ?: return@launch
|
||||||
|
|
||||||
isUploadingImage = true
|
mediaUploadTracker.startUpload(myMultiOrchestrator.hasNonMedia())
|
||||||
|
|
||||||
val results =
|
val results =
|
||||||
myMultiOrchestrator.upload(
|
myMultiOrchestrator.upload(
|
||||||
@@ -243,7 +246,7 @@ open class EditPostViewModel : ViewModel() {
|
|||||||
onError(stringRes(context, R.string.failed_to_upload_media_no_details), errorMessages.joinToString(".\n"))
|
onError(stringRes(context, R.string.failed_to_upload_media_no_details), errorMessages.joinToString(".\n"))
|
||||||
}
|
}
|
||||||
|
|
||||||
isUploadingImage = false
|
mediaUploadTracker.finishUpload()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -255,7 +258,7 @@ open class EditPostViewModel : ViewModel() {
|
|||||||
|
|
||||||
multiOrchestrator = null
|
multiOrchestrator = null
|
||||||
urlPreview = null
|
urlPreview = null
|
||||||
isUploadingImage = false
|
mediaUploadTracker.finishUpload()
|
||||||
|
|
||||||
wantsInvoice = false
|
wantsInvoice = false
|
||||||
|
|
||||||
@@ -296,7 +299,7 @@ open class EditPostViewModel : ViewModel() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun canPost() = message.text.isNotBlank() && !isUploadingImage && !wantsInvoice && multiOrchestrator == null
|
fun canPost() = message.text.isNotBlank() && !mediaUploadTracker.isUploading && !wantsInvoice && multiOrchestrator == null
|
||||||
|
|
||||||
fun selectImage(uris: ImmutableList<SelectedMedia>) {
|
fun selectImage(uris: ImmutableList<SelectedMedia>) {
|
||||||
multiOrchestrator = MultiOrchestrator(uris)
|
multiOrchestrator = MultiOrchestrator(uris)
|
||||||
|
|||||||
+1
-1
@@ -53,7 +53,7 @@ class BlossomServersViewModel : ViewModel() {
|
|||||||
fun refresh() {
|
fun refresh() {
|
||||||
isModified = false
|
isModified = false
|
||||||
_fileServers.update {
|
_fileServers.update {
|
||||||
val obtainedFileServers = obtainFileServers() ?: emptyList()
|
val obtainedFileServers = obtainFileServers()
|
||||||
obtainedFileServers.mapNotNull { serverUrl ->
|
obtainedFileServers.mapNotNull { serverUrl ->
|
||||||
try {
|
try {
|
||||||
ServerName(
|
ServerName(
|
||||||
|
|||||||
+47
@@ -0,0 +1,47 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (c) 2025 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.actions.uploads
|
||||||
|
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.runtime.mutableStateOf
|
||||||
|
import androidx.compose.runtime.setValue
|
||||||
|
|
||||||
|
class MediaUploadTracker {
|
||||||
|
var isUploadingImage by mutableStateOf(false)
|
||||||
|
private set
|
||||||
|
var isUploadingFile by mutableStateOf(false)
|
||||||
|
private set
|
||||||
|
|
||||||
|
val isUploading: Boolean get() = isUploadingImage || isUploadingFile
|
||||||
|
|
||||||
|
fun startUpload(hasNonMedia: Boolean) {
|
||||||
|
if (hasNonMedia) {
|
||||||
|
isUploadingFile = true
|
||||||
|
} else {
|
||||||
|
isUploadingImage = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun finishUpload() {
|
||||||
|
isUploadingImage = false
|
||||||
|
isUploadingFile = false
|
||||||
|
}
|
||||||
|
}
|
||||||
+4
-2
@@ -48,6 +48,7 @@ import java.util.concurrent.atomic.AtomicBoolean
|
|||||||
@Composable
|
@Composable
|
||||||
fun SelectFromFiles(
|
fun SelectFromFiles(
|
||||||
isUploading: Boolean,
|
isUploading: Boolean,
|
||||||
|
enabled: Boolean = true,
|
||||||
tint: Color,
|
tint: Color,
|
||||||
modifier: Modifier,
|
modifier: Modifier,
|
||||||
onFilesChosen: (ImmutableList<SelectedMedia>) -> Unit,
|
onFilesChosen: (ImmutableList<SelectedMedia>) -> Unit,
|
||||||
@@ -64,19 +65,20 @@ fun SelectFromFiles(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
FileSelectButton(isUploading, tint, modifier) { showFileSelect = true }
|
FileSelectButton(isUploading, enabled, tint, modifier) { showFileSelect = true }
|
||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
private fun FileSelectButton(
|
private fun FileSelectButton(
|
||||||
isUploading: Boolean,
|
isUploading: Boolean,
|
||||||
|
enabled: Boolean,
|
||||||
tint: Color,
|
tint: Color,
|
||||||
modifier: Modifier,
|
modifier: Modifier,
|
||||||
onClick: () -> Unit,
|
onClick: () -> Unit,
|
||||||
) {
|
) {
|
||||||
IconButton(
|
IconButton(
|
||||||
modifier = modifier,
|
modifier = modifier,
|
||||||
enabled = !isUploading,
|
enabled = enabled && !isUploading,
|
||||||
onClick = { onClick() },
|
onClick = { onClick() },
|
||||||
) {
|
) {
|
||||||
if (!isUploading) {
|
if (!isUploading) {
|
||||||
|
|||||||
+5
-3
@@ -69,6 +69,7 @@ class SelectedMedia(
|
|||||||
@Composable
|
@Composable
|
||||||
fun SelectFromGallery(
|
fun SelectFromGallery(
|
||||||
isUploading: Boolean,
|
isUploading: Boolean,
|
||||||
|
enabled: Boolean = true,
|
||||||
tint: Color,
|
tint: Color,
|
||||||
modifier: Modifier,
|
modifier: Modifier,
|
||||||
onImageChosen: (ImmutableList<SelectedMedia>) -> Unit,
|
onImageChosen: (ImmutableList<SelectedMedia>) -> Unit,
|
||||||
@@ -85,7 +86,7 @@ fun SelectFromGallery(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
GallerySelectButton(isUploading, tint, modifier) { showGallerySelect = true }
|
GallerySelectButton(isUploading, enabled, tint, modifier) { showGallerySelect = true }
|
||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
@@ -107,19 +108,20 @@ fun SelectSingleFromGallery(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
GallerySelectButton(isUploading, tint, modifier) { showGallerySelect = true }
|
GallerySelectButton(isUploading, true, tint, modifier) { showGallerySelect = true }
|
||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
private fun GallerySelectButton(
|
private fun GallerySelectButton(
|
||||||
isUploading: Boolean,
|
isUploading: Boolean,
|
||||||
|
enabled: Boolean,
|
||||||
tint: Color,
|
tint: Color,
|
||||||
modifier: Modifier,
|
modifier: Modifier,
|
||||||
onClick: () -> Unit,
|
onClick: () -> Unit,
|
||||||
) {
|
) {
|
||||||
IconButton(
|
IconButton(
|
||||||
modifier = modifier,
|
modifier = modifier,
|
||||||
enabled = !isUploading,
|
enabled = enabled && !isUploading,
|
||||||
onClick = { onClick() },
|
onClick = { onClick() },
|
||||||
) {
|
) {
|
||||||
if (!isUploading) {
|
if (!isUploading) {
|
||||||
|
|||||||
@@ -119,11 +119,12 @@ object ShareHelper {
|
|||||||
bytesRead >= 12 && matchesMagicNumbers(header, 4, MOV_FTYP) -> detectMp4OrMov(header)
|
bytesRead >= 12 && matchesMagicNumbers(header, 4, MOV_FTYP) -> detectMp4OrMov(header)
|
||||||
|
|
||||||
// MP4/MOV alternative: moov, mdat, or free at offset 4
|
// MP4/MOV alternative: moov, mdat, or free at offset 4
|
||||||
bytesRead >= 8 && (
|
bytesRead >= 8 &&
|
||||||
matchesMagicNumbers(header, 4, MOV_MOOV) ||
|
(
|
||||||
matchesMagicNumbers(header, 4, MOV_MDAT) ||
|
matchesMagicNumbers(header, 4, MOV_MOOV) ||
|
||||||
matchesMagicNumbers(header, 4, MOV_FREE)
|
matchesMagicNumbers(header, 4, MOV_MDAT) ||
|
||||||
) -> "mp4"
|
matchesMagicNumbers(header, 4, MOV_FREE)
|
||||||
|
) -> "mp4"
|
||||||
|
|
||||||
else -> defaultExtension
|
else -> defaultExtension
|
||||||
}
|
}
|
||||||
|
|||||||
+108
@@ -0,0 +1,108 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (c) 2025 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.components
|
||||||
|
|
||||||
|
import androidx.compose.foundation.gestures.awaitEachGesture
|
||||||
|
import androidx.compose.foundation.gestures.awaitFirstDown
|
||||||
|
import androidx.compose.foundation.pager.PagerState
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.runtime.mutableFloatStateOf
|
||||||
|
import androidx.compose.runtime.mutableIntStateOf
|
||||||
|
import androidx.compose.runtime.mutableStateOf
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
|
import androidx.compose.runtime.setValue
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.composed
|
||||||
|
import androidx.compose.ui.geometry.Offset
|
||||||
|
import androidx.compose.ui.input.nestedscroll.NestedScrollConnection
|
||||||
|
import androidx.compose.ui.input.nestedscroll.NestedScrollSource
|
||||||
|
import androidx.compose.ui.input.nestedscroll.nestedScroll
|
||||||
|
import androidx.compose.ui.input.pointer.pointerInput
|
||||||
|
import androidx.compose.ui.layout.onSizeChanged
|
||||||
|
|
||||||
|
private const val PAGER_ZONE_FRACTION = 0.5f
|
||||||
|
|
||||||
|
fun Modifier.zonedDrawerSwipe(
|
||||||
|
pagerState: PagerState,
|
||||||
|
openDrawer: () -> Unit,
|
||||||
|
): Modifier =
|
||||||
|
composed {
|
||||||
|
var widthPx by remember { mutableFloatStateOf(1f) }
|
||||||
|
var gestureStartX by remember { mutableFloatStateOf(0f) }
|
||||||
|
var gestureStartPage by remember { mutableIntStateOf(0) }
|
||||||
|
var drawerOpened by remember { mutableStateOf(false) }
|
||||||
|
|
||||||
|
val connection =
|
||||||
|
remember {
|
||||||
|
object : NestedScrollConnection {
|
||||||
|
override fun onPreScroll(
|
||||||
|
available: Offset,
|
||||||
|
source: NestedScrollSource,
|
||||||
|
): Offset {
|
||||||
|
if (source != NestedScrollSource.UserInput) return Offset.Zero
|
||||||
|
if (drawerOpened) return Offset(available.x, 0f)
|
||||||
|
|
||||||
|
// Non-first pages in the drawer zone: intercept before the
|
||||||
|
// pager consumes the delta to page backwards.
|
||||||
|
if (available.x > 0f) {
|
||||||
|
val wasOnFirstPage = gestureStartPage == 0
|
||||||
|
val isInPagerZone = gestureStartX < widthPx * PAGER_ZONE_FRACTION
|
||||||
|
|
||||||
|
if (!wasOnFirstPage && !isInPagerZone) {
|
||||||
|
drawerOpened = true
|
||||||
|
openDrawer()
|
||||||
|
return Offset(available.x, 0f)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return Offset.Zero
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onPostScroll(
|
||||||
|
consumed: Offset,
|
||||||
|
available: Offset,
|
||||||
|
source: NestedScrollSource,
|
||||||
|
): Offset {
|
||||||
|
if (source != NestedScrollSource.UserInput) return Offset.Zero
|
||||||
|
if (drawerOpened) return Offset(available.x, 0f)
|
||||||
|
|
||||||
|
// First page: open drawer only with unconsumed right-swipe
|
||||||
|
// so child LazyRows can scroll first.
|
||||||
|
if (available.x > 0f && gestureStartPage == 0) {
|
||||||
|
drawerOpened = true
|
||||||
|
openDrawer()
|
||||||
|
return Offset(available.x, 0f)
|
||||||
|
}
|
||||||
|
return Offset.Zero
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
this
|
||||||
|
.onSizeChanged { widthPx = it.width.toFloat() }
|
||||||
|
.pointerInput(Unit) {
|
||||||
|
awaitEachGesture {
|
||||||
|
val down = awaitFirstDown(requireUnconsumed = false)
|
||||||
|
gestureStartX = down.position.x
|
||||||
|
gestureStartPage = pagerState.currentPage
|
||||||
|
drawerOpened = false
|
||||||
|
}
|
||||||
|
}.nestedScroll(connection)
|
||||||
|
}
|
||||||
@@ -37,8 +37,10 @@ import androidx.compose.runtime.setValue
|
|||||||
import androidx.compose.ui.platform.LocalContext
|
import androidx.compose.ui.platform.LocalContext
|
||||||
import androidx.core.net.toUri
|
import androidx.core.net.toUri
|
||||||
import androidx.core.util.Consumer
|
import androidx.core.util.Consumer
|
||||||
|
import androidx.navigation.NavDestination.Companion.hasRoute
|
||||||
import androidx.navigation.compose.NavHost
|
import androidx.navigation.compose.NavHost
|
||||||
import androidx.navigation.compose.composable
|
import androidx.navigation.compose.composable
|
||||||
|
import androidx.navigation.compose.currentBackStackEntryAsState
|
||||||
import com.vitorpamplona.amethyst.Amethyst
|
import com.vitorpamplona.amethyst.Amethyst
|
||||||
import com.vitorpamplona.amethyst.R
|
import com.vitorpamplona.amethyst.R
|
||||||
import com.vitorpamplona.amethyst.service.crashreports.DisplayCrashMessages
|
import com.vitorpamplona.amethyst.service.crashreports.DisplayCrashMessages
|
||||||
@@ -120,6 +122,10 @@ import com.vitorpamplona.amethyst.ui.screen.loggedIn.settings.UpdateZapAmountScr
|
|||||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.settings.UserSettingsScreen
|
import com.vitorpamplona.amethyst.ui.screen.loggedIn.settings.UserSettingsScreen
|
||||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.threadview.ThreadScreen
|
import com.vitorpamplona.amethyst.ui.screen.loggedIn.threadview.ThreadScreen
|
||||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.video.VideoScreen
|
import com.vitorpamplona.amethyst.ui.screen.loggedIn.video.VideoScreen
|
||||||
|
import com.vitorpamplona.amethyst.ui.screen.loggedIn.wallet.WalletReceiveScreen
|
||||||
|
import com.vitorpamplona.amethyst.ui.screen.loggedIn.wallet.WalletScreen
|
||||||
|
import com.vitorpamplona.amethyst.ui.screen.loggedIn.wallet.WalletSendScreen
|
||||||
|
import com.vitorpamplona.amethyst.ui.screen.loggedIn.wallet.WalletTransactionsScreen
|
||||||
import com.vitorpamplona.amethyst.ui.screen.loggedOff.AddAccountDialog
|
import com.vitorpamplona.amethyst.ui.screen.loggedOff.AddAccountDialog
|
||||||
import com.vitorpamplona.amethyst.ui.uriToRoute
|
import com.vitorpamplona.amethyst.ui.uriToRoute
|
||||||
import com.vitorpamplona.quartz.experimental.ephemChat.chat.RoomId
|
import com.vitorpamplona.quartz.experimental.ephemChat.chat.RoomId
|
||||||
@@ -138,7 +144,17 @@ fun AppNavigation(
|
|||||||
) {
|
) {
|
||||||
val nav = rememberNav()
|
val nav = rememberNav()
|
||||||
|
|
||||||
AccountSwitcherAndLeftDrawerLayout(accountViewModel, accountSessionManager, nav) {
|
val navBackStackEntry by nav.controller.currentBackStackEntryAsState()
|
||||||
|
val isTabPagerRoute =
|
||||||
|
navBackStackEntry?.destination?.let { dest ->
|
||||||
|
dest.hasRoute<Route.Home>() || dest.hasRoute<Route.Message>()
|
||||||
|
} ?: false
|
||||||
|
val drawerGesturesEnabled =
|
||||||
|
!isTabPagerRoute ||
|
||||||
|
nav.drawerState.isOpen ||
|
||||||
|
nav.drawerState.targetValue != nav.drawerState.currentValue
|
||||||
|
|
||||||
|
AccountSwitcherAndLeftDrawerLayout(accountViewModel, accountSessionManager, nav, drawerGesturesEnabled) {
|
||||||
NavHost(
|
NavHost(
|
||||||
navController = nav.controller,
|
navController = nav.controller,
|
||||||
startDestination = Route.Home,
|
startDestination = Route.Home,
|
||||||
@@ -152,6 +168,11 @@ fun AppNavigation(
|
|||||||
composable<Route.Notification> { NotificationScreen(accountViewModel, nav) }
|
composable<Route.Notification> { NotificationScreen(accountViewModel, nav) }
|
||||||
composable<Route.Chess> { ChessLobbyScreen(accountViewModel, nav) }
|
composable<Route.Chess> { ChessLobbyScreen(accountViewModel, nav) }
|
||||||
|
|
||||||
|
composableFromEnd<Route.Wallet> { WalletScreen(accountViewModel, nav) }
|
||||||
|
composableFromEnd<Route.WalletSend> { WalletSendScreen(accountViewModel, nav) }
|
||||||
|
composableFromEnd<Route.WalletReceive> { WalletReceiveScreen(accountViewModel, nav) }
|
||||||
|
composableFromEnd<Route.WalletTransactions> { WalletTransactionsScreen(accountViewModel, nav) }
|
||||||
|
|
||||||
composableFromEnd<Route.Lists> { ListOfPeopleListsScreen(accountViewModel, nav) }
|
composableFromEnd<Route.Lists> { ListOfPeopleListsScreen(accountViewModel, nav) }
|
||||||
composableFromEndArgs<Route.MyPeopleListView> { PeopleListScreen(it.dTag, accountViewModel, nav) }
|
composableFromEndArgs<Route.MyPeopleListView> { PeopleListScreen(it.dTag, accountViewModel, nav) }
|
||||||
composableFromEndArgs<Route.MyFollowPackView> { FollowPackScreen(it.dTag, accountViewModel, nav) }
|
composableFromEndArgs<Route.MyFollowPackView> { FollowPackScreen(it.dTag, accountViewModel, nav) }
|
||||||
|
|||||||
+9
@@ -46,6 +46,7 @@ import androidx.compose.material.icons.Icons
|
|||||||
import androidx.compose.material.icons.automirrored.filled.Send
|
import androidx.compose.material.icons.automirrored.filled.Send
|
||||||
import androidx.compose.material.icons.filled.AccountCircle
|
import androidx.compose.material.icons.filled.AccountCircle
|
||||||
import androidx.compose.material.icons.filled.Delete
|
import androidx.compose.material.icons.filled.Delete
|
||||||
|
import androidx.compose.material.icons.outlined.AccountBalanceWallet
|
||||||
import androidx.compose.material.icons.outlined.CollectionsBookmark
|
import androidx.compose.material.icons.outlined.CollectionsBookmark
|
||||||
import androidx.compose.material.icons.outlined.Drafts
|
import androidx.compose.material.icons.outlined.Drafts
|
||||||
import androidx.compose.material.icons.outlined.GroupAdd
|
import androidx.compose.material.icons.outlined.GroupAdd
|
||||||
@@ -463,6 +464,14 @@ fun ListContent(
|
|||||||
route = Route.Drafts,
|
route = Route.Drafts,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
NavigationRow(
|
||||||
|
title = R.string.wallet,
|
||||||
|
icon = Icons.Outlined.AccountBalanceWallet,
|
||||||
|
tint = MaterialTheme.colorScheme.onBackground,
|
||||||
|
nav = nav,
|
||||||
|
route = Route.Wallet,
|
||||||
|
)
|
||||||
|
|
||||||
NavigationRow(
|
NavigationRow(
|
||||||
title = R.string.route_chess,
|
title = R.string.route_chess,
|
||||||
icon = R.drawable.ic_chess,
|
icon = R.drawable.ic_chess,
|
||||||
|
|||||||
@@ -43,6 +43,14 @@ sealed class Route {
|
|||||||
|
|
||||||
@Serializable object Chess : Route()
|
@Serializable object Chess : Route()
|
||||||
|
|
||||||
|
@Serializable object Wallet : Route()
|
||||||
|
|
||||||
|
@Serializable object WalletSend : Route()
|
||||||
|
|
||||||
|
@Serializable object WalletReceive : Route()
|
||||||
|
|
||||||
|
@Serializable object WalletTransactions : Route()
|
||||||
|
|
||||||
@Serializable object Search : Route()
|
@Serializable object Search : Route()
|
||||||
|
|
||||||
@Serializable object SecurityFilters : Route()
|
@Serializable object SecurityFilters : Route()
|
||||||
|
|||||||
@@ -62,7 +62,7 @@ fun BadgeCompose(
|
|||||||
nav: INav,
|
nav: INav,
|
||||||
) {
|
) {
|
||||||
val noteState by observeNote(likeSetCard.note, accountViewModel)
|
val noteState by observeNote(likeSetCard.note, accountViewModel)
|
||||||
val note = noteState?.note
|
val note = noteState.note
|
||||||
|
|
||||||
val context = LocalContext.current.applicationContext
|
val context = LocalContext.current.applicationContext
|
||||||
|
|
||||||
|
|||||||
@@ -30,10 +30,10 @@ import androidx.compose.runtime.remember
|
|||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.graphics.Color
|
import androidx.compose.ui.graphics.Color
|
||||||
import androidx.compose.ui.platform.LocalContext
|
import androidx.compose.ui.platform.LocalContext
|
||||||
import androidx.compose.ui.platform.LocalLifecycleOwner
|
|
||||||
import androidx.compose.ui.text.font.FontWeight
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
import androidx.compose.ui.text.style.TextOverflow
|
import androidx.compose.ui.text.style.TextOverflow
|
||||||
import androidx.lifecycle.LifecycleOwner
|
import androidx.lifecycle.LifecycleOwner
|
||||||
|
import androidx.lifecycle.compose.LocalLifecycleOwner
|
||||||
import com.vitorpamplona.amethyst.commons.model.ImmutableListOfLists
|
import com.vitorpamplona.amethyst.commons.model.ImmutableListOfLists
|
||||||
import com.vitorpamplona.amethyst.model.Note
|
import com.vitorpamplona.amethyst.model.Note
|
||||||
import com.vitorpamplona.amethyst.model.User
|
import com.vitorpamplona.amethyst.model.User
|
||||||
|
|||||||
@@ -49,10 +49,10 @@ fun showAmountInteger(amount: BigDecimal?): String {
|
|||||||
if (amount.abs() < BigDecimal(0.01)) return ""
|
if (amount.abs() < BigDecimal(0.01)) return ""
|
||||||
|
|
||||||
return when {
|
return when {
|
||||||
amount >= OneGiga -> dfG.get().format(amount.div(OneGiga).setScale(0, RoundingMode.HALF_UP))
|
amount >= OneGiga -> dfG.get()?.format(amount.div(OneGiga).setScale(0, RoundingMode.HALF_UP)) ?: ""
|
||||||
amount >= OneMega -> dfM.get().format(amount.div(OneMega).setScale(0, RoundingMode.HALF_UP))
|
amount >= OneMega -> dfM.get()?.format(amount.div(OneMega).setScale(0, RoundingMode.HALF_UP)) ?: ""
|
||||||
amount >= TenKilo -> dfK.get().format(amount.div(OneKilo).setScale(0, RoundingMode.HALF_UP))
|
amount >= TenKilo -> dfK.get()?.format(amount.div(OneKilo).setScale(0, RoundingMode.HALF_UP)) ?: ""
|
||||||
else -> dfN.get().format(amount)
|
else -> dfN.get()?.format(amount) ?: ""
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -118,13 +118,13 @@ class PollNoteViewModel : ViewModel() {
|
|||||||
it.zappedValue.value = zappedValue
|
it.zappedValue.value = zappedValue
|
||||||
it.tally.value = tallyValue.toFloat()
|
it.tally.value = tallyValue.toFloat()
|
||||||
it.consensusThreadhold.value = consensusThreshold != null && tallyValue >= consensusThreshold!!
|
it.consensusThreadhold.value = consensusThreshold != null && tallyValue >= consensusThreshold!!
|
||||||
it.zappedByLoggedIn.value = account?.userProfile()?.let { it1 -> cachedIsPollOptionZappedBy(it.option, it1) } ?: false
|
it.zappedByLoggedIn.value = account.userProfile().let { it1 -> cachedIsPollOptionZappedBy(it.option, it1) }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun checkIfCanZap(): Boolean {
|
fun checkIfCanZap(): Boolean {
|
||||||
val account = account ?: return false
|
val account = account
|
||||||
val note = pollNote ?: return false
|
val note = pollNote ?: return false
|
||||||
return account.userProfile() != note.author && !wasZappedByLoggedInAccount
|
return account.userProfile() != note.author && !wasZappedByLoggedInAccount
|
||||||
}
|
}
|
||||||
|
|||||||
+2
@@ -78,6 +78,7 @@ import kotlinx.collections.immutable.toImmutableList
|
|||||||
fun ImageVideoDescription(
|
fun ImageVideoDescription(
|
||||||
uris: MultiOrchestrator,
|
uris: MultiOrchestrator,
|
||||||
defaultServer: ServerName,
|
defaultServer: ServerName,
|
||||||
|
isUploading: Boolean,
|
||||||
onAdd: (String, ServerName, Boolean, Int, Boolean) -> Unit,
|
onAdd: (String, ServerName, Boolean, Int, Boolean) -> Unit,
|
||||||
onDelete: (SelectedMediaProcessing) -> Unit,
|
onDelete: (SelectedMediaProcessing) -> Unit,
|
||||||
onCancel: () -> Unit,
|
onCancel: () -> Unit,
|
||||||
@@ -319,6 +320,7 @@ fun ImageVideoDescription(
|
|||||||
Modifier
|
Modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.padding(vertical = 10.dp),
|
.padding(vertical = 10.dp),
|
||||||
|
enabled = !isUploading,
|
||||||
onClick = { onAdd(message, selectedServer, sensitiveContent, mediaQualitySlider, useH265Codec) },
|
onClick = { onAdd(message, selectedServer, sensitiveContent, mediaQualitySlider, useH265Codec) },
|
||||||
shape = QuoteBorder,
|
shape = QuoteBorder,
|
||||||
colors =
|
colors =
|
||||||
|
|||||||
+8
-5
@@ -45,6 +45,7 @@ import com.vitorpamplona.amethyst.service.uploads.MultiOrchestrator
|
|||||||
import com.vitorpamplona.amethyst.service.uploads.UploadOrchestrator
|
import com.vitorpamplona.amethyst.service.uploads.UploadOrchestrator
|
||||||
import com.vitorpamplona.amethyst.ui.actions.NewMessageTagger
|
import com.vitorpamplona.amethyst.ui.actions.NewMessageTagger
|
||||||
import com.vitorpamplona.amethyst.ui.actions.mediaServers.ServerName
|
import com.vitorpamplona.amethyst.ui.actions.mediaServers.ServerName
|
||||||
|
import com.vitorpamplona.amethyst.ui.actions.uploads.MediaUploadTracker
|
||||||
import com.vitorpamplona.amethyst.ui.actions.uploads.SelectedMedia
|
import com.vitorpamplona.amethyst.ui.actions.uploads.SelectedMedia
|
||||||
import com.vitorpamplona.amethyst.ui.actions.uploads.SelectedMediaProcessing
|
import com.vitorpamplona.amethyst.ui.actions.uploads.SelectedMediaProcessing
|
||||||
import com.vitorpamplona.amethyst.ui.note.creators.draftTags.DraftTagState
|
import com.vitorpamplona.amethyst.ui.note.creators.draftTags.DraftTagState
|
||||||
@@ -149,7 +150,9 @@ open class CommentPostViewModel :
|
|||||||
|
|
||||||
val urlPreviews = PreviewState()
|
val urlPreviews = PreviewState()
|
||||||
|
|
||||||
var isUploadingImage by mutableStateOf(false)
|
val mediaUploadTracker = MediaUploadTracker()
|
||||||
|
val isUploadingImage: Boolean get() = mediaUploadTracker.isUploadingImage
|
||||||
|
val isUploadingFile: Boolean get() = mediaUploadTracker.isUploadingFile
|
||||||
|
|
||||||
var userSuggestions: UserSuggestionState? = null
|
var userSuggestions: UserSuggestionState? = null
|
||||||
var userSuggestionsMainMessage: UserSuggestionAnchor? = null
|
var userSuggestionsMainMessage: UserSuggestionAnchor? = null
|
||||||
@@ -487,7 +490,7 @@ open class CommentPostViewModel :
|
|||||||
viewModelScope.launch(Dispatchers.IO) {
|
viewModelScope.launch(Dispatchers.IO) {
|
||||||
val myMultiOrchestrator = multiOrchestrator ?: return@launch
|
val myMultiOrchestrator = multiOrchestrator ?: return@launch
|
||||||
|
|
||||||
isUploadingImage = true
|
mediaUploadTracker.startUpload(myMultiOrchestrator.hasNonMedia())
|
||||||
|
|
||||||
val results =
|
val results =
|
||||||
myMultiOrchestrator.upload(
|
myMultiOrchestrator.upload(
|
||||||
@@ -551,7 +554,7 @@ open class CommentPostViewModel :
|
|||||||
onError(stringRes(context, R.string.failed_to_upload_media_no_details), errorMessages.joinToString(".\n"))
|
onError(stringRes(context, R.string.failed_to_upload_media_no_details), errorMessages.joinToString(".\n"))
|
||||||
}
|
}
|
||||||
|
|
||||||
isUploadingImage = false
|
mediaUploadTracker.finishUpload()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -564,7 +567,7 @@ open class CommentPostViewModel :
|
|||||||
externalIdentity = null
|
externalIdentity = null
|
||||||
|
|
||||||
multiOrchestrator = null
|
multiOrchestrator = null
|
||||||
isUploadingImage = false
|
mediaUploadTracker.finishUpload()
|
||||||
|
|
||||||
notifying = null
|
notifying = null
|
||||||
|
|
||||||
@@ -676,7 +679,7 @@ open class CommentPostViewModel :
|
|||||||
|
|
||||||
fun canPost(): Boolean =
|
fun canPost(): Boolean =
|
||||||
message.text.isNotBlank() &&
|
message.text.isNotBlank() &&
|
||||||
!isUploadingImage &&
|
!mediaUploadTracker.isUploading &&
|
||||||
!wantsInvoice &&
|
!wantsInvoice &&
|
||||||
(!wantsZapraiser || zapRaiserAmount.value != null) &&
|
(!wantsZapraiser || zapRaiserAmount.value != null) &&
|
||||||
multiOrchestrator == null
|
multiOrchestrator == null
|
||||||
|
|||||||
+4
-1
@@ -300,6 +300,7 @@ private fun GenericCommentPostBody(
|
|||||||
ImageVideoDescription(
|
ImageVideoDescription(
|
||||||
it,
|
it,
|
||||||
accountViewModel.account.settings.defaultFileServer,
|
accountViewModel.account.settings.defaultFileServer,
|
||||||
|
isUploading = postViewModel.mediaUploadTracker.isUploading,
|
||||||
onAdd = { alt, server, sensitiveContent, mediaQuality, _ ->
|
onAdd = { alt, server, sensitiveContent, mediaQuality, _ ->
|
||||||
postViewModel.upload(alt, if (sensitiveContent) "" else null, mediaQuality, server, accountViewModel.toastManager::toast, context)
|
postViewModel.upload(alt, if (sensitiveContent) "" else null, mediaQuality, server, accountViewModel.toastManager::toast, context)
|
||||||
accountViewModel.account.settings.changeDefaultFileServer(server)
|
accountViewModel.account.settings.changeDefaultFileServer(server)
|
||||||
@@ -391,6 +392,7 @@ private fun BottomRowActions(postViewModel: CommentPostViewModel) {
|
|||||||
) {
|
) {
|
||||||
SelectFromGallery(
|
SelectFromGallery(
|
||||||
isUploading = postViewModel.isUploadingImage,
|
isUploading = postViewModel.isUploadingImage,
|
||||||
|
enabled = !postViewModel.isUploadingFile,
|
||||||
tint = MaterialTheme.colorScheme.onBackground,
|
tint = MaterialTheme.colorScheme.onBackground,
|
||||||
modifier = Modifier,
|
modifier = Modifier,
|
||||||
) {
|
) {
|
||||||
@@ -398,7 +400,8 @@ private fun BottomRowActions(postViewModel: CommentPostViewModel) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
SelectFromFiles(
|
SelectFromFiles(
|
||||||
isUploading = postViewModel.isUploadingImage,
|
isUploading = postViewModel.isUploadingFile,
|
||||||
|
enabled = !postViewModel.isUploadingImage,
|
||||||
tint = MaterialTheme.colorScheme.onBackground,
|
tint = MaterialTheme.colorScheme.onBackground,
|
||||||
modifier = Modifier,
|
modifier = Modifier,
|
||||||
) {
|
) {
|
||||||
|
|||||||
@@ -96,7 +96,7 @@ fun RenderLiveChessChallenge(
|
|||||||
nav: INav,
|
nav: INav,
|
||||||
) {
|
) {
|
||||||
val event = (note.event as? LiveChessGameChallengeEvent) ?: return
|
val event = (note.event as? LiveChessGameChallengeEvent) ?: return
|
||||||
val gameId = event.gameId() ?: return
|
val gameId = event.gameId()
|
||||||
|
|
||||||
val chessViewModel: ChessViewModelNew =
|
val chessViewModel: ChessViewModelNew =
|
||||||
viewModel(
|
viewModel(
|
||||||
|
|||||||
@@ -72,9 +72,9 @@ private fun ObserverAndRenderNIP95(
|
|||||||
val content by
|
val content by
|
||||||
remember(noteState) {
|
remember(noteState) {
|
||||||
// Creates a new object when the event arrives to force an update of the image.
|
// Creates a new object when the event arrives to force an update of the image.
|
||||||
val note = noteState?.note
|
val note = noteState.note
|
||||||
val uri = header.toNostrUri()
|
val uri = header.toNostrUri()
|
||||||
val localDir = note?.idHex?.let { File(Amethyst.instance.nip95cache, it) }
|
val localDir = note.idHex.let { File(Amethyst.instance.nip95cache, it) }
|
||||||
val blurHash = eventHeader.blurhash()
|
val blurHash = eventHeader.blurhash()
|
||||||
val dimensions = eventHeader.dimensions()
|
val dimensions = eventHeader.dimensions()
|
||||||
val description = eventHeader.alt() ?: eventHeader.content
|
val description = eventHeader.alt() ?: eventHeader.content
|
||||||
|
|||||||
@@ -163,7 +163,7 @@ fun RenderTextModificationEvent(
|
|||||||
}
|
}
|
||||||
|
|
||||||
LaunchedEffect(key1 = noteState) {
|
LaunchedEffect(key1 = noteState) {
|
||||||
val newAuthor = accountViewModel.isLoggedUser(noteState?.note?.author)
|
val newAuthor = accountViewModel.isLoggedUser(noteState.note.author)
|
||||||
|
|
||||||
if (isAuthorTheLoggedUser.value != newAuthor) {
|
if (isAuthorTheLoggedUser.value != newAuthor) {
|
||||||
isAuthorTheLoggedUser.value = newAuthor
|
isAuthorTheLoggedUser.value = newAuthor
|
||||||
|
|||||||
+2
@@ -49,6 +49,7 @@ fun AccountSwitcherAndLeftDrawerLayout(
|
|||||||
accountViewModel: AccountViewModel,
|
accountViewModel: AccountViewModel,
|
||||||
accountSessionManager: AccountSessionManager,
|
accountSessionManager: AccountSessionManager,
|
||||||
nav: INav,
|
nav: INav,
|
||||||
|
gesturesEnabled: Boolean = true,
|
||||||
content: @Composable () -> Unit,
|
content: @Composable () -> Unit,
|
||||||
) {
|
) {
|
||||||
val scope = rememberCoroutineScope()
|
val scope = rememberCoroutineScope()
|
||||||
@@ -83,6 +84,7 @@ fun AccountSwitcherAndLeftDrawerLayout(
|
|||||||
|
|
||||||
ModalNavigationDrawer(
|
ModalNavigationDrawer(
|
||||||
drawerState = nav.drawerState,
|
drawerState = nav.drawerState,
|
||||||
|
gesturesEnabled = gesturesEnabled,
|
||||||
drawerContent = {
|
drawerContent = {
|
||||||
DrawerContent(nav, openSheetFunction, accountViewModel)
|
DrawerContent(nav, openSheetFunction, accountViewModel)
|
||||||
BackHandler(enabled = nav.drawerState.isOpen, nav::closeDrawer)
|
BackHandler(enabled = nav.drawerState.isOpen, nav::closeDrawer)
|
||||||
|
|||||||
+1
-2
@@ -158,7 +158,6 @@ import kotlinx.coroutines.flow.onStart
|
|||||||
import kotlinx.coroutines.flow.stateIn
|
import kotlinx.coroutines.flow.stateIn
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
import java.util.Locale
|
|
||||||
|
|
||||||
@Stable
|
@Stable
|
||||||
class AccountViewModel(
|
class AccountViewModel(
|
||||||
@@ -1009,7 +1008,7 @@ class AccountViewModel(
|
|||||||
|
|
||||||
fun removeDontTranslateFrom(languageCode: String) = launchSigner { account.removeDontTranslateFrom(languageCode) }
|
fun removeDontTranslateFrom(languageCode: String) = launchSigner { account.removeDontTranslateFrom(languageCode) }
|
||||||
|
|
||||||
fun updateTranslateTo(languageCode: Locale) = launchSigner { account.updateTranslateTo(languageCode) }
|
fun updateTranslateTo(languageCode: String) = launchSigner { account.updateTranslateTo(languageCode) }
|
||||||
|
|
||||||
fun prefer(
|
fun prefer(
|
||||||
source: String,
|
source: String,
|
||||||
|
|||||||
+92
-4
@@ -141,7 +141,8 @@ class ChatNewMessageViewModel :
|
|||||||
|
|
||||||
val urlPreviews = PreviewState()
|
val urlPreviews = PreviewState()
|
||||||
|
|
||||||
var isUploadingImage by mutableStateOf(false)
|
val isUploadingImage: Boolean get() = uploadState?.isUploadingImage ?: false
|
||||||
|
val isUploadingFile: Boolean get() = uploadState?.isUploadingFile ?: false
|
||||||
|
|
||||||
var userSuggestions: UserSuggestionState? = null
|
var userSuggestions: UserSuggestionState? = null
|
||||||
var userSuggestionsMainMessage: UserSuggestionAnchor? = null
|
var userSuggestionsMainMessage: UserSuggestionAnchor? = null
|
||||||
@@ -403,7 +404,16 @@ class ChatNewMessageViewModel :
|
|||||||
|
|
||||||
accountViewModel.launchSigner {
|
accountViewModel.launchSigner {
|
||||||
if (nip17) {
|
if (nip17) {
|
||||||
ChatFileUploader(account).justUploadNIP17(uploadState, onError, context) {
|
ChatFileUploader(account).justUploadNIP17(
|
||||||
|
uploadState,
|
||||||
|
onError,
|
||||||
|
onEncryptedUploadError = { title, message ->
|
||||||
|
encryptedUploadErrorTitle = title
|
||||||
|
encryptedUploadErrorMessage = message
|
||||||
|
pendingRetryMode = RetryMode.HOLD
|
||||||
|
},
|
||||||
|
context,
|
||||||
|
) {
|
||||||
uploadsWaitingToBeSent += it
|
uploadsWaitingToBeSent += it
|
||||||
draftTag.newVersion()
|
draftTag.newVersion()
|
||||||
onceUploaded()
|
onceUploaded()
|
||||||
@@ -428,7 +438,19 @@ class ChatNewMessageViewModel :
|
|||||||
|
|
||||||
accountViewModel.launchSigner {
|
accountViewModel.launchSigner {
|
||||||
if (nip17) {
|
if (nip17) {
|
||||||
ChatFileUploader(account).justUploadNIP17(uploadState, onError, context) {
|
ChatFileUploader(account).justUploadNIP17(
|
||||||
|
uploadState,
|
||||||
|
onError,
|
||||||
|
onEncryptedUploadError = { title, message ->
|
||||||
|
encryptedUploadErrorTitle = title
|
||||||
|
encryptedUploadErrorMessage = message
|
||||||
|
pendingRetryMode = RetryMode.SEND
|
||||||
|
pendingRetryOnError = onError
|
||||||
|
pendingRetryContext = context
|
||||||
|
pendingRetryOnceUploaded = onceUploaded
|
||||||
|
},
|
||||||
|
context,
|
||||||
|
) {
|
||||||
ChatFileSender(room, account).sendNIP17(it)
|
ChatFileSender(room, account).sendNIP17(it)
|
||||||
draftTag.newVersion()
|
draftTag.newVersion()
|
||||||
onceUploaded()
|
onceUploaded()
|
||||||
@@ -443,6 +465,69 @@ class ChatNewMessageViewModel :
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Encrypted upload error state for retry dialog
|
||||||
|
var encryptedUploadErrorTitle by mutableStateOf<String?>(null)
|
||||||
|
var encryptedUploadErrorMessage by mutableStateOf<String?>(null)
|
||||||
|
var pendingRetryMode by mutableStateOf<RetryMode?>(null)
|
||||||
|
var pendingRetryOnError by mutableStateOf<((String, String) -> Unit)?>(null)
|
||||||
|
var pendingRetryContext by mutableStateOf<Context?>(null)
|
||||||
|
var pendingRetryOnceUploaded by mutableStateOf<(() -> Unit)?>(null)
|
||||||
|
|
||||||
|
enum class RetryMode { HOLD, SEND }
|
||||||
|
|
||||||
|
fun dismissEncryptedUploadError() {
|
||||||
|
encryptedUploadErrorTitle = null
|
||||||
|
encryptedUploadErrorMessage = null
|
||||||
|
pendingRetryMode = null
|
||||||
|
pendingRetryOnError = null
|
||||||
|
pendingRetryContext = null
|
||||||
|
pendingRetryOnceUploaded = null
|
||||||
|
}
|
||||||
|
|
||||||
|
fun retryWithoutEncryption() {
|
||||||
|
val mode = pendingRetryMode ?: return
|
||||||
|
val onError = pendingRetryOnError
|
||||||
|
val context = pendingRetryContext
|
||||||
|
val onceUploaded = pendingRetryOnceUploaded
|
||||||
|
val room = room
|
||||||
|
val uploadState = uploadState
|
||||||
|
|
||||||
|
dismissEncryptedUploadError()
|
||||||
|
|
||||||
|
if (uploadState == null || context == null) return
|
||||||
|
|
||||||
|
uploadState.encryptFiles = false
|
||||||
|
|
||||||
|
accountViewModel.launchSigner {
|
||||||
|
when (mode) {
|
||||||
|
RetryMode.HOLD -> {
|
||||||
|
ChatFileUploader(account).justUploadNIP17Unencrypted(
|
||||||
|
uploadState,
|
||||||
|
onError ?: accountViewModel.toastManager::toast,
|
||||||
|
context,
|
||||||
|
) {
|
||||||
|
uploadsWaitingToBeSent += it
|
||||||
|
draftTag.newVersion()
|
||||||
|
onceUploaded?.invoke()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
RetryMode.SEND -> {
|
||||||
|
if (room == null) return@launchSigner
|
||||||
|
ChatFileUploader(account).justUploadNIP17Unencrypted(
|
||||||
|
uploadState,
|
||||||
|
onError ?: accountViewModel.toastManager::toast,
|
||||||
|
context,
|
||||||
|
) {
|
||||||
|
ChatFileSender(room, account).sendNIP17(it)
|
||||||
|
draftTag.newVersion()
|
||||||
|
onceUploaded?.invoke()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private suspend fun innerSendPost(draftTag: String?) {
|
private suspend fun innerSendPost(draftTag: String?) {
|
||||||
val room = room ?: return
|
val room = room ?: return
|
||||||
|
|
||||||
@@ -561,6 +646,9 @@ class ChatNewMessageViewModel :
|
|||||||
userSuggestionsMainMessage = null
|
userSuggestionsMainMessage = null
|
||||||
|
|
||||||
uploadsWaitingToBeSent = emptyList()
|
uploadsWaitingToBeSent = emptyList()
|
||||||
|
uploadState?.reset()
|
||||||
|
|
||||||
|
dismissEncryptedUploadError()
|
||||||
|
|
||||||
iMetaAttachments.reset()
|
iMetaAttachments.reset()
|
||||||
|
|
||||||
@@ -692,7 +780,7 @@ class ChatNewMessageViewModel :
|
|||||||
|
|
||||||
fun canPost(): Boolean =
|
fun canPost(): Boolean =
|
||||||
message.text.isNotBlank() &&
|
message.text.isNotBlank() &&
|
||||||
uploadState?.isUploadingImage != true &&
|
uploadState?.mediaUploadTracker?.isUploading != true &&
|
||||||
!wantsInvoice &&
|
!wantsInvoice &&
|
||||||
(!wantsZapraiser || zapRaiserAmount.value != null) &&
|
(!wantsZapraiser || zapRaiserAmount.value != null) &&
|
||||||
(toUsers.text.isNotBlank()) &&
|
(toUsers.text.isNotBlank()) &&
|
||||||
|
|||||||
+13
-1
@@ -287,6 +287,7 @@ fun GroupDMScreenContent(
|
|||||||
ImageVideoDescription(
|
ImageVideoDescription(
|
||||||
selectedFiles,
|
selectedFiles,
|
||||||
accountViewModel.account.settings.defaultFileServer,
|
accountViewModel.account.settings.defaultFileServer,
|
||||||
|
isUploading = uploading.mediaUploadTracker.isUploading,
|
||||||
onAdd = { alt, server, sensitiveContent, mediaQuality, _ ->
|
onAdd = { alt, server, sensitiveContent, mediaQuality, _ ->
|
||||||
postViewModel.uploadAndHold(
|
postViewModel.uploadAndHold(
|
||||||
accountViewModel.toastManager::toast,
|
accountViewModel.toastManager::toast,
|
||||||
@@ -301,6 +302,15 @@ fun GroupDMScreenContent(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
postViewModel.encryptedUploadErrorTitle?.let { title ->
|
||||||
|
EncryptedUploadErrorDialog(
|
||||||
|
title = title,
|
||||||
|
message = postViewModel.encryptedUploadErrorMessage ?: "",
|
||||||
|
onDismiss = postViewModel::dismissEncryptedUploadError,
|
||||||
|
onRetryWithoutEncryption = postViewModel::retryWithoutEncryption,
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -386,6 +396,7 @@ private fun BottomRowActions(
|
|||||||
if (postViewModel.room != null) {
|
if (postViewModel.room != null) {
|
||||||
SelectFromGallery(
|
SelectFromGallery(
|
||||||
isUploading = postViewModel.isUploadingImage,
|
isUploading = postViewModel.isUploadingImage,
|
||||||
|
enabled = !postViewModel.isUploadingFile,
|
||||||
tint = MaterialTheme.colorScheme.onBackground,
|
tint = MaterialTheme.colorScheme.onBackground,
|
||||||
modifier = Modifier,
|
modifier = Modifier,
|
||||||
) {
|
) {
|
||||||
@@ -393,7 +404,8 @@ private fun BottomRowActions(
|
|||||||
}
|
}
|
||||||
|
|
||||||
SelectFromFiles(
|
SelectFromFiles(
|
||||||
isUploading = postViewModel.isUploadingImage,
|
isUploading = postViewModel.isUploadingFile,
|
||||||
|
enabled = !postViewModel.isUploadingImage,
|
||||||
tint = MaterialTheme.colorScheme.onBackground,
|
tint = MaterialTheme.colorScheme.onBackground,
|
||||||
modifier = Modifier,
|
modifier = Modifier,
|
||||||
) {
|
) {
|
||||||
|
|||||||
+49
-3
@@ -27,8 +27,10 @@ import androidx.compose.foundation.layout.Column
|
|||||||
import androidx.compose.foundation.layout.Row
|
import androidx.compose.foundation.layout.Row
|
||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.material3.AlertDialog
|
||||||
import androidx.compose.material3.MaterialTheme
|
import androidx.compose.material3.MaterialTheme
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
|
import androidx.compose.material3.TextButton
|
||||||
import androidx.compose.material3.TextFieldDefaults
|
import androidx.compose.material3.TextFieldDefaults
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
@@ -88,10 +90,12 @@ fun PrivateMessageEditFieldRow(
|
|||||||
nav: INav,
|
nav: INav,
|
||||||
) {
|
) {
|
||||||
BackHandler {
|
BackHandler {
|
||||||
accountViewModel.launchSigner {
|
if (channelScreenModel.message.text.isNotBlank()) {
|
||||||
channelScreenModel.sendDraftSync()
|
accountViewModel.launchSigner {
|
||||||
channelScreenModel.cancel()
|
channelScreenModel.sendDraftSync()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
channelScreenModel.cancel()
|
||||||
nav.popBack()
|
nav.popBack()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -114,6 +118,15 @@ fun PrivateMessageEditFieldRow(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
channelScreenModel.encryptedUploadErrorTitle?.let { title ->
|
||||||
|
EncryptedUploadErrorDialog(
|
||||||
|
title = title,
|
||||||
|
message = channelScreenModel.encryptedUploadErrorMessage ?: "",
|
||||||
|
onDismiss = channelScreenModel::dismissEncryptedUploadError,
|
||||||
|
onRetryWithoutEncryption = channelScreenModel::retryWithoutEncryption,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
Column(
|
Column(
|
||||||
modifier = EditFieldModifier,
|
modifier = EditFieldModifier,
|
||||||
) {
|
) {
|
||||||
@@ -215,3 +228,36 @@ fun KeyboardLeadingIcon(
|
|||||||
ToggleNip17Button(channelScreenModel, accountViewModel)
|
ToggleNip17Button(channelScreenModel, accountViewModel)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun EncryptedUploadErrorDialog(
|
||||||
|
title: String,
|
||||||
|
message: String,
|
||||||
|
onDismiss: () -> Unit,
|
||||||
|
onRetryWithoutEncryption: () -> Unit,
|
||||||
|
) {
|
||||||
|
AlertDialog(
|
||||||
|
onDismissRequest = onDismiss,
|
||||||
|
title = { Text(title) },
|
||||||
|
text = {
|
||||||
|
Column(verticalArrangement = Arrangement.spacedBy(8.dp)) {
|
||||||
|
Text(message)
|
||||||
|
Text(
|
||||||
|
stringRes(R.string.upload_without_encryption_warning),
|
||||||
|
style = MaterialTheme.typography.bodySmall,
|
||||||
|
color = MaterialTheme.colorScheme.error,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
confirmButton = {
|
||||||
|
TextButton(onClick = onRetryWithoutEncryption) {
|
||||||
|
Text(stringRes(R.string.retry_without_encryption))
|
||||||
|
}
|
||||||
|
},
|
||||||
|
dismissButton = {
|
||||||
|
TextButton(onClick = onDismiss) {
|
||||||
|
Text(stringRes(R.string.cancel))
|
||||||
|
}
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|||||||
+30
@@ -24,11 +24,14 @@ import com.vitorpamplona.amethyst.model.Account
|
|||||||
import com.vitorpamplona.amethyst.model.LocalCache
|
import com.vitorpamplona.amethyst.model.LocalCache
|
||||||
import com.vitorpamplona.amethyst.service.uploads.UploadOrchestrator
|
import com.vitorpamplona.amethyst.service.uploads.UploadOrchestrator
|
||||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.privateDM.send.IMetaAttachments
|
import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.privateDM.send.IMetaAttachments
|
||||||
|
import com.vitorpamplona.quartz.nip01Core.tags.references.references
|
||||||
import com.vitorpamplona.quartz.nip04Dm.messages.PrivateDmEvent
|
import com.vitorpamplona.quartz.nip04Dm.messages.PrivateDmEvent
|
||||||
import com.vitorpamplona.quartz.nip17Dm.base.ChatroomKey
|
import com.vitorpamplona.quartz.nip17Dm.base.ChatroomKey
|
||||||
import com.vitorpamplona.quartz.nip17Dm.files.ChatMessageEncryptedFileHeaderEvent
|
import com.vitorpamplona.quartz.nip17Dm.files.ChatMessageEncryptedFileHeaderEvent
|
||||||
|
import com.vitorpamplona.quartz.nip17Dm.messages.ChatMessageEvent
|
||||||
import com.vitorpamplona.quartz.nip31Alts.alt
|
import com.vitorpamplona.quartz.nip31Alts.alt
|
||||||
import com.vitorpamplona.quartz.nip36SensitiveContent.contentWarning
|
import com.vitorpamplona.quartz.nip36SensitiveContent.contentWarning
|
||||||
|
import com.vitorpamplona.quartz.nip92IMeta.imetas
|
||||||
import com.vitorpamplona.quartz.utils.ciphers.AESGCM
|
import com.vitorpamplona.quartz.utils.ciphers.AESGCM
|
||||||
|
|
||||||
class ChatFileSender(
|
class ChatFileSender(
|
||||||
@@ -39,6 +42,8 @@ class ChatFileSender(
|
|||||||
uploads.forEach {
|
uploads.forEach {
|
||||||
if (it.cipher != null) {
|
if (it.cipher != null) {
|
||||||
sendNIP17(it.result, it.caption, it.contentWarningReason, it.cipher)
|
sendNIP17(it.result, it.caption, it.contentWarningReason, it.cipher)
|
||||||
|
} else {
|
||||||
|
sendNIP17AsHiddenLink(it.result, it.caption, it.contentWarningReason)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -70,6 +75,31 @@ class ChatFileSender(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
suspend fun sendNIP17AsHiddenLink(
|
||||||
|
result: UploadOrchestrator.OrchestratorResult.ServerResult,
|
||||||
|
caption: String?,
|
||||||
|
contentWarningReason: String?,
|
||||||
|
) {
|
||||||
|
val iMetaAttachments = IMetaAttachments()
|
||||||
|
iMetaAttachments.add(result, caption, contentWarningReason)
|
||||||
|
|
||||||
|
val toUsers = chatroom.users.map { LocalCache.getOrCreateUser(it).toPTag() }
|
||||||
|
|
||||||
|
val template =
|
||||||
|
ChatMessageEvent.build(result.url, toUsers) {
|
||||||
|
references(listOf(result.url))
|
||||||
|
|
||||||
|
if (!caption.isNullOrEmpty()) {
|
||||||
|
alt(caption)
|
||||||
|
}
|
||||||
|
contentWarningReason?.let { contentWarning(it) }
|
||||||
|
|
||||||
|
imetas(iMetaAttachments.filterIsIn(setOf(result.url)))
|
||||||
|
}
|
||||||
|
|
||||||
|
account.sendNip17PrivateMessage(template)
|
||||||
|
}
|
||||||
|
|
||||||
// ------
|
// ------
|
||||||
// NIP 04
|
// NIP 04
|
||||||
// ------
|
// ------
|
||||||
|
|||||||
+1
@@ -75,5 +75,6 @@ fun RoomChatFileUploadDialog(
|
|||||||
onCancel,
|
onCancel,
|
||||||
accountViewModel,
|
accountViewModel,
|
||||||
nav,
|
nav,
|
||||||
|
isNip17 = channelScreenModel.nip17,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
+82
-34
@@ -39,59 +39,62 @@ class ChatFileUploader(
|
|||||||
suspend fun justUploadNIP17(
|
suspend fun justUploadNIP17(
|
||||||
viewState: ChatFileUploadState,
|
viewState: ChatFileUploadState,
|
||||||
onError: (title: String, message: String) -> Unit,
|
onError: (title: String, message: String) -> Unit,
|
||||||
|
onEncryptedUploadError: (title: String, message: String) -> Unit,
|
||||||
context: Context,
|
context: Context,
|
||||||
onceUploaded: suspend (List<SuccessfulUploads>) -> Unit,
|
onceUploaded: suspend (List<SuccessfulUploads>) -> Unit,
|
||||||
) {
|
) {
|
||||||
val orchestrator = viewState.multiOrchestrator ?: return
|
val orchestrator = viewState.multiOrchestrator ?: return
|
||||||
viewState.isUploadingImage = true
|
viewState.mediaUploadTracker.startUpload(orchestrator.hasNonMedia())
|
||||||
|
|
||||||
val cipher = AESGCM()
|
if (viewState.encryptFiles) {
|
||||||
|
val cipher = AESGCM()
|
||||||
|
|
||||||
val results =
|
val results =
|
||||||
orchestrator.uploadEncrypted(
|
orchestrator.uploadEncrypted(
|
||||||
viewState.caption,
|
viewState.caption,
|
||||||
viewState.contentWarningReason,
|
viewState.contentWarningReason,
|
||||||
MediaCompressor.intToCompressorQuality(viewState.mediaQualitySlider),
|
MediaCompressor.intToCompressorQuality(viewState.mediaQualitySlider),
|
||||||
cipher,
|
cipher,
|
||||||
viewState.selectedServer,
|
viewState.selectedServer,
|
||||||
account,
|
account,
|
||||||
context,
|
context,
|
||||||
stripMetadata = viewState.stripLocationMetadata,
|
)
|
||||||
)
|
|
||||||
|
|
||||||
if (results.allGood) {
|
if (results.allGood) {
|
||||||
val list =
|
val list =
|
||||||
results.successful.mapNotNull { state ->
|
results.successful.mapNotNull { state ->
|
||||||
if (state.result is UploadOrchestrator.OrchestratorResult.ServerResult) {
|
if (state.result is UploadOrchestrator.OrchestratorResult.ServerResult) {
|
||||||
SuccessfulUploads(state.result, viewState.caption, viewState.contentWarningReason, cipher)
|
SuccessfulUploads(state.result, viewState.caption, viewState.contentWarningReason, cipher)
|
||||||
} else {
|
} else {
|
||||||
null
|
null
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
onceUploaded(list)
|
onceUploaded(list)
|
||||||
viewState.reset()
|
viewState.reset()
|
||||||
|
} else {
|
||||||
|
val errorMessages = results.errors.map { stringRes(context, it.errorResource, *it.params) }.distinct()
|
||||||
|
|
||||||
|
onEncryptedUploadError(
|
||||||
|
stringRes(context, R.string.failed_to_upload_encrypted_media_title),
|
||||||
|
stringRes(context, R.string.failed_to_upload_encrypted_media_message) + "\n\n" + errorMessages.joinToString(".\n"),
|
||||||
|
)
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
val errorMessages = results.errors.map { stringRes(context, it.errorResource, *it.params) }.distinct()
|
justUploadNIP17Unencrypted(viewState, onError, context, onceUploaded)
|
||||||
|
|
||||||
onError(stringRes(context, R.string.failed_to_upload_media_no_details), errorMessages.joinToString(".\n"))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
viewState.isUploadingImage = false
|
viewState.mediaUploadTracker.finishUpload()
|
||||||
}
|
}
|
||||||
|
|
||||||
// ------
|
suspend fun justUploadNIP17Unencrypted(
|
||||||
// NIP 04
|
|
||||||
// ------
|
|
||||||
|
|
||||||
suspend fun justUploadNIP04(
|
|
||||||
viewState: ChatFileUploadState,
|
viewState: ChatFileUploadState,
|
||||||
onError: (title: String, message: String) -> Unit,
|
onError: (title: String, message: String) -> Unit,
|
||||||
context: Context,
|
context: Context,
|
||||||
onceUploaded: suspend (List<SuccessfulUploads>) -> Unit,
|
onceUploaded: suspend (List<SuccessfulUploads>) -> Unit,
|
||||||
) {
|
) {
|
||||||
val orchestrator = viewState.multiOrchestrator ?: return
|
val orchestrator = viewState.multiOrchestrator ?: return
|
||||||
viewState.isUploadingImage = true
|
viewState.mediaUploadTracker.startUpload(orchestrator.hasNonMedia())
|
||||||
|
|
||||||
val results =
|
val results =
|
||||||
orchestrator.upload(
|
orchestrator.upload(
|
||||||
@@ -122,6 +125,51 @@ class ChatFileUploader(
|
|||||||
onError(stringRes(context, R.string.failed_to_upload_media_no_details), errorMessages.joinToString(".\n"))
|
onError(stringRes(context, R.string.failed_to_upload_media_no_details), errorMessages.joinToString(".\n"))
|
||||||
}
|
}
|
||||||
|
|
||||||
viewState.isUploadingImage = false
|
viewState.mediaUploadTracker.finishUpload()
|
||||||
|
}
|
||||||
|
|
||||||
|
// ------
|
||||||
|
// NIP 04
|
||||||
|
// ------
|
||||||
|
|
||||||
|
suspend fun justUploadNIP04(
|
||||||
|
viewState: ChatFileUploadState,
|
||||||
|
onError: (title: String, message: String) -> Unit,
|
||||||
|
context: Context,
|
||||||
|
onceUploaded: suspend (List<SuccessfulUploads>) -> Unit,
|
||||||
|
) {
|
||||||
|
val orchestrator = viewState.multiOrchestrator ?: return
|
||||||
|
viewState.mediaUploadTracker.startUpload(orchestrator.hasNonMedia())
|
||||||
|
|
||||||
|
val results =
|
||||||
|
orchestrator.upload(
|
||||||
|
viewState.caption,
|
||||||
|
viewState.contentWarningReason,
|
||||||
|
MediaCompressor.intToCompressorQuality(viewState.mediaQualitySlider),
|
||||||
|
viewState.selectedServer,
|
||||||
|
account,
|
||||||
|
context,
|
||||||
|
stripMetadata = viewState.stripLocationMetadata,
|
||||||
|
)
|
||||||
|
|
||||||
|
if (results.allGood) {
|
||||||
|
val list =
|
||||||
|
results.successful.mapNotNull { state ->
|
||||||
|
if (state.result is UploadOrchestrator.OrchestratorResult.ServerResult) {
|
||||||
|
SuccessfulUploads(state.result, viewState.caption, viewState.contentWarningReason, null)
|
||||||
|
} else {
|
||||||
|
null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onceUploaded(list)
|
||||||
|
viewState.reset()
|
||||||
|
} else {
|
||||||
|
val errorMessages = results.errors.map { stringRes(context, it.errorResource, *it.params) }.distinct()
|
||||||
|
|
||||||
|
onError(stringRes(context, R.string.failed_to_upload_media_no_details), errorMessages.joinToString(".\n"))
|
||||||
|
}
|
||||||
|
|
||||||
|
viewState.mediaUploadTracker.finishUpload()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+2
-2
@@ -105,7 +105,7 @@ class ChannelMetadataViewModel : ViewModel() {
|
|||||||
|
|
||||||
fun createOrUpdate(onDone: (PublicChatChannel) -> Unit) {
|
fun createOrUpdate(onDone: (PublicChatChannel) -> Unit) {
|
||||||
viewModelScope.launch(Dispatchers.IO) {
|
viewModelScope.launch(Dispatchers.IO) {
|
||||||
account?.let { account ->
|
account.let { account ->
|
||||||
val channel = originalChannel
|
val channel = originalChannel
|
||||||
if (channel == null) {
|
if (channel == null) {
|
||||||
val template =
|
val template =
|
||||||
@@ -205,7 +205,7 @@ class ChannelMetadataViewModel : ViewModel() {
|
|||||||
onUploaded: (String) -> Unit,
|
onUploaded: (String) -> Unit,
|
||||||
onError: (String, String) -> Unit,
|
onError: (String, String) -> Unit,
|
||||||
) {
|
) {
|
||||||
val account = account ?: return
|
val account = account
|
||||||
onUploading(true)
|
onUploading(true)
|
||||||
|
|
||||||
val strippedUri =
|
val strippedUri =
|
||||||
|
|||||||
+7
-4
@@ -134,7 +134,8 @@ open class ChannelNewMessageViewModel :
|
|||||||
|
|
||||||
var message by mutableStateOf(TextFieldValue(""))
|
var message by mutableStateOf(TextFieldValue(""))
|
||||||
var urlPreview by mutableStateOf<String?>(null)
|
var urlPreview by mutableStateOf<String?>(null)
|
||||||
var isUploadingImage by mutableStateOf(false)
|
val isUploadingImage: Boolean get() = uploadState?.isUploadingImage ?: false
|
||||||
|
val isUploadingFile: Boolean get() = uploadState?.isUploadingFile ?: false
|
||||||
|
|
||||||
var userSuggestions: UserSuggestionState? = null
|
var userSuggestions: UserSuggestionState? = null
|
||||||
var userSuggestionsMainMessage: UserSuggestionAnchor? = null
|
var userSuggestionsMainMessage: UserSuggestionAnchor? = null
|
||||||
@@ -337,7 +338,7 @@ open class ChannelNewMessageViewModel :
|
|||||||
|
|
||||||
val myMultiOrchestrator = uploadState.multiOrchestrator ?: return@launch
|
val myMultiOrchestrator = uploadState.multiOrchestrator ?: return@launch
|
||||||
|
|
||||||
isUploadingImage = true
|
uploadState.mediaUploadTracker.startUpload(myMultiOrchestrator.hasNonMedia())
|
||||||
|
|
||||||
val results =
|
val results =
|
||||||
myMultiOrchestrator.upload(
|
myMultiOrchestrator.upload(
|
||||||
@@ -379,7 +380,7 @@ open class ChannelNewMessageViewModel :
|
|||||||
onError(stringRes(context, R.string.failed_to_upload_media_no_details), errorMessages.joinToString(".\n"))
|
onError(stringRes(context, R.string.failed_to_upload_media_no_details), errorMessages.joinToString(".\n"))
|
||||||
}
|
}
|
||||||
|
|
||||||
isUploadingImage = false
|
uploadState.mediaUploadTracker.finishUpload()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -547,6 +548,8 @@ open class ChannelNewMessageViewModel :
|
|||||||
userSuggestions?.reset()
|
userSuggestions?.reset()
|
||||||
userSuggestionsMainMessage = null
|
userSuggestionsMainMessage = null
|
||||||
|
|
||||||
|
uploadState?.reset()
|
||||||
|
|
||||||
iMetaAttachments.reset()
|
iMetaAttachments.reset()
|
||||||
|
|
||||||
emojiSuggestions?.reset()
|
emojiSuggestions?.reset()
|
||||||
@@ -633,7 +636,7 @@ open class ChannelNewMessageViewModel :
|
|||||||
|
|
||||||
fun canPost(): Boolean =
|
fun canPost(): Boolean =
|
||||||
message.text.isNotBlank() &&
|
message.text.isNotBlank() &&
|
||||||
uploadState?.isUploadingImage != true &&
|
uploadState?.mediaUploadTracker?.isUploading != true &&
|
||||||
!wantsInvoice &&
|
!wantsInvoice &&
|
||||||
(!wantsZapraiser || zapRaiserAmount != null) &&
|
(!wantsZapraiser || zapRaiserAmount != null) &&
|
||||||
uploadState?.multiOrchestrator == null
|
uploadState?.multiOrchestrator == null
|
||||||
|
|||||||
+7
-1
@@ -48,6 +48,7 @@ import androidx.compose.ui.Modifier
|
|||||||
import androidx.compose.ui.graphics.Color
|
import androidx.compose.ui.graphics.Color
|
||||||
import com.vitorpamplona.amethyst.R
|
import com.vitorpamplona.amethyst.R
|
||||||
import com.vitorpamplona.amethyst.commons.ui.feeds.FeedContentState
|
import com.vitorpamplona.amethyst.commons.ui.feeds.FeedContentState
|
||||||
|
import com.vitorpamplona.amethyst.ui.components.zonedDrawerSwipe
|
||||||
import com.vitorpamplona.amethyst.ui.navigation.navs.INav
|
import com.vitorpamplona.amethyst.ui.navigation.navs.INav
|
||||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
||||||
import com.vitorpamplona.amethyst.ui.stringRes
|
import com.vitorpamplona.amethyst.ui.stringRes
|
||||||
@@ -124,7 +125,12 @@ fun MessagesPager(
|
|||||||
HorizontalPager(
|
HorizontalPager(
|
||||||
contentPadding = paddingValues,
|
contentPadding = paddingValues,
|
||||||
state = pagerState,
|
state = pagerState,
|
||||||
userScrollEnabled = false,
|
userScrollEnabled = true,
|
||||||
|
modifier =
|
||||||
|
Modifier.zonedDrawerSwipe(
|
||||||
|
pagerState = pagerState,
|
||||||
|
openDrawer = nav::openDrawer,
|
||||||
|
),
|
||||||
) { page ->
|
) { page ->
|
||||||
ChatroomListFeedView(
|
ChatroomListFeedView(
|
||||||
feedContentState = tabs[page].feedContentState,
|
feedContentState = tabs[page].feedContentState,
|
||||||
|
|||||||
+13
-1
@@ -81,6 +81,7 @@ fun ChatFileUploadDialog(
|
|||||||
onCancel: () -> Unit,
|
onCancel: () -> Unit,
|
||||||
accountViewModel: AccountViewModel,
|
accountViewModel: AccountViewModel,
|
||||||
nav: INav,
|
nav: INav,
|
||||||
|
isNip17: Boolean = false,
|
||||||
) {
|
) {
|
||||||
val scrollState = rememberScrollState()
|
val scrollState = rememberScrollState()
|
||||||
|
|
||||||
@@ -132,7 +133,7 @@ fun ChatFileUploadDialog(
|
|||||||
) {
|
) {
|
||||||
Column(Modifier.fillMaxSize().padding(start = 10.dp, end = 10.dp, bottom = 10.dp)) {
|
Column(Modifier.fillMaxSize().padding(start = 10.dp, end = 10.dp, bottom = 10.dp)) {
|
||||||
Column(Modifier.fillMaxWidth().verticalScroll(scrollState)) {
|
Column(Modifier.fillMaxWidth().verticalScroll(scrollState)) {
|
||||||
ImageVideoPostChat(state, accountViewModel)
|
ImageVideoPostChat(state, accountViewModel, isNip17)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -144,6 +145,7 @@ fun ChatFileUploadDialog(
|
|||||||
private fun ImageVideoPostChat(
|
private fun ImageVideoPostChat(
|
||||||
fileUploadState: ChatFileUploadState,
|
fileUploadState: ChatFileUploadState,
|
||||||
accountViewModel: AccountViewModel,
|
accountViewModel: AccountViewModel,
|
||||||
|
isNip17: Boolean = false,
|
||||||
) {
|
) {
|
||||||
val fileServers by accountViewModel.account.blossomServers.hostNameFlow
|
val fileServers by accountViewModel.account.blossomServers.hostNameFlow
|
||||||
.collectAsState()
|
.collectAsState()
|
||||||
@@ -190,6 +192,16 @@ private fun ImageVideoPostChat(
|
|||||||
onCheckedChange = fileUploadState::updateContentWarning,
|
onCheckedChange = fileUploadState::updateContentWarning,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
if (isNip17) {
|
||||||
|
SettingSwitchItem(
|
||||||
|
title = R.string.encrypt_files_label,
|
||||||
|
description = R.string.encrypt_files_description,
|
||||||
|
modifier = Modifier.fillMaxWidth().padding(top = 8.dp),
|
||||||
|
checked = fileUploadState.encryptFiles,
|
||||||
|
onCheckedChange = { fileUploadState.encryptFiles = it },
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
SettingsRow(R.string.file_server, R.string.file_server_description) {
|
SettingsRow(R.string.file_server, R.string.file_server_description) {
|
||||||
TextSpinner(
|
TextSpinner(
|
||||||
label = "",
|
label = "",
|
||||||
|
|||||||
+8
-4
@@ -28,6 +28,7 @@ import androidx.compose.runtime.setValue
|
|||||||
import com.vitorpamplona.amethyst.commons.richtext.RichTextParser
|
import com.vitorpamplona.amethyst.commons.richtext.RichTextParser
|
||||||
import com.vitorpamplona.amethyst.service.uploads.MultiOrchestrator
|
import com.vitorpamplona.amethyst.service.uploads.MultiOrchestrator
|
||||||
import com.vitorpamplona.amethyst.ui.actions.mediaServers.ServerName
|
import com.vitorpamplona.amethyst.ui.actions.mediaServers.ServerName
|
||||||
|
import com.vitorpamplona.amethyst.ui.actions.uploads.MediaUploadTracker
|
||||||
import com.vitorpamplona.amethyst.ui.actions.uploads.SelectedMedia
|
import com.vitorpamplona.amethyst.ui.actions.uploads.SelectedMedia
|
||||||
import com.vitorpamplona.amethyst.ui.actions.uploads.SelectedMediaProcessing
|
import com.vitorpamplona.amethyst.ui.actions.uploads.SelectedMediaProcessing
|
||||||
import kotlinx.collections.immutable.ImmutableList
|
import kotlinx.collections.immutable.ImmutableList
|
||||||
@@ -37,7 +38,9 @@ class ChatFileUploadState(
|
|||||||
val defaultServer: ServerName,
|
val defaultServer: ServerName,
|
||||||
defaultStripLocationMetadata: Boolean = true,
|
defaultStripLocationMetadata: Boolean = true,
|
||||||
) {
|
) {
|
||||||
var isUploadingImage by mutableStateOf(false)
|
val mediaUploadTracker = MediaUploadTracker()
|
||||||
|
val isUploadingImage: Boolean get() = mediaUploadTracker.isUploadingImage
|
||||||
|
val isUploadingFile: Boolean get() = mediaUploadTracker.isUploadingFile
|
||||||
|
|
||||||
var selectedServer by mutableStateOf(defaultServer)
|
var selectedServer by mutableStateOf(defaultServer)
|
||||||
var caption by mutableStateOf("")
|
var caption by mutableStateOf("")
|
||||||
@@ -53,8 +56,8 @@ class ChatFileUploadState(
|
|||||||
// 0 = Low, 1 = Medium, 2 = High, 3=UNCOMPRESSED
|
// 0 = Low, 1 = Medium, 2 = High, 3=UNCOMPRESSED
|
||||||
var mediaQualitySlider by mutableIntStateOf(1)
|
var mediaQualitySlider by mutableIntStateOf(1)
|
||||||
|
|
||||||
// Strip location and sensitive metadata from files before upload
|
|
||||||
var stripLocationMetadata by mutableStateOf(defaultStripLocationMetadata)
|
var stripLocationMetadata by mutableStateOf(defaultStripLocationMetadata)
|
||||||
|
var encryptFiles by mutableStateOf(true)
|
||||||
|
|
||||||
fun load(uris: ImmutableList<SelectedMedia>) {
|
fun load(uris: ImmutableList<SelectedMedia>) {
|
||||||
reset()
|
reset()
|
||||||
@@ -68,16 +71,17 @@ class ChatFileUploadState(
|
|||||||
|
|
||||||
fun reset() {
|
fun reset() {
|
||||||
multiOrchestrator = null
|
multiOrchestrator = null
|
||||||
isUploadingImage = false
|
mediaUploadTracker.finishUpload()
|
||||||
caption = ""
|
caption = ""
|
||||||
selectedServer = defaultServer
|
selectedServer = defaultServer
|
||||||
|
encryptFiles = true
|
||||||
}
|
}
|
||||||
|
|
||||||
fun deleteMediaToUpload(selected: SelectedMediaProcessing) {
|
fun deleteMediaToUpload(selected: SelectedMediaProcessing) {
|
||||||
multiOrchestrator?.remove(selected)
|
multiOrchestrator?.remove(selected)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun canPost(): Boolean = !isUploadingImage && multiOrchestrator != null
|
fun canPost(): Boolean = !mediaUploadTracker.isUploading && multiOrchestrator != null
|
||||||
|
|
||||||
fun hasPickedMedia() = multiOrchestrator != null
|
fun hasPickedMedia() = multiOrchestrator != null
|
||||||
|
|
||||||
|
|||||||
+4
-2
@@ -297,8 +297,10 @@ fun ChessLobbyContent(
|
|||||||
val userPubkey = accountViewModel.account.userProfile().pubkeyHex
|
val userPubkey = accountViewModel.account.userProfile().pubkeyHex
|
||||||
|
|
||||||
val hasContent =
|
val hasContent =
|
||||||
activeGames.isNotEmpty() || spectatingGames.isNotEmpty() ||
|
activeGames.isNotEmpty() ||
|
||||||
publicGames.isNotEmpty() || challenges.isNotEmpty()
|
spectatingGames.isNotEmpty() ||
|
||||||
|
publicGames.isNotEmpty() ||
|
||||||
|
challenges.isNotEmpty()
|
||||||
|
|
||||||
if (!hasContent) {
|
if (!hasContent) {
|
||||||
// Empty state - use LazyColumn so pull-to-refresh works
|
// Empty state - use LazyColumn so pull-to-refresh works
|
||||||
|
|||||||
+4
-1
@@ -269,6 +269,7 @@ private fun NewProductBody(
|
|||||||
ImageVideoDescription(
|
ImageVideoDescription(
|
||||||
uris = it,
|
uris = it,
|
||||||
defaultServer = accountViewModel.account.settings.defaultFileServer,
|
defaultServer = accountViewModel.account.settings.defaultFileServer,
|
||||||
|
isUploading = postViewModel.mediaUploadTracker.isUploading,
|
||||||
onAdd = { alt, server, sensitiveContent, mediaQuality, _ ->
|
onAdd = { alt, server, sensitiveContent, mediaQuality, _ ->
|
||||||
postViewModel.upload(alt, if (sensitiveContent) "" else null, mediaQuality, server, accountViewModel.toastManager::toast, context)
|
postViewModel.upload(alt, if (sensitiveContent) "" else null, mediaQuality, server, accountViewModel.toastManager::toast, context)
|
||||||
accountViewModel.account.settings.changeDefaultFileServer(server)
|
accountViewModel.account.settings.changeDefaultFileServer(server)
|
||||||
@@ -359,6 +360,7 @@ private fun BottomRowActions(postViewModel: NewProductViewModel) {
|
|||||||
) {
|
) {
|
||||||
SelectFromGallery(
|
SelectFromGallery(
|
||||||
isUploading = postViewModel.isUploadingImage,
|
isUploading = postViewModel.isUploadingImage,
|
||||||
|
enabled = !postViewModel.isUploadingFile,
|
||||||
tint = MaterialTheme.colorScheme.onBackground,
|
tint = MaterialTheme.colorScheme.onBackground,
|
||||||
modifier = Modifier,
|
modifier = Modifier,
|
||||||
) {
|
) {
|
||||||
@@ -366,7 +368,8 @@ private fun BottomRowActions(postViewModel: NewProductViewModel) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
SelectFromFiles(
|
SelectFromFiles(
|
||||||
isUploading = postViewModel.isUploadingImage,
|
isUploading = postViewModel.isUploadingFile,
|
||||||
|
enabled = !postViewModel.isUploadingImage,
|
||||||
tint = MaterialTheme.colorScheme.onBackground,
|
tint = MaterialTheme.colorScheme.onBackground,
|
||||||
modifier = Modifier,
|
modifier = Modifier,
|
||||||
) {
|
) {
|
||||||
|
|||||||
+15
-15
@@ -45,6 +45,7 @@ import com.vitorpamplona.amethyst.service.uploads.MultiOrchestrator
|
|||||||
import com.vitorpamplona.amethyst.service.uploads.UploadOrchestrator
|
import com.vitorpamplona.amethyst.service.uploads.UploadOrchestrator
|
||||||
import com.vitorpamplona.amethyst.ui.actions.NewMessageTagger
|
import com.vitorpamplona.amethyst.ui.actions.NewMessageTagger
|
||||||
import com.vitorpamplona.amethyst.ui.actions.mediaServers.ServerName
|
import com.vitorpamplona.amethyst.ui.actions.mediaServers.ServerName
|
||||||
|
import com.vitorpamplona.amethyst.ui.actions.uploads.MediaUploadTracker
|
||||||
import com.vitorpamplona.amethyst.ui.actions.uploads.SelectedMedia
|
import com.vitorpamplona.amethyst.ui.actions.uploads.SelectedMedia
|
||||||
import com.vitorpamplona.amethyst.ui.actions.uploads.SelectedMediaProcessing
|
import com.vitorpamplona.amethyst.ui.actions.uploads.SelectedMediaProcessing
|
||||||
import com.vitorpamplona.amethyst.ui.note.creators.draftTags.DraftTagState
|
import com.vitorpamplona.amethyst.ui.note.creators.draftTags.DraftTagState
|
||||||
@@ -133,7 +134,9 @@ open class NewProductViewModel :
|
|||||||
|
|
||||||
val urlPreviews = PreviewState()
|
val urlPreviews = PreviewState()
|
||||||
|
|
||||||
var isUploadingImage by mutableStateOf(false)
|
val mediaUploadTracker = MediaUploadTracker()
|
||||||
|
val isUploadingImage: Boolean get() = mediaUploadTracker.isUploadingImage
|
||||||
|
val isUploadingFile: Boolean get() = mediaUploadTracker.isUploadingFile
|
||||||
|
|
||||||
var userSuggestions: UserSuggestionState? = null
|
var userSuggestions: UserSuggestionState? = null
|
||||||
var userSuggestionsMainMessage: UserSuggestionAnchor? = null
|
var userSuggestionsMainMessage: UserSuggestionAnchor? = null
|
||||||
@@ -217,7 +220,7 @@ open class NewProductViewModel :
|
|||||||
}
|
}
|
||||||
|
|
||||||
open fun quote(quote: Note) {
|
open fun quote(quote: Note) {
|
||||||
val accountViewModel = accountViewModel ?: return
|
val accountViewModel = accountViewModel
|
||||||
|
|
||||||
message = TextFieldValue(message.text + "\nnostr:${quote.toNEvent()}")
|
message = TextFieldValue(message.text + "\nnostr:${quote.toNEvent()}")
|
||||||
|
|
||||||
@@ -304,7 +307,6 @@ open class NewProductViewModel :
|
|||||||
}
|
}
|
||||||
|
|
||||||
suspend fun sendPostSync() {
|
suspend fun sendPostSync() {
|
||||||
val accountViewModel = accountViewModel ?: return
|
|
||||||
val template = createTemplate() ?: return
|
val template = createTemplate() ?: return
|
||||||
|
|
||||||
val version = draftTag.current
|
val version = draftTag.current
|
||||||
@@ -317,8 +319,6 @@ open class NewProductViewModel :
|
|||||||
}
|
}
|
||||||
|
|
||||||
suspend fun sendDraftSync() {
|
suspend fun sendDraftSync() {
|
||||||
val accountViewModel = accountViewModel ?: return
|
|
||||||
|
|
||||||
if (message.text.isBlank()) {
|
if (message.text.isBlank()) {
|
||||||
accountViewModel.account.deleteDraftIgnoreErrors(draftTag.current)
|
accountViewModel.account.deleteDraftIgnoreErrors(draftTag.current)
|
||||||
} else {
|
} else {
|
||||||
@@ -328,7 +328,7 @@ open class NewProductViewModel :
|
|||||||
}
|
}
|
||||||
|
|
||||||
private suspend fun createTemplate(): EventTemplate<out Event>? {
|
private suspend fun createTemplate(): EventTemplate<out Event>? {
|
||||||
val accountViewModel = accountViewModel ?: return null
|
val accountViewModel = accountViewModel
|
||||||
|
|
||||||
val tagger =
|
val tagger =
|
||||||
NewMessageTagger(
|
NewMessageTagger(
|
||||||
@@ -337,7 +337,7 @@ open class NewProductViewModel :
|
|||||||
)
|
)
|
||||||
tagger.run()
|
tagger.run()
|
||||||
|
|
||||||
val emojis = findEmoji(tagger.message, account?.emoji?.myEmojis?.value)
|
val emojis = findEmoji(tagger.message, account.emoji.myEmojis.value)
|
||||||
val urls = findURLs(tagger.message)
|
val urls = findURLs(tagger.message)
|
||||||
val usedAttachments = iMetaDescription.filterIsIn(urls.toSet()) + productImages.map { it.toIMeta() }
|
val usedAttachments = iMetaDescription.filterIsIn(urls.toSet()) + productImages.map { it.toIMeta() }
|
||||||
|
|
||||||
@@ -396,10 +396,10 @@ open class NewProductViewModel :
|
|||||||
context: Context,
|
context: Context,
|
||||||
) {
|
) {
|
||||||
viewModelScope.launch(Dispatchers.IO) {
|
viewModelScope.launch(Dispatchers.IO) {
|
||||||
val myAccount = account ?: return@launch
|
val myAccount = account
|
||||||
val myMultiOrchestrator = multiOrchestrator ?: return@launch
|
val myMultiOrchestrator = multiOrchestrator ?: return@launch
|
||||||
|
|
||||||
isUploadingImage = true
|
mediaUploadTracker.startUpload(myMultiOrchestrator.hasNonMedia())
|
||||||
|
|
||||||
val results =
|
val results =
|
||||||
myMultiOrchestrator.upload(
|
myMultiOrchestrator.upload(
|
||||||
@@ -444,7 +444,7 @@ open class NewProductViewModel :
|
|||||||
onError(stringRes(context, R.string.failed_to_upload_media_no_details), errorMessages.joinToString(".\n"))
|
onError(stringRes(context, R.string.failed_to_upload_media_no_details), errorMessages.joinToString(".\n"))
|
||||||
}
|
}
|
||||||
|
|
||||||
isUploadingImage = false
|
mediaUploadTracker.finishUpload()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -454,7 +454,7 @@ open class NewProductViewModel :
|
|||||||
message = TextFieldValue("")
|
message = TextFieldValue("")
|
||||||
|
|
||||||
multiOrchestrator = null
|
multiOrchestrator = null
|
||||||
isUploadingImage = false
|
mediaUploadTracker.finishUpload()
|
||||||
|
|
||||||
wantsInvoice = false
|
wantsInvoice = false
|
||||||
wantsZapraiser = false
|
wantsZapraiser = false
|
||||||
@@ -498,8 +498,8 @@ open class NewProductViewModel :
|
|||||||
this.multiOrchestrator?.remove(selected)
|
this.multiOrchestrator?.remove(selected)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun updateMessage(it: TextFieldValue) {
|
override fun updateMessage(newMessage: TextFieldValue) {
|
||||||
message = it
|
message = newMessage
|
||||||
urlPreviews.update(message)
|
urlPreviews.update(message)
|
||||||
|
|
||||||
if (message.selection.collapsed) {
|
if (message.selection.collapsed) {
|
||||||
@@ -575,7 +575,7 @@ open class NewProductViewModel :
|
|||||||
|
|
||||||
fun canPost(): Boolean =
|
fun canPost(): Boolean =
|
||||||
message.text.isNotBlank() &&
|
message.text.isNotBlank() &&
|
||||||
!isUploadingImage &&
|
!mediaUploadTracker.isUploading &&
|
||||||
!wantsInvoice &&
|
!wantsInvoice &&
|
||||||
(!wantsZapraiser || zapRaiserAmount.value != null) &&
|
(!wantsZapraiser || zapRaiserAmount.value != null) &&
|
||||||
title.text.isNotBlank() &&
|
title.text.isNotBlank() &&
|
||||||
@@ -613,7 +613,7 @@ open class NewProductViewModel :
|
|||||||
|
|
||||||
override fun updateZapFromText() {
|
override fun updateZapFromText() {
|
||||||
viewModelScope.launch(Dispatchers.IO) {
|
viewModelScope.launch(Dispatchers.IO) {
|
||||||
val tagger = NewMessageTagger(message.text, emptyList(), emptyList(), accountViewModel!!)
|
val tagger = NewMessageTagger(message.text, emptyList(), emptyList(), accountViewModel)
|
||||||
tagger.run()
|
tagger.run()
|
||||||
tagger.pTags?.forEach { taggedUser ->
|
tagger.pTags?.forEach { taggedUser ->
|
||||||
if (!forwardZapTo.value.items.any { it.key == taggedUser }) {
|
if (!forwardZapTo.value.items.any { it.key == taggedUser }) {
|
||||||
|
|||||||
+2
-1
@@ -83,7 +83,8 @@ class HashtagFeedFilter(
|
|||||||
event is PrivateDmEvent ||
|
event is PrivateDmEvent ||
|
||||||
event is PollNoteEvent ||
|
event is PollNoteEvent ||
|
||||||
event is AudioHeaderEvent
|
event is AudioHeaderEvent
|
||||||
) && event.isTaggedHash(hashTag)
|
) &&
|
||||||
|
event.isTaggedHash(hashTag)
|
||||||
|
|
||||||
fun acceptableViaScope(
|
fun acceptableViaScope(
|
||||||
event: Event?,
|
event: Event?,
|
||||||
|
|||||||
+7
-1
@@ -63,6 +63,7 @@ import com.vitorpamplona.amethyst.model.TopFilter
|
|||||||
import com.vitorpamplona.amethyst.service.OnlineChecker
|
import com.vitorpamplona.amethyst.service.OnlineChecker
|
||||||
import com.vitorpamplona.amethyst.service.location.LocationState
|
import com.vitorpamplona.amethyst.service.location.LocationState
|
||||||
import com.vitorpamplona.amethyst.ui.actions.CrossfadeIfEnabled
|
import com.vitorpamplona.amethyst.ui.actions.CrossfadeIfEnabled
|
||||||
|
import com.vitorpamplona.amethyst.ui.components.zonedDrawerSwipe
|
||||||
import com.vitorpamplona.amethyst.ui.feeds.ChannelFeedContentState
|
import com.vitorpamplona.amethyst.ui.feeds.ChannelFeedContentState
|
||||||
import com.vitorpamplona.amethyst.ui.feeds.ChannelFeedState
|
import com.vitorpamplona.amethyst.ui.feeds.ChannelFeedState
|
||||||
import com.vitorpamplona.amethyst.ui.feeds.PagerStateKeys
|
import com.vitorpamplona.amethyst.ui.feeds.PagerStateKeys
|
||||||
@@ -211,7 +212,12 @@ private fun HomePages(
|
|||||||
HorizontalPager(
|
HorizontalPager(
|
||||||
contentPadding = it,
|
contentPadding = it,
|
||||||
state = pagerState,
|
state = pagerState,
|
||||||
userScrollEnabled = false,
|
userScrollEnabled = true,
|
||||||
|
modifier =
|
||||||
|
Modifier.zonedDrawerSwipe(
|
||||||
|
pagerState = pagerState,
|
||||||
|
openDrawer = nav::openDrawer,
|
||||||
|
),
|
||||||
) { page ->
|
) { page ->
|
||||||
HomeFeeds(
|
HomeFeeds(
|
||||||
feedState = tabs[page].feedState,
|
feedState = tabs[page].feedState,
|
||||||
|
|||||||
+4
-1
@@ -356,6 +356,7 @@ private fun NewPostScreenBody(
|
|||||||
ImageVideoDescription(
|
ImageVideoDescription(
|
||||||
it,
|
it,
|
||||||
accountViewModel.account.settings.defaultFileServer,
|
accountViewModel.account.settings.defaultFileServer,
|
||||||
|
isUploading = postViewModel.mediaUploadTracker.isUploading,
|
||||||
onAdd = { alt, server, sensitiveContent, mediaQuality, useH265 ->
|
onAdd = { alt, server, sensitiveContent, mediaQuality, useH265 ->
|
||||||
postViewModel.upload(alt, if (sensitiveContent) "" else null, mediaQuality, server, accountViewModel.toastManager::toast, context, useH265)
|
postViewModel.upload(alt, if (sensitiveContent) "" else null, mediaQuality, server, accountViewModel.toastManager::toast, context, useH265)
|
||||||
accountViewModel.account.settings.changeDefaultFileServer(server)
|
accountViewModel.account.settings.changeDefaultFileServer(server)
|
||||||
@@ -494,6 +495,7 @@ private fun BottomRowActions(postViewModel: ShortNotePostViewModel) {
|
|||||||
) {
|
) {
|
||||||
SelectFromGallery(
|
SelectFromGallery(
|
||||||
isUploading = postViewModel.isUploadingImage,
|
isUploading = postViewModel.isUploadingImage,
|
||||||
|
enabled = !postViewModel.isUploadingFile,
|
||||||
tint = MaterialTheme.colorScheme.onBackground,
|
tint = MaterialTheme.colorScheme.onBackground,
|
||||||
modifier = Modifier,
|
modifier = Modifier,
|
||||||
) {
|
) {
|
||||||
@@ -501,7 +503,8 @@ private fun BottomRowActions(postViewModel: ShortNotePostViewModel) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
SelectFromFiles(
|
SelectFromFiles(
|
||||||
isUploading = postViewModel.isUploadingImage,
|
isUploading = postViewModel.isUploadingFile,
|
||||||
|
enabled = !postViewModel.isUploadingImage,
|
||||||
tint = MaterialTheme.colorScheme.onBackground,
|
tint = MaterialTheme.colorScheme.onBackground,
|
||||||
modifier = Modifier,
|
modifier = Modifier,
|
||||||
) {
|
) {
|
||||||
|
|||||||
+11
-7
@@ -49,6 +49,7 @@ import com.vitorpamplona.amethyst.service.uploads.UploadOrchestrator
|
|||||||
import com.vitorpamplona.amethyst.service.uploads.UploadingState
|
import com.vitorpamplona.amethyst.service.uploads.UploadingState
|
||||||
import com.vitorpamplona.amethyst.ui.actions.NewMessageTagger
|
import com.vitorpamplona.amethyst.ui.actions.NewMessageTagger
|
||||||
import com.vitorpamplona.amethyst.ui.actions.mediaServers.ServerName
|
import com.vitorpamplona.amethyst.ui.actions.mediaServers.ServerName
|
||||||
|
import com.vitorpamplona.amethyst.ui.actions.uploads.MediaUploadTracker
|
||||||
import com.vitorpamplona.amethyst.ui.actions.uploads.RecordingResult
|
import com.vitorpamplona.amethyst.ui.actions.uploads.RecordingResult
|
||||||
import com.vitorpamplona.amethyst.ui.actions.uploads.SelectedMedia
|
import com.vitorpamplona.amethyst.ui.actions.uploads.SelectedMedia
|
||||||
import com.vitorpamplona.amethyst.ui.actions.uploads.SelectedMediaProcessing
|
import com.vitorpamplona.amethyst.ui.actions.uploads.SelectedMediaProcessing
|
||||||
@@ -178,7 +179,9 @@ open class ShortNotePostViewModel :
|
|||||||
|
|
||||||
val urlPreviews = PreviewState()
|
val urlPreviews = PreviewState()
|
||||||
|
|
||||||
var isUploadingImage by mutableStateOf(false)
|
val mediaUploadTracker = MediaUploadTracker()
|
||||||
|
val isUploadingImage: Boolean get() = mediaUploadTracker.isUploadingImage
|
||||||
|
val isUploadingFile: Boolean get() = mediaUploadTracker.isUploadingFile
|
||||||
|
|
||||||
var userSuggestions: UserSuggestionState? = null
|
var userSuggestions: UserSuggestionState? = null
|
||||||
var userSuggestionsMainMessage: UserSuggestionAnchor? = null
|
var userSuggestionsMainMessage: UserSuggestionAnchor? = null
|
||||||
@@ -829,7 +832,7 @@ open class ShortNotePostViewModel :
|
|||||||
viewModelScope.launch(Dispatchers.IO) {
|
viewModelScope.launch(Dispatchers.IO) {
|
||||||
val myMultiOrchestrator = multiOrchestrator ?: return@launch
|
val myMultiOrchestrator = multiOrchestrator ?: return@launch
|
||||||
|
|
||||||
isUploadingImage = true
|
mediaUploadTracker.startUpload(myMultiOrchestrator.hasNonMedia())
|
||||||
|
|
||||||
val results =
|
val results =
|
||||||
myMultiOrchestrator.upload(
|
myMultiOrchestrator.upload(
|
||||||
@@ -885,7 +888,7 @@ open class ShortNotePostViewModel :
|
|||||||
onError(stringRes(context, R.string.failed_to_upload_media_no_details), errorMessages.joinToString(".\n"))
|
onError(stringRes(context, R.string.failed_to_upload_media_no_details), errorMessages.joinToString(".\n"))
|
||||||
}
|
}
|
||||||
|
|
||||||
isUploadingImage = false
|
mediaUploadTracker.finishUpload()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -897,7 +900,7 @@ open class ShortNotePostViewModel :
|
|||||||
forkedFromNote = null
|
forkedFromNote = null
|
||||||
|
|
||||||
multiOrchestrator = null
|
multiOrchestrator = null
|
||||||
isUploadingImage = false
|
mediaUploadTracker.finishUpload()
|
||||||
voiceAnonymization.clear()
|
voiceAnonymization.clear()
|
||||||
deleteVoiceLocalFile()
|
deleteVoiceLocalFile()
|
||||||
voiceRecording = null
|
voiceRecording = null
|
||||||
@@ -1032,19 +1035,20 @@ open class ShortNotePostViewModel :
|
|||||||
fun canPost(): Boolean {
|
fun canPost(): Boolean {
|
||||||
// Voice messages can be posted without text (with either uploaded or pending recording)
|
// Voice messages can be posted without text (with either uploaded or pending recording)
|
||||||
if (voiceMetadata != null || voiceRecording != null) {
|
if (voiceMetadata != null || voiceRecording != null) {
|
||||||
return !isUploadingVoice && !isUploadingImage && processingPreset == null
|
return !isUploadingVoice && !mediaUploadTracker.isUploading && processingPreset == null
|
||||||
}
|
}
|
||||||
|
|
||||||
// Regular text/media posts require text
|
// Regular text/media posts require text
|
||||||
return message.text.isNotBlank() &&
|
return message.text.isNotBlank() &&
|
||||||
!isUploadingImage &&
|
!mediaUploadTracker.isUploading &&
|
||||||
!isUploadingVoice &&
|
!isUploadingVoice &&
|
||||||
!wantsInvoice &&
|
!wantsInvoice &&
|
||||||
(!wantsZapRaiser || zapRaiserAmount.value != null) &&
|
(!wantsZapRaiser || zapRaiserAmount.value != null) &&
|
||||||
(
|
(
|
||||||
!wantsPoll ||
|
!wantsPoll ||
|
||||||
(
|
(
|
||||||
pollOptions.isNotEmpty() && pollOptions.all { it.value.label.isNotEmpty() } &&
|
pollOptions.isNotEmpty() &&
|
||||||
|
pollOptions.all { it.value.label.isNotEmpty() } &&
|
||||||
closedAt > TimeUtils.oneMinuteFromNow()
|
closedAt > TimeUtils.oneMinuteFromNow()
|
||||||
)
|
)
|
||||||
) &&
|
) &&
|
||||||
|
|||||||
+4
-4
@@ -21,10 +21,10 @@
|
|||||||
package com.vitorpamplona.amethyst.ui.screen.loggedIn.notifications
|
package com.vitorpamplona.amethyst.ui.screen.loggedIn.notifications
|
||||||
|
|
||||||
import androidx.compose.runtime.Stable
|
import androidx.compose.runtime.Stable
|
||||||
import com.patrykandpatrick.vico.core.cartesian.data.CartesianChartModel
|
import com.patrykandpatrick.vico.compose.cartesian.data.CartesianChartModel
|
||||||
import com.patrykandpatrick.vico.core.cartesian.data.LineCartesianLayerModel
|
import com.patrykandpatrick.vico.compose.cartesian.data.LineCartesianLayerModel
|
||||||
import com.patrykandpatrick.vico.core.common.data.ExtraStore
|
import com.patrykandpatrick.vico.compose.common.data.ExtraStore
|
||||||
import com.patrykandpatrick.vico.core.common.data.MutableExtraStore
|
import com.patrykandpatrick.vico.compose.common.data.MutableExtraStore
|
||||||
import com.vitorpamplona.amethyst.model.Account
|
import com.vitorpamplona.amethyst.model.Account
|
||||||
import com.vitorpamplona.amethyst.model.LocalCache
|
import com.vitorpamplona.amethyst.model.LocalCache
|
||||||
import com.vitorpamplona.amethyst.model.Note
|
import com.vitorpamplona.amethyst.model.Note
|
||||||
|
|||||||
+3
-3
@@ -21,9 +21,9 @@
|
|||||||
package com.vitorpamplona.amethyst.ui.screen.loggedIn.notifications.chart
|
package com.vitorpamplona.amethyst.ui.screen.loggedIn.notifications.chart
|
||||||
|
|
||||||
import androidx.compose.runtime.Stable
|
import androidx.compose.runtime.Stable
|
||||||
import com.patrykandpatrick.vico.core.cartesian.CartesianMeasuringContext
|
import com.patrykandpatrick.vico.compose.cartesian.CartesianMeasuringContext
|
||||||
import com.patrykandpatrick.vico.core.cartesian.axis.Axis
|
import com.patrykandpatrick.vico.compose.cartesian.axis.Axis
|
||||||
import com.patrykandpatrick.vico.core.cartesian.data.CartesianValueFormatter
|
import com.patrykandpatrick.vico.compose.cartesian.data.CartesianValueFormatter
|
||||||
import com.vitorpamplona.amethyst.ui.note.showAmountIntegerWithZero
|
import com.vitorpamplona.amethyst.ui.note.showAmountIntegerWithZero
|
||||||
import com.vitorpamplona.amethyst.ui.note.showAmountWithZero
|
import com.vitorpamplona.amethyst.ui.note.showAmountWithZero
|
||||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.notifications.ShowDecimals
|
import com.vitorpamplona.amethyst.ui.screen.loggedIn.notifications.ShowDecimals
|
||||||
|
|||||||
+3
-3
@@ -21,9 +21,9 @@
|
|||||||
package com.vitorpamplona.amethyst.ui.screen.loggedIn.notifications.chart
|
package com.vitorpamplona.amethyst.ui.screen.loggedIn.notifications.chart
|
||||||
|
|
||||||
import androidx.compose.runtime.Stable
|
import androidx.compose.runtime.Stable
|
||||||
import com.patrykandpatrick.vico.core.cartesian.CartesianMeasuringContext
|
import com.patrykandpatrick.vico.compose.cartesian.CartesianMeasuringContext
|
||||||
import com.patrykandpatrick.vico.core.cartesian.axis.Axis
|
import com.patrykandpatrick.vico.compose.cartesian.axis.Axis
|
||||||
import com.patrykandpatrick.vico.core.cartesian.data.CartesianValueFormatter
|
import com.patrykandpatrick.vico.compose.cartesian.data.CartesianValueFormatter
|
||||||
import kotlin.math.roundToInt
|
import kotlin.math.roundToInt
|
||||||
|
|
||||||
@Stable
|
@Stable
|
||||||
|
|||||||
+3
-3
@@ -22,9 +22,9 @@ package com.vitorpamplona.amethyst.ui.screen.loggedIn.notifications.chart
|
|||||||
|
|
||||||
import android.util.LruCache
|
import android.util.LruCache
|
||||||
import androidx.compose.runtime.Stable
|
import androidx.compose.runtime.Stable
|
||||||
import com.patrykandpatrick.vico.core.cartesian.CartesianMeasuringContext
|
import com.patrykandpatrick.vico.compose.cartesian.CartesianMeasuringContext
|
||||||
import com.patrykandpatrick.vico.core.cartesian.axis.Axis
|
import com.patrykandpatrick.vico.compose.cartesian.axis.Axis
|
||||||
import com.patrykandpatrick.vico.core.cartesian.data.CartesianValueFormatter
|
import com.patrykandpatrick.vico.compose.cartesian.data.CartesianValueFormatter
|
||||||
import java.time.LocalDateTime
|
import java.time.LocalDateTime
|
||||||
import java.time.format.DateTimeFormatter
|
import java.time.format.DateTimeFormatter
|
||||||
import kotlin.math.roundToInt
|
import kotlin.math.roundToInt
|
||||||
|
|||||||
+24
-21
@@ -21,34 +21,34 @@
|
|||||||
package com.vitorpamplona.amethyst.ui.screen.loggedIn.notifications.chart
|
package com.vitorpamplona.amethyst.ui.screen.loggedIn.notifications.chart
|
||||||
|
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.ui.graphics.Brush
|
||||||
import androidx.compose.ui.graphics.Color
|
import androidx.compose.ui.graphics.Color
|
||||||
import androidx.compose.ui.graphics.toArgb
|
import androidx.compose.ui.text.TextStyle
|
||||||
|
import androidx.compose.ui.unit.sp
|
||||||
import com.patrykandpatrick.vico.compose.cartesian.CartesianChartHost
|
import com.patrykandpatrick.vico.compose.cartesian.CartesianChartHost
|
||||||
|
import com.patrykandpatrick.vico.compose.cartesian.axis.Axis
|
||||||
|
import com.patrykandpatrick.vico.compose.cartesian.axis.HorizontalAxis
|
||||||
|
import com.patrykandpatrick.vico.compose.cartesian.axis.VerticalAxis
|
||||||
import com.patrykandpatrick.vico.compose.cartesian.axis.rememberAxisLabelComponent
|
import com.patrykandpatrick.vico.compose.cartesian.axis.rememberAxisLabelComponent
|
||||||
import com.patrykandpatrick.vico.compose.cartesian.axis.rememberBottom
|
import com.patrykandpatrick.vico.compose.cartesian.data.CartesianChartModel
|
||||||
import com.patrykandpatrick.vico.compose.cartesian.axis.rememberEnd
|
import com.patrykandpatrick.vico.compose.cartesian.layer.LineCartesianLayer
|
||||||
import com.patrykandpatrick.vico.compose.cartesian.axis.rememberStart
|
import com.patrykandpatrick.vico.compose.cartesian.layer.LineCartesianLayer.AreaFill.Companion.single
|
||||||
|
import com.patrykandpatrick.vico.compose.cartesian.layer.LineCartesianLayer.Line
|
||||||
import com.patrykandpatrick.vico.compose.cartesian.rememberCartesianChart
|
import com.patrykandpatrick.vico.compose.cartesian.rememberCartesianChart
|
||||||
import com.patrykandpatrick.vico.compose.common.fill
|
import com.patrykandpatrick.vico.compose.common.Fill
|
||||||
import com.patrykandpatrick.vico.core.cartesian.axis.Axis
|
|
||||||
import com.patrykandpatrick.vico.core.cartesian.axis.HorizontalAxis
|
|
||||||
import com.patrykandpatrick.vico.core.cartesian.axis.VerticalAxis
|
|
||||||
import com.patrykandpatrick.vico.core.cartesian.data.CartesianChartModel
|
|
||||||
import com.patrykandpatrick.vico.core.cartesian.layer.LineCartesianLayer
|
|
||||||
import com.patrykandpatrick.vico.core.common.shader.ShaderProvider
|
|
||||||
import com.vitorpamplona.amethyst.ui.theme.BitcoinOrange
|
import com.vitorpamplona.amethyst.ui.theme.BitcoinOrange
|
||||||
import com.vitorpamplona.amethyst.ui.theme.RoyalBlue
|
import com.vitorpamplona.amethyst.ui.theme.RoyalBlue
|
||||||
|
|
||||||
fun makeLine(color: Color): LineCartesianLayer.Line =
|
fun makeLine(color: Color): Line =
|
||||||
LineCartesianLayer.Line(
|
Line(
|
||||||
fill = LineCartesianLayer.LineFill.single(fill(color)),
|
fill = LineCartesianLayer.LineFill.single(Fill(color)),
|
||||||
areaFill =
|
areaFill =
|
||||||
LineCartesianLayer.AreaFill.single(
|
single(
|
||||||
fill(
|
Fill(
|
||||||
ShaderProvider.verticalGradient(
|
brush =
|
||||||
color.copy(alpha = 0.4f).toArgb(),
|
Brush.verticalGradient(
|
||||||
Color.Transparent.toArgb(),
|
colors = listOf(color.copy(alpha = 0.4f), Color.Transparent),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
pointConnector = LineCartesianLayer.PointConnector.cubic(),
|
pointConnector = LineCartesianLayer.PointConnector.cubic(),
|
||||||
@@ -84,7 +84,10 @@ fun ShowChart(model: CartesianChartModel) {
|
|||||||
),
|
),
|
||||||
endAxis =
|
endAxis =
|
||||||
VerticalAxis.rememberEnd(
|
VerticalAxis.rememberEnd(
|
||||||
label = rememberAxisLabelComponent(color = BitcoinOrange),
|
label =
|
||||||
|
rememberAxisLabelComponent(
|
||||||
|
style = TextStyle(color = BitcoinOrange, fontSize = 12.sp),
|
||||||
|
),
|
||||||
valueFormatter = AmountValueFormatter(),
|
valueFormatter = AmountValueFormatter(),
|
||||||
itemPlacer = VerticalAxis.ItemPlacer.count({ 7 }),
|
itemPlacer = VerticalAxis.ItemPlacer.count({ 7 }),
|
||||||
),
|
),
|
||||||
|
|||||||
+4
-1
@@ -246,6 +246,7 @@ fun PublicMessageScreenContent(
|
|||||||
ImageVideoDescription(
|
ImageVideoDescription(
|
||||||
it,
|
it,
|
||||||
accountViewModel.account.settings.defaultFileServer,
|
accountViewModel.account.settings.defaultFileServer,
|
||||||
|
isUploading = postViewModel.mediaUploadTracker.isUploading,
|
||||||
onAdd = { alt, server, sensitiveContent, mediaQuality, _ ->
|
onAdd = { alt, server, sensitiveContent, mediaQuality, _ ->
|
||||||
postViewModel.upload(alt, if (sensitiveContent) "" else null, mediaQuality, server, accountViewModel.toastManager::toast, context)
|
postViewModel.upload(alt, if (sensitiveContent) "" else null, mediaQuality, server, accountViewModel.toastManager::toast, context)
|
||||||
accountViewModel.account.settings.changeDefaultFileServer(server)
|
accountViewModel.account.settings.changeDefaultFileServer(server)
|
||||||
@@ -321,6 +322,7 @@ private fun BottomRowActions(
|
|||||||
) {
|
) {
|
||||||
SelectFromGallery(
|
SelectFromGallery(
|
||||||
isUploading = postViewModel.isUploadingImage,
|
isUploading = postViewModel.isUploadingImage,
|
||||||
|
enabled = !postViewModel.isUploadingFile,
|
||||||
tint = MaterialTheme.colorScheme.onBackground,
|
tint = MaterialTheme.colorScheme.onBackground,
|
||||||
modifier = Modifier,
|
modifier = Modifier,
|
||||||
) {
|
) {
|
||||||
@@ -328,7 +330,8 @@ private fun BottomRowActions(
|
|||||||
}
|
}
|
||||||
|
|
||||||
SelectFromFiles(
|
SelectFromFiles(
|
||||||
isUploading = postViewModel.isUploadingImage,
|
isUploading = postViewModel.isUploadingFile,
|
||||||
|
enabled = !postViewModel.isUploadingImage,
|
||||||
tint = MaterialTheme.colorScheme.onBackground,
|
tint = MaterialTheme.colorScheme.onBackground,
|
||||||
modifier = Modifier,
|
modifier = Modifier,
|
||||||
) {
|
) {
|
||||||
|
|||||||
+12
-7
@@ -45,6 +45,7 @@ import com.vitorpamplona.amethyst.service.uploads.MultiOrchestrator
|
|||||||
import com.vitorpamplona.amethyst.service.uploads.UploadOrchestrator
|
import com.vitorpamplona.amethyst.service.uploads.UploadOrchestrator
|
||||||
import com.vitorpamplona.amethyst.ui.actions.NewMessageTagger
|
import com.vitorpamplona.amethyst.ui.actions.NewMessageTagger
|
||||||
import com.vitorpamplona.amethyst.ui.actions.mediaServers.ServerName
|
import com.vitorpamplona.amethyst.ui.actions.mediaServers.ServerName
|
||||||
|
import com.vitorpamplona.amethyst.ui.actions.uploads.MediaUploadTracker
|
||||||
import com.vitorpamplona.amethyst.ui.actions.uploads.SelectedMedia
|
import com.vitorpamplona.amethyst.ui.actions.uploads.SelectedMedia
|
||||||
import com.vitorpamplona.amethyst.ui.actions.uploads.SelectedMediaProcessing
|
import com.vitorpamplona.amethyst.ui.actions.uploads.SelectedMediaProcessing
|
||||||
import com.vitorpamplona.amethyst.ui.note.creators.draftTags.DraftTagState
|
import com.vitorpamplona.amethyst.ui.note.creators.draftTags.DraftTagState
|
||||||
@@ -149,7 +150,9 @@ class NewPublicMessageViewModel :
|
|||||||
|
|
||||||
val urlPreviews = PreviewState()
|
val urlPreviews = PreviewState()
|
||||||
|
|
||||||
var isUploadingImage by mutableStateOf(false)
|
val mediaUploadTracker = MediaUploadTracker()
|
||||||
|
val isUploadingImage: Boolean get() = mediaUploadTracker.isUploadingImage
|
||||||
|
val isUploadingFile: Boolean get() = mediaUploadTracker.isUploadingFile
|
||||||
|
|
||||||
var userSuggestions: UserSuggestionState? = null
|
var userSuggestions: UserSuggestionState? = null
|
||||||
var userSuggestionsMainMessage: UserSuggestionAnchor? = null
|
var userSuggestionsMainMessage: UserSuggestionAnchor? = null
|
||||||
@@ -319,7 +322,7 @@ class NewPublicMessageViewModel :
|
|||||||
}
|
}
|
||||||
|
|
||||||
suspend fun sendPostSync() {
|
suspend fun sendPostSync() {
|
||||||
val template = createTemplate() ?: return
|
val template = createTemplate()
|
||||||
val extraNotesToBroadcast = mutableListOf<Event>()
|
val extraNotesToBroadcast = mutableListOf<Event>()
|
||||||
|
|
||||||
if (nip95attachments.isNotEmpty()) {
|
if (nip95attachments.isNotEmpty()) {
|
||||||
@@ -351,7 +354,7 @@ class NewPublicMessageViewModel :
|
|||||||
broadcast.add(it.second)
|
broadcast.add(it.second)
|
||||||
}
|
}
|
||||||
|
|
||||||
val template = createTemplate() ?: return
|
val template = createTemplate()
|
||||||
accountViewModel.account.createAndSendDraftIgnoreErrors(draftTag.current, template, broadcast)
|
accountViewModel.account.createAndSendDraftIgnoreErrors(draftTag.current, template, broadcast)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -439,7 +442,7 @@ class NewPublicMessageViewModel :
|
|||||||
viewModelScope.launch(Dispatchers.IO) {
|
viewModelScope.launch(Dispatchers.IO) {
|
||||||
val myMultiOrchestrator = multiOrchestrator ?: return@launch
|
val myMultiOrchestrator = multiOrchestrator ?: return@launch
|
||||||
|
|
||||||
isUploadingImage = true
|
mediaUploadTracker.startUpload(myMultiOrchestrator.hasNonMedia())
|
||||||
|
|
||||||
val results =
|
val results =
|
||||||
myMultiOrchestrator.upload(
|
myMultiOrchestrator.upload(
|
||||||
@@ -456,7 +459,7 @@ class NewPublicMessageViewModel :
|
|||||||
if (state.result is UploadOrchestrator.OrchestratorResult.NIP95Result) {
|
if (state.result is UploadOrchestrator.OrchestratorResult.NIP95Result) {
|
||||||
val nip95 = account.createNip95(state.result.bytes, headerInfo = state.result.fileHeader, alt, contentWarningReason)
|
val nip95 = account.createNip95(state.result.bytes, headerInfo = state.result.fileHeader, alt, contentWarningReason)
|
||||||
nip95attachments = nip95attachments + nip95
|
nip95attachments = nip95attachments + nip95
|
||||||
val note = nip95.let { it1 -> account?.consumeNip95(it1.first, it1.second) }
|
val note = nip95.let { it1 -> account.consumeNip95(it1.first, it1.second) }
|
||||||
|
|
||||||
note?.let {
|
note?.let {
|
||||||
message = message.insertUrlAtCursor("nostr:" + it.toNEvent())
|
message = message.insertUrlAtCursor("nostr:" + it.toNEvent())
|
||||||
@@ -494,7 +497,7 @@ class NewPublicMessageViewModel :
|
|||||||
onError(stringRes(context, R.string.failed_to_upload_media_no_details), errorMessages.joinToString(".\n"))
|
onError(stringRes(context, R.string.failed_to_upload_media_no_details), errorMessages.joinToString(".\n"))
|
||||||
}
|
}
|
||||||
|
|
||||||
isUploadingImage = false
|
mediaUploadTracker.finishUpload()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -523,6 +526,8 @@ class NewPublicMessageViewModel :
|
|||||||
userSuggestions?.reset()
|
userSuggestions?.reset()
|
||||||
userSuggestionsMainMessage = null
|
userSuggestionsMainMessage = null
|
||||||
|
|
||||||
|
mediaUploadTracker.finishUpload()
|
||||||
|
|
||||||
iMetaAttachments.reset()
|
iMetaAttachments.reset()
|
||||||
|
|
||||||
emojiSuggestions?.reset()
|
emojiSuggestions?.reset()
|
||||||
@@ -628,7 +633,7 @@ class NewPublicMessageViewModel :
|
|||||||
|
|
||||||
fun canPost(): Boolean =
|
fun canPost(): Boolean =
|
||||||
message.text.isNotBlank() &&
|
message.text.isNotBlank() &&
|
||||||
!isUploadingImage &&
|
!mediaUploadTracker.isUploading &&
|
||||||
!wantsInvoice &&
|
!wantsInvoice &&
|
||||||
(!wantsZapraiser || zapRaiserAmount.value != null) &&
|
(!wantsZapraiser || zapRaiserAmount.value != null) &&
|
||||||
(toUsers.text.isNotBlank()) &&
|
(toUsers.text.isNotBlank()) &&
|
||||||
|
|||||||
+1
@@ -63,6 +63,7 @@ class UserProfileFollowersUserFeedViewModel(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@OptIn(kotlinx.coroutines.FlowPreview::class)
|
||||||
val followersFlow: StateFlow<List<User>> =
|
val followersFlow: StateFlow<List<User>> =
|
||||||
account.cache
|
account.cache
|
||||||
.observeEvents(followerFilter)
|
.observeEvents(followerFilter)
|
||||||
|
|||||||
+1
@@ -55,6 +55,7 @@ class UserProfileFollowsUserFeedViewModel(
|
|||||||
return LocalCache.load(nonHiddenFollows).sortedWith(sortingModel)
|
return LocalCache.load(nonHiddenFollows).sortedWith(sortingModel)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@OptIn(kotlinx.coroutines.FlowPreview::class)
|
||||||
val followsFlow: StateFlow<List<User>> =
|
val followsFlow: StateFlow<List<User>> =
|
||||||
contactList
|
contactList
|
||||||
.flow()
|
.flow()
|
||||||
|
|||||||
+7
-6
@@ -77,12 +77,13 @@ class UserProfileGalleryFeedFilter(
|
|||||||
val noteEvent = it.event
|
val noteEvent = it.event
|
||||||
return (
|
return (
|
||||||
(
|
(
|
||||||
it.event?.pubKey == user.pubkeyHex && (
|
it.event?.pubKey == user.pubkeyHex &&
|
||||||
noteEvent is PictureEvent ||
|
(
|
||||||
noteEvent is RegularVideoEvent ||
|
noteEvent is PictureEvent ||
|
||||||
(noteEvent is ReplaceableVideoEvent && it is AddressableNote) ||
|
noteEvent is RegularVideoEvent ||
|
||||||
(noteEvent is ProfileGalleryEntryEvent && noteEvent.hasUrl() && noteEvent.hasFromEvent())
|
(noteEvent is ReplaceableVideoEvent && it is AddressableNote) ||
|
||||||
)
|
(noteEvent is ProfileGalleryEntryEvent && noteEvent.hasUrl() && noteEvent.hasFromEvent())
|
||||||
|
)
|
||||||
) // && noteEvent.isOneOf(SUPPORTED_VIDEO_FEED_MIME_TYPES_SET))
|
) // && noteEvent.isOneOf(SUPPORTED_VIDEO_FEED_MIME_TYPES_SET))
|
||||||
) &&
|
) &&
|
||||||
params.match(noteEvent, it.relays) &&
|
params.match(noteEvent, it.relays) &&
|
||||||
|
|||||||
+1
-1
@@ -56,7 +56,7 @@ fun WatchApp(
|
|||||||
|
|
||||||
LaunchedEffect(key1 = appState) {
|
LaunchedEffect(key1 = appState) {
|
||||||
withContext(Dispatchers.IO) {
|
withContext(Dispatchers.IO) {
|
||||||
(appState?.note?.event as? AppDefinitionEvent)?.appMetaData()?.let { metaData ->
|
(appState.note.event as? AppDefinitionEvent)?.appMetaData()?.let { metaData ->
|
||||||
metaData.picture?.ifBlank { null }?.let { newLogo ->
|
metaData.picture?.ifBlank { null }?.let { newLogo ->
|
||||||
if (newLogo != appLogo) appLogo = newLogo
|
if (newLogo != appLogo) appLogo = newLogo
|
||||||
}
|
}
|
||||||
|
|||||||
+1
@@ -112,6 +112,7 @@ class UserProfileZapsViewModel(
|
|||||||
return results.map { (user, amount) -> ZapAmount(user, amount) }.sortedWith(sortingModel)
|
return results.map { (user, amount) -> ZapAmount(user, amount) }.sortedWith(sortingModel)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@OptIn(kotlinx.coroutines.FlowPreview::class)
|
||||||
val receivedZapAmountsByUser: StateFlow<List<ZapAmount>> =
|
val receivedZapAmountsByUser: StateFlow<List<ZapAmount>> =
|
||||||
account.cache
|
account.cache
|
||||||
.observeEvents(zapsToUser)
|
.observeEvents(zapsToUser)
|
||||||
|
|||||||
+15
-7
@@ -188,6 +188,14 @@ fun MappedAllRelayListView(
|
|||||||
val proxyRelays by proxyViewModel.relays.collectAsStateWithLifecycle()
|
val proxyRelays by proxyViewModel.relays.collectAsStateWithLifecycle()
|
||||||
val relayFeedsFeedState by relayFeedsViewModel.relays.collectAsStateWithLifecycle()
|
val relayFeedsFeedState by relayFeedsViewModel.relays.collectAsStateWithLifecycle()
|
||||||
|
|
||||||
|
val outboxCounts by nip65ViewModel.homeCountResults.collectAsStateWithLifecycle()
|
||||||
|
val inboxCounts by nip65ViewModel.notifCountResults.collectAsStateWithLifecycle()
|
||||||
|
val dmCounts by dmViewModel.countResults.collectAsStateWithLifecycle()
|
||||||
|
val privateHomeCounts by privateOutboxViewModel.countResults.collectAsStateWithLifecycle()
|
||||||
|
val proxyCounts by proxyViewModel.countResults.collectAsStateWithLifecycle()
|
||||||
|
val indexerCounts by indexerViewModel.countResults.collectAsStateWithLifecycle()
|
||||||
|
val searchCounts by searchViewModel.countResults.collectAsStateWithLifecycle()
|
||||||
|
|
||||||
Scaffold(
|
Scaffold(
|
||||||
topBar = {
|
topBar = {
|
||||||
SavingTopBar(
|
SavingTopBar(
|
||||||
@@ -261,7 +269,7 @@ fun MappedAllRelayListView(
|
|||||||
SettingsCategoryFirstModifier,
|
SettingsCategoryFirstModifier,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
renderNip65HomeItems(homeFeedState, nip65ViewModel, accountViewModel, nav)
|
renderNip65HomeItems(homeFeedState, nip65ViewModel, accountViewModel, nav, outboxCounts)
|
||||||
|
|
||||||
item {
|
item {
|
||||||
SettingsCategory(
|
SettingsCategory(
|
||||||
@@ -270,7 +278,7 @@ fun MappedAllRelayListView(
|
|||||||
SettingsCategorySpacingModifier,
|
SettingsCategorySpacingModifier,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
renderNip65NotifItems(notifFeedState, nip65ViewModel, accountViewModel, nav)
|
renderNip65NotifItems(notifFeedState, nip65ViewModel, accountViewModel, nav, inboxCounts)
|
||||||
|
|
||||||
item {
|
item {
|
||||||
SettingsCategoryWithButton(
|
SettingsCategoryWithButton(
|
||||||
@@ -282,7 +290,7 @@ fun MappedAllRelayListView(
|
|||||||
},
|
},
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
renderDMItems(dmFeedState, dmViewModel, accountViewModel, nav)
|
renderDMItems(dmFeedState, dmViewModel, accountViewModel, nav, dmCounts)
|
||||||
|
|
||||||
item {
|
item {
|
||||||
SettingsCategory(
|
SettingsCategory(
|
||||||
@@ -291,7 +299,7 @@ fun MappedAllRelayListView(
|
|||||||
SettingsCategorySpacingModifier,
|
SettingsCategorySpacingModifier,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
renderPrivateOutboxItems(privateOutboxFeedState, privateOutboxViewModel, accountViewModel, nav)
|
renderPrivateOutboxItems(privateOutboxFeedState, privateOutboxViewModel, accountViewModel, nav, privateHomeCounts)
|
||||||
|
|
||||||
item {
|
item {
|
||||||
SettingsCategory(
|
SettingsCategory(
|
||||||
@@ -300,7 +308,7 @@ fun MappedAllRelayListView(
|
|||||||
SettingsCategorySpacingModifier,
|
SettingsCategorySpacingModifier,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
renderProxyItems(proxyRelays, proxyViewModel, accountViewModel, nav)
|
renderProxyItems(proxyRelays, proxyViewModel, accountViewModel, nav, proxyCounts)
|
||||||
|
|
||||||
item {
|
item {
|
||||||
SettingsCategory(
|
SettingsCategory(
|
||||||
@@ -320,7 +328,7 @@ fun MappedAllRelayListView(
|
|||||||
ResetIndexerRelays(indexerViewModel)
|
ResetIndexerRelays(indexerViewModel)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
renderIndexerItems(indexerRelays, indexerViewModel, accountViewModel, nav)
|
renderIndexerItems(indexerRelays, indexerViewModel, accountViewModel, nav, indexerCounts)
|
||||||
|
|
||||||
item {
|
item {
|
||||||
SettingsCategoryWithButton(
|
SettingsCategoryWithButton(
|
||||||
@@ -331,7 +339,7 @@ fun MappedAllRelayListView(
|
|||||||
ResetSearchRelays(searchViewModel)
|
ResetSearchRelays(searchViewModel)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
renderSearchItems(searchFeedState, searchViewModel, accountViewModel, nav)
|
renderSearchItems(searchFeedState, searchViewModel, accountViewModel, nav, searchCounts)
|
||||||
|
|
||||||
item {
|
item {
|
||||||
SettingsCategory(
|
SettingsCategory(
|
||||||
|
|||||||
+2
-2
@@ -47,6 +47,7 @@ import androidx.compose.material.icons.automirrored.filled.Feed
|
|||||||
import androidx.compose.material.icons.automirrored.filled.Label
|
import androidx.compose.material.icons.automirrored.filled.Label
|
||||||
import androidx.compose.material.icons.automirrored.filled.List
|
import androidx.compose.material.icons.automirrored.filled.List
|
||||||
import androidx.compose.material.icons.automirrored.filled.Message
|
import androidx.compose.material.icons.automirrored.filled.Message
|
||||||
|
import androidx.compose.material.icons.automirrored.filled.Send
|
||||||
import androidx.compose.material.icons.filled.AttachMoney
|
import androidx.compose.material.icons.filled.AttachMoney
|
||||||
import androidx.compose.material.icons.filled.Bolt
|
import androidx.compose.material.icons.filled.Bolt
|
||||||
import androidx.compose.material.icons.filled.Code
|
import androidx.compose.material.icons.filled.Code
|
||||||
@@ -61,7 +62,6 @@ import androidx.compose.material.icons.filled.Language
|
|||||||
import androidx.compose.material.icons.filled.Lock
|
import androidx.compose.material.icons.filled.Lock
|
||||||
import androidx.compose.material.icons.filled.Payment
|
import androidx.compose.material.icons.filled.Payment
|
||||||
import androidx.compose.material.icons.filled.PrivacyTip
|
import androidx.compose.material.icons.filled.PrivacyTip
|
||||||
import androidx.compose.material.icons.filled.Send
|
|
||||||
import androidx.compose.material.icons.filled.Storage
|
import androidx.compose.material.icons.filled.Storage
|
||||||
import androidx.compose.material.icons.filled.Tag
|
import androidx.compose.material.icons.filled.Tag
|
||||||
import androidx.compose.material.icons.filled.Topic
|
import androidx.compose.material.icons.filled.Topic
|
||||||
@@ -931,7 +931,7 @@ private fun OutboxEventsCard(eventIds: Set<HexKey>) {
|
|||||||
horizontalArrangement = Arrangement.spacedBy(4.dp),
|
horizontalArrangement = Arrangement.spacedBy(4.dp),
|
||||||
) {
|
) {
|
||||||
Icon(
|
Icon(
|
||||||
imageVector = Icons.Default.Send,
|
imageVector = Icons.AutoMirrored.Filled.Send,
|
||||||
contentDescription = null,
|
contentDescription = null,
|
||||||
modifier = Modifier.size(12.dp),
|
modifier = Modifier.size(12.dp),
|
||||||
tint = MaterialTheme.colorScheme.onTertiaryContainer,
|
tint = MaterialTheme.colorScheme.onTertiaryContainer,
|
||||||
|
|||||||
+6
@@ -63,6 +63,7 @@ fun BasicRelaySetupInfoClickableRow(
|
|||||||
onClick: () -> Unit,
|
onClick: () -> Unit,
|
||||||
nip11CachedRetriever: Nip11CachedRetriever,
|
nip11CachedRetriever: Nip11CachedRetriever,
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
|
countResult: RelayCountResult? = null,
|
||||||
accountViewModel: AccountViewModel,
|
accountViewModel: AccountViewModel,
|
||||||
nav: INav,
|
nav: INav,
|
||||||
) {
|
) {
|
||||||
@@ -104,6 +105,11 @@ fun BasicRelaySetupInfoClickableRow(
|
|||||||
|
|
||||||
UsedBy(item, accountViewModel, nav)
|
UsedBy(item, accountViewModel, nav)
|
||||||
|
|
||||||
|
RelayEventCountRow(
|
||||||
|
countResult = countResult,
|
||||||
|
modifier = ReactionRowHeightChatMaxWidth,
|
||||||
|
)
|
||||||
|
|
||||||
RelayStatusRow(
|
RelayStatusRow(
|
||||||
item = item,
|
item = item,
|
||||||
onClick = onClick,
|
onClick = onClick,
|
||||||
|
|||||||
+2
@@ -32,6 +32,7 @@ fun BasicRelaySetupInfoDialog(
|
|||||||
item: BasicRelaySetupInfo,
|
item: BasicRelaySetupInfo,
|
||||||
nip11CachedRetriever: Nip11CachedRetriever,
|
nip11CachedRetriever: Nip11CachedRetriever,
|
||||||
onDelete: ((BasicRelaySetupInfo) -> Unit)?,
|
onDelete: ((BasicRelaySetupInfo) -> Unit)?,
|
||||||
|
countResult: RelayCountResult? = null,
|
||||||
accountViewModel: AccountViewModel,
|
accountViewModel: AccountViewModel,
|
||||||
nav: INav,
|
nav: INav,
|
||||||
) {
|
) {
|
||||||
@@ -43,6 +44,7 @@ fun BasicRelaySetupInfoDialog(
|
|||||||
onClick = { nav.nav(Route.RelayInfo(item.relay.url)) },
|
onClick = { nav.nav(Route.RelayInfo(item.relay.url)) },
|
||||||
nip11CachedRetriever = nip11CachedRetriever,
|
nip11CachedRetriever = nip11CachedRetriever,
|
||||||
modifier = HalfVertPadding,
|
modifier = HalfVertPadding,
|
||||||
|
countResult = countResult,
|
||||||
accountViewModel = accountViewModel,
|
accountViewModel = accountViewModel,
|
||||||
nav = nav,
|
nav = nav,
|
||||||
)
|
)
|
||||||
|
|||||||
+41
@@ -26,6 +26,7 @@ import com.vitorpamplona.amethyst.Amethyst
|
|||||||
import com.vitorpamplona.amethyst.model.Account
|
import com.vitorpamplona.amethyst.model.Account
|
||||||
import com.vitorpamplona.amethyst.service.replace
|
import com.vitorpamplona.amethyst.service.replace
|
||||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
||||||
|
import com.vitorpamplona.quartz.nip01Core.relay.client.accessories.queryCountSuspend
|
||||||
import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl
|
import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
@@ -40,6 +41,9 @@ abstract class BasicRelaySetupInfoModel : ViewModel() {
|
|||||||
private val _relays = MutableStateFlow<List<BasicRelaySetupInfo>>(emptyList())
|
private val _relays = MutableStateFlow<List<BasicRelaySetupInfo>>(emptyList())
|
||||||
val relays = _relays.asStateFlow()
|
val relays = _relays.asStateFlow()
|
||||||
|
|
||||||
|
private val _countResults = MutableStateFlow<Map<NormalizedRelayUrl, RelayCountResult>>(emptyMap())
|
||||||
|
val countResults = _countResults.asStateFlow()
|
||||||
|
|
||||||
var hasModified = false
|
var hasModified = false
|
||||||
|
|
||||||
fun init(accountViewModel: AccountViewModel) {
|
fun init(accountViewModel: AccountViewModel) {
|
||||||
@@ -50,12 +54,15 @@ abstract class BasicRelaySetupInfoModel : ViewModel() {
|
|||||||
fun load() {
|
fun load() {
|
||||||
clear()
|
clear()
|
||||||
loadRelayDocuments()
|
loadRelayDocuments()
|
||||||
|
loadCounts()
|
||||||
}
|
}
|
||||||
|
|
||||||
abstract fun getRelayList(): List<NormalizedRelayUrl>?
|
abstract fun getRelayList(): List<NormalizedRelayUrl>?
|
||||||
|
|
||||||
abstract suspend fun saveRelayList(urlList: List<NormalizedRelayUrl>)
|
abstract suspend fun saveRelayList(urlList: List<NormalizedRelayUrl>)
|
||||||
|
|
||||||
|
open fun countFilters(relayUrl: NormalizedRelayUrl): List<CountFilter> = emptyList()
|
||||||
|
|
||||||
fun create() {
|
fun create() {
|
||||||
if (hasModified) {
|
if (hasModified) {
|
||||||
accountViewModel.launchSigner {
|
accountViewModel.launchSigner {
|
||||||
@@ -79,6 +86,40 @@ abstract class BasicRelaySetupInfoModel : ViewModel() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun loadCounts() {
|
||||||
|
_countResults.value = emptyMap()
|
||||||
|
|
||||||
|
val client = account.client
|
||||||
|
val relayList = _relays.value
|
||||||
|
if (relayList.isEmpty()) return
|
||||||
|
|
||||||
|
relayList.forEach { item ->
|
||||||
|
val filters = countFilters(item.relay)
|
||||||
|
if (filters.isEmpty()) return@forEach
|
||||||
|
|
||||||
|
filters.forEach { countFilter ->
|
||||||
|
viewModelScope.launch(Dispatchers.IO) {
|
||||||
|
val result = client.queryCountSuspend(item.relay, countFilter.filter)
|
||||||
|
if (result != null) {
|
||||||
|
_countResults.update { currentMap ->
|
||||||
|
val current = currentMap[item.relay] ?: RelayCountResult()
|
||||||
|
val entries = current.counts.toMutableList()
|
||||||
|
val newEntry =
|
||||||
|
RelayCountResult.CountEntry(
|
||||||
|
label = countFilter.label,
|
||||||
|
count = result.count,
|
||||||
|
approximate = result.approximate,
|
||||||
|
)
|
||||||
|
val existing = entries.indexOfFirst { it.label == countFilter.label }
|
||||||
|
if (existing >= 0) entries[existing] = newEntry else entries.add(newEntry)
|
||||||
|
currentMap + (item.relay to RelayCountResult(entries))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
open fun relayListBuilder(): List<BasicRelaySetupInfo> {
|
open fun relayListBuilder(): List<BasicRelaySetupInfo> {
|
||||||
val relayList = getRelayList() ?: emptyList()
|
val relayList = getRelayList() ?: emptyList()
|
||||||
|
|
||||||
|
|||||||
+105
@@ -0,0 +1,105 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (c) 2025 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.loggedIn.relays.common
|
||||||
|
|
||||||
|
import androidx.compose.foundation.background
|
||||||
|
import androidx.compose.foundation.border
|
||||||
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
|
import androidx.compose.foundation.layout.Row
|
||||||
|
import androidx.compose.foundation.layout.Spacer
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.foundation.layout.width
|
||||||
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
|
import androidx.compose.material.icons.Icons
|
||||||
|
import androidx.compose.material.icons.filled.Storage
|
||||||
|
import androidx.compose.material3.Icon
|
||||||
|
import androidx.compose.material3.MaterialTheme
|
||||||
|
import androidx.compose.material3.Text
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.draw.clip
|
||||||
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import com.vitorpamplona.amethyst.R
|
||||||
|
import com.vitorpamplona.amethyst.service.countToHumanReadable
|
||||||
|
import com.vitorpamplona.amethyst.ui.stringRes
|
||||||
|
import com.vitorpamplona.amethyst.ui.theme.Font10SP
|
||||||
|
import com.vitorpamplona.amethyst.ui.theme.Size10Modifier
|
||||||
|
import com.vitorpamplona.amethyst.ui.theme.allGoodColor
|
||||||
|
|
||||||
|
private val PillShape = RoundedCornerShape(12.dp)
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun RelayEventCountRow(
|
||||||
|
countResult: RelayCountResult?,
|
||||||
|
modifier: Modifier,
|
||||||
|
) {
|
||||||
|
if (countResult == null || countResult.counts.isEmpty()) return
|
||||||
|
|
||||||
|
val pillColor = MaterialTheme.colorScheme.allGoodColor
|
||||||
|
|
||||||
|
Row(
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
horizontalArrangement = Arrangement.Start,
|
||||||
|
modifier = modifier,
|
||||||
|
) {
|
||||||
|
countResult.counts.forEachIndexed { index, entry ->
|
||||||
|
if (index > 0) {
|
||||||
|
Spacer(modifier = Modifier.width(6.dp))
|
||||||
|
}
|
||||||
|
|
||||||
|
val countText =
|
||||||
|
if (entry.approximate) {
|
||||||
|
"~${countToHumanReadable(entry.count, stringRes(entry.label))}"
|
||||||
|
} else {
|
||||||
|
countToHumanReadable(entry.count, stringRes(entry.label))
|
||||||
|
}
|
||||||
|
|
||||||
|
Row(
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
modifier =
|
||||||
|
Modifier
|
||||||
|
.clip(PillShape)
|
||||||
|
.border(width = 1.dp, color = pillColor.copy(alpha = 0.4f), shape = PillShape)
|
||||||
|
.background(pillColor.copy(alpha = 0.1f))
|
||||||
|
.padding(horizontal = 6.dp, vertical = 2.dp),
|
||||||
|
) {
|
||||||
|
Icon(
|
||||||
|
imageVector = Icons.Default.Storage,
|
||||||
|
contentDescription = stringRes(R.string.relay_event_count),
|
||||||
|
modifier = Size10Modifier,
|
||||||
|
tint = pillColor,
|
||||||
|
)
|
||||||
|
|
||||||
|
Spacer(modifier = Modifier.width(3.dp))
|
||||||
|
|
||||||
|
Text(
|
||||||
|
text = countText,
|
||||||
|
maxLines = 1,
|
||||||
|
fontSize = Font10SP,
|
||||||
|
fontWeight = FontWeight.Medium,
|
||||||
|
color = pillColor,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+41
@@ -0,0 +1,41 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (c) 2025 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.loggedIn.relays.common
|
||||||
|
|
||||||
|
import androidx.compose.runtime.Immutable
|
||||||
|
import com.vitorpamplona.quartz.nip01Core.relay.filters.Filter
|
||||||
|
|
||||||
|
@Immutable
|
||||||
|
data class RelayCountResult(
|
||||||
|
val counts: List<CountEntry> = emptyList(),
|
||||||
|
) {
|
||||||
|
@Immutable
|
||||||
|
data class CountEntry(
|
||||||
|
val label: Int,
|
||||||
|
val count: Int,
|
||||||
|
val approximate: Boolean = false,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
data class CountFilter(
|
||||||
|
val label: Int,
|
||||||
|
val filter: Filter,
|
||||||
|
)
|
||||||
+4
@@ -35,10 +35,12 @@ import com.vitorpamplona.amethyst.ui.navigation.navs.rememberExtendedNav
|
|||||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
||||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.common.BasicRelaySetupInfo
|
import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.common.BasicRelaySetupInfo
|
||||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.common.BasicRelaySetupInfoDialog
|
import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.common.BasicRelaySetupInfoDialog
|
||||||
|
import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.common.RelayCountResult
|
||||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.common.RelayUrlEditField
|
import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.common.RelayUrlEditField
|
||||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.common.relaySetupInfoBuilder
|
import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.common.relaySetupInfoBuilder
|
||||||
import com.vitorpamplona.amethyst.ui.theme.FeedPadding
|
import com.vitorpamplona.amethyst.ui.theme.FeedPadding
|
||||||
import com.vitorpamplona.amethyst.ui.theme.StdVertSpacer
|
import com.vitorpamplona.amethyst.ui.theme.StdVertSpacer
|
||||||
|
import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun DMRelayList(
|
fun DMRelayList(
|
||||||
@@ -64,12 +66,14 @@ fun LazyListScope.renderDMItems(
|
|||||||
postViewModel: DMRelayListViewModel,
|
postViewModel: DMRelayListViewModel,
|
||||||
accountViewModel: AccountViewModel,
|
accountViewModel: AccountViewModel,
|
||||||
nav: INav,
|
nav: INav,
|
||||||
|
countResults: Map<NormalizedRelayUrl, RelayCountResult> = emptyMap(),
|
||||||
) {
|
) {
|
||||||
itemsIndexed(feedState, key = { _, item -> "DM" + item.relay.url }) { index, item ->
|
itemsIndexed(feedState, key = { _, item -> "DM" + item.relay.url }) { index, item ->
|
||||||
BasicRelaySetupInfoDialog(
|
BasicRelaySetupInfoDialog(
|
||||||
item,
|
item,
|
||||||
onDelete = { postViewModel.deleteRelay(item) },
|
onDelete = { postViewModel.deleteRelay(item) },
|
||||||
nip11CachedRetriever = Amethyst.instance.nip11Cache,
|
nip11CachedRetriever = Amethyst.instance.nip11Cache,
|
||||||
|
countResult = countResults[item.relay],
|
||||||
accountViewModel = accountViewModel,
|
accountViewModel = accountViewModel,
|
||||||
nav = nav,
|
nav = nav,
|
||||||
)
|
)
|
||||||
|
|||||||
+17
@@ -21,8 +21,13 @@
|
|||||||
package com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.dm
|
package com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.dm
|
||||||
|
|
||||||
import androidx.compose.runtime.Stable
|
import androidx.compose.runtime.Stable
|
||||||
|
import com.vitorpamplona.amethyst.R
|
||||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.common.BasicRelaySetupInfoModel
|
import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.common.BasicRelaySetupInfoModel
|
||||||
|
import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.common.CountFilter
|
||||||
|
import com.vitorpamplona.quartz.nip01Core.relay.filters.Filter
|
||||||
import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl
|
import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl
|
||||||
|
import com.vitorpamplona.quartz.nip04Dm.messages.PrivateDmEvent
|
||||||
|
import com.vitorpamplona.quartz.nip59Giftwrap.wraps.GiftWrapEvent
|
||||||
|
|
||||||
@Stable
|
@Stable
|
||||||
class DMRelayListViewModel : BasicRelaySetupInfoModel() {
|
class DMRelayListViewModel : BasicRelaySetupInfoModel() {
|
||||||
@@ -31,4 +36,16 @@ class DMRelayListViewModel : BasicRelaySetupInfoModel() {
|
|||||||
override suspend fun saveRelayList(urlList: List<NormalizedRelayUrl>) {
|
override suspend fun saveRelayList(urlList: List<NormalizedRelayUrl>) {
|
||||||
account.saveDMRelayList(urlList)
|
account.saveDMRelayList(urlList)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun countFilters(relayUrl: NormalizedRelayUrl): List<CountFilter> =
|
||||||
|
listOf(
|
||||||
|
CountFilter(
|
||||||
|
label = R.string.dms,
|
||||||
|
filter =
|
||||||
|
Filter(
|
||||||
|
kinds = listOf(GiftWrapEvent.KIND, PrivateDmEvent.KIND),
|
||||||
|
tags = mapOf("p" to listOf(account.pubKey)),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
+4
@@ -35,10 +35,12 @@ import com.vitorpamplona.amethyst.ui.navigation.navs.rememberExtendedNav
|
|||||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
||||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.common.BasicRelaySetupInfo
|
import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.common.BasicRelaySetupInfo
|
||||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.common.BasicRelaySetupInfoDialog
|
import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.common.BasicRelaySetupInfoDialog
|
||||||
|
import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.common.RelayCountResult
|
||||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.common.RelayUrlEditField
|
import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.common.RelayUrlEditField
|
||||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.common.relaySetupInfoBuilder
|
import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.common.relaySetupInfoBuilder
|
||||||
import com.vitorpamplona.amethyst.ui.theme.FeedPadding
|
import com.vitorpamplona.amethyst.ui.theme.FeedPadding
|
||||||
import com.vitorpamplona.amethyst.ui.theme.StdVertSpacer
|
import com.vitorpamplona.amethyst.ui.theme.StdVertSpacer
|
||||||
|
import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun IndexerRelayList(
|
fun IndexerRelayList(
|
||||||
@@ -65,12 +67,14 @@ fun LazyListScope.renderIndexerItems(
|
|||||||
postViewModel: IndexerRelayListViewModel,
|
postViewModel: IndexerRelayListViewModel,
|
||||||
accountViewModel: AccountViewModel,
|
accountViewModel: AccountViewModel,
|
||||||
nav: INav,
|
nav: INav,
|
||||||
|
countResults: Map<NormalizedRelayUrl, RelayCountResult> = emptyMap(),
|
||||||
) {
|
) {
|
||||||
itemsIndexed(feedState, key = { _, item -> "Indexer" + item.relay.url }) { index, item ->
|
itemsIndexed(feedState, key = { _, item -> "Indexer" + item.relay.url }) { index, item ->
|
||||||
BasicRelaySetupInfoDialog(
|
BasicRelaySetupInfoDialog(
|
||||||
item,
|
item,
|
||||||
onDelete = { postViewModel.deleteRelay(item) },
|
onDelete = { postViewModel.deleteRelay(item) },
|
||||||
nip11CachedRetriever = Amethyst.instance.nip11Cache,
|
nip11CachedRetriever = Amethyst.instance.nip11Cache,
|
||||||
|
countResult = countResults[item.relay],
|
||||||
accountViewModel = accountViewModel,
|
accountViewModel = accountViewModel,
|
||||||
nav = nav,
|
nav = nav,
|
||||||
)
|
)
|
||||||
|
|||||||
+17
@@ -21,8 +21,13 @@
|
|||||||
package com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.indexer
|
package com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.indexer
|
||||||
|
|
||||||
import androidx.compose.runtime.Stable
|
import androidx.compose.runtime.Stable
|
||||||
|
import com.vitorpamplona.amethyst.R
|
||||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.common.BasicRelaySetupInfoModel
|
import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.common.BasicRelaySetupInfoModel
|
||||||
|
import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.common.CountFilter
|
||||||
|
import com.vitorpamplona.quartz.nip01Core.metadata.MetadataEvent
|
||||||
|
import com.vitorpamplona.quartz.nip01Core.relay.filters.Filter
|
||||||
import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl
|
import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl
|
||||||
|
import com.vitorpamplona.quartz.nip65RelayList.AdvertisedRelayListEvent
|
||||||
|
|
||||||
@Stable
|
@Stable
|
||||||
class IndexerRelayListViewModel : BasicRelaySetupInfoModel() {
|
class IndexerRelayListViewModel : BasicRelaySetupInfoModel() {
|
||||||
@@ -33,4 +38,16 @@ class IndexerRelayListViewModel : BasicRelaySetupInfoModel() {
|
|||||||
override suspend fun saveRelayList(urlList: List<NormalizedRelayUrl>) {
|
override suspend fun saveRelayList(urlList: List<NormalizedRelayUrl>) {
|
||||||
account.saveIndexerRelayList(urlList)
|
account.saveIndexerRelayList(urlList)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun countFilters(relayUrl: NormalizedRelayUrl): List<CountFilter> =
|
||||||
|
listOf(
|
||||||
|
CountFilter(
|
||||||
|
label = R.string.profiles,
|
||||||
|
filter = Filter(kinds = listOf(MetadataEvent.KIND)),
|
||||||
|
),
|
||||||
|
CountFilter(
|
||||||
|
label = R.string.relay_settings_lower,
|
||||||
|
filter = Filter(kinds = listOf(AdvertisedRelayListEvent.KIND)),
|
||||||
|
),
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
+4
@@ -35,10 +35,12 @@ import com.vitorpamplona.amethyst.ui.navigation.navs.rememberExtendedNav
|
|||||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
||||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.common.BasicRelaySetupInfo
|
import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.common.BasicRelaySetupInfo
|
||||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.common.BasicRelaySetupInfoDialog
|
import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.common.BasicRelaySetupInfoDialog
|
||||||
|
import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.common.RelayCountResult
|
||||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.common.RelayUrlEditField
|
import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.common.RelayUrlEditField
|
||||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.common.relaySetupInfoBuilder
|
import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.common.relaySetupInfoBuilder
|
||||||
import com.vitorpamplona.amethyst.ui.theme.FeedPadding
|
import com.vitorpamplona.amethyst.ui.theme.FeedPadding
|
||||||
import com.vitorpamplona.amethyst.ui.theme.StdVertSpacer
|
import com.vitorpamplona.amethyst.ui.theme.StdVertSpacer
|
||||||
|
import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun PrivateOutboxRelayList(
|
fun PrivateOutboxRelayList(
|
||||||
@@ -64,12 +66,14 @@ fun LazyListScope.renderPrivateOutboxItems(
|
|||||||
postViewModel: PrivateOutboxRelayListViewModel,
|
postViewModel: PrivateOutboxRelayListViewModel,
|
||||||
accountViewModel: AccountViewModel,
|
accountViewModel: AccountViewModel,
|
||||||
nav: INav,
|
nav: INav,
|
||||||
|
countResults: Map<NormalizedRelayUrl, RelayCountResult> = emptyMap(),
|
||||||
) {
|
) {
|
||||||
itemsIndexed(feedState, key = { _, item -> "Outbox" + item.relay.url }) { index, item ->
|
itemsIndexed(feedState, key = { _, item -> "Outbox" + item.relay.url }) { index, item ->
|
||||||
BasicRelaySetupInfoDialog(
|
BasicRelaySetupInfoDialog(
|
||||||
item,
|
item,
|
||||||
onDelete = { postViewModel.deleteRelay(item) },
|
onDelete = { postViewModel.deleteRelay(item) },
|
||||||
nip11CachedRetriever = Amethyst.instance.nip11Cache,
|
nip11CachedRetriever = Amethyst.instance.nip11Cache,
|
||||||
|
countResult = countResults[item.relay],
|
||||||
accountViewModel = accountViewModel,
|
accountViewModel = accountViewModel,
|
||||||
nav = nav,
|
nav = nav,
|
||||||
)
|
)
|
||||||
|
|||||||
+11
@@ -21,7 +21,10 @@
|
|||||||
package com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.nip37
|
package com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.nip37
|
||||||
|
|
||||||
import androidx.compose.runtime.Stable
|
import androidx.compose.runtime.Stable
|
||||||
|
import com.vitorpamplona.amethyst.R
|
||||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.common.BasicRelaySetupInfoModel
|
import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.common.BasicRelaySetupInfoModel
|
||||||
|
import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.common.CountFilter
|
||||||
|
import com.vitorpamplona.quartz.nip01Core.relay.filters.Filter
|
||||||
import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl
|
import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl
|
||||||
|
|
||||||
@Stable
|
@Stable
|
||||||
@@ -33,4 +36,12 @@ class PrivateOutboxRelayListViewModel : BasicRelaySetupInfoModel() {
|
|||||||
override suspend fun saveRelayList(urlList: List<NormalizedRelayUrl>) {
|
override suspend fun saveRelayList(urlList: List<NormalizedRelayUrl>) {
|
||||||
account.savePrivateOutboxRelayList(urlList)
|
account.savePrivateOutboxRelayList(urlList)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun countFilters(relayUrl: NormalizedRelayUrl): List<CountFilter> =
|
||||||
|
listOf(
|
||||||
|
CountFilter(
|
||||||
|
label = R.string.events,
|
||||||
|
filter = Filter(authors = listOf(account.pubKey)),
|
||||||
|
),
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
+6
@@ -35,10 +35,12 @@ import com.vitorpamplona.amethyst.ui.navigation.navs.rememberExtendedNav
|
|||||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
||||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.common.BasicRelaySetupInfo
|
import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.common.BasicRelaySetupInfo
|
||||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.common.BasicRelaySetupInfoDialog
|
import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.common.BasicRelaySetupInfoDialog
|
||||||
|
import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.common.RelayCountResult
|
||||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.common.RelayUrlEditField
|
import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.common.RelayUrlEditField
|
||||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.common.relaySetupInfoBuilder
|
import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.common.relaySetupInfoBuilder
|
||||||
import com.vitorpamplona.amethyst.ui.theme.FeedPadding
|
import com.vitorpamplona.amethyst.ui.theme.FeedPadding
|
||||||
import com.vitorpamplona.amethyst.ui.theme.StdVertSpacer
|
import com.vitorpamplona.amethyst.ui.theme.StdVertSpacer
|
||||||
|
import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun Nip65RelayList(
|
fun Nip65RelayList(
|
||||||
@@ -107,12 +109,14 @@ fun LazyListScope.renderNip65HomeItems(
|
|||||||
postViewModel: Nip65RelayListViewModel,
|
postViewModel: Nip65RelayListViewModel,
|
||||||
accountViewModel: AccountViewModel,
|
accountViewModel: AccountViewModel,
|
||||||
nav: INav,
|
nav: INav,
|
||||||
|
countResults: Map<NormalizedRelayUrl, RelayCountResult> = emptyMap(),
|
||||||
) {
|
) {
|
||||||
itemsIndexed(feedState, key = { _, item -> "Nip65Home" + item.relay.url }) { index, item ->
|
itemsIndexed(feedState, key = { _, item -> "Nip65Home" + item.relay.url }) { index, item ->
|
||||||
BasicRelaySetupInfoDialog(
|
BasicRelaySetupInfoDialog(
|
||||||
item,
|
item,
|
||||||
onDelete = { postViewModel.deleteHomeRelay(item) },
|
onDelete = { postViewModel.deleteHomeRelay(item) },
|
||||||
nip11CachedRetriever = Amethyst.instance.nip11Cache,
|
nip11CachedRetriever = Amethyst.instance.nip11Cache,
|
||||||
|
countResult = countResults[item.relay],
|
||||||
accountViewModel = accountViewModel,
|
accountViewModel = accountViewModel,
|
||||||
nav = nav,
|
nav = nav,
|
||||||
)
|
)
|
||||||
@@ -133,12 +137,14 @@ fun LazyListScope.renderNip65NotifItems(
|
|||||||
postViewModel: Nip65RelayListViewModel,
|
postViewModel: Nip65RelayListViewModel,
|
||||||
accountViewModel: AccountViewModel,
|
accountViewModel: AccountViewModel,
|
||||||
nav: INav,
|
nav: INav,
|
||||||
|
countResults: Map<NormalizedRelayUrl, RelayCountResult> = emptyMap(),
|
||||||
) {
|
) {
|
||||||
itemsIndexed(feedState, key = { _, item -> "Nip65Notif" + item.relay.url }) { index, item ->
|
itemsIndexed(feedState, key = { _, item -> "Nip65Notif" + item.relay.url }) { index, item ->
|
||||||
BasicRelaySetupInfoDialog(
|
BasicRelaySetupInfoDialog(
|
||||||
item,
|
item,
|
||||||
onDelete = { postViewModel.deleteNotifRelay(item) },
|
onDelete = { postViewModel.deleteNotifRelay(item) },
|
||||||
nip11CachedRetriever = Amethyst.instance.nip11Cache,
|
nip11CachedRetriever = Amethyst.instance.nip11Cache,
|
||||||
|
countResult = countResults[item.relay],
|
||||||
accountViewModel = accountViewModel,
|
accountViewModel = accountViewModel,
|
||||||
nav = nav,
|
nav = nav,
|
||||||
)
|
)
|
||||||
|
|||||||
+57
@@ -24,11 +24,16 @@ import androidx.compose.runtime.Stable
|
|||||||
import androidx.lifecycle.ViewModel
|
import androidx.lifecycle.ViewModel
|
||||||
import androidx.lifecycle.viewModelScope
|
import androidx.lifecycle.viewModelScope
|
||||||
import com.vitorpamplona.amethyst.Amethyst
|
import com.vitorpamplona.amethyst.Amethyst
|
||||||
|
import com.vitorpamplona.amethyst.R
|
||||||
import com.vitorpamplona.amethyst.model.Account
|
import com.vitorpamplona.amethyst.model.Account
|
||||||
import com.vitorpamplona.amethyst.service.replace
|
import com.vitorpamplona.amethyst.service.replace
|
||||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
||||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.common.BasicRelaySetupInfo
|
import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.common.BasicRelaySetupInfo
|
||||||
|
import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.common.RelayCountResult
|
||||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.common.relaySetupInfoBuilder
|
import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.common.relaySetupInfoBuilder
|
||||||
|
import com.vitorpamplona.quartz.nip01Core.relay.client.accessories.queryCountSuspend
|
||||||
|
import com.vitorpamplona.quartz.nip01Core.relay.filters.Filter
|
||||||
|
import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl
|
||||||
import com.vitorpamplona.quartz.nip65RelayList.tags.AdvertisedRelayInfo
|
import com.vitorpamplona.quartz.nip65RelayList.tags.AdvertisedRelayInfo
|
||||||
import com.vitorpamplona.quartz.nip65RelayList.tags.AdvertisedRelayType
|
import com.vitorpamplona.quartz.nip65RelayList.tags.AdvertisedRelayType
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
@@ -48,6 +53,12 @@ class Nip65RelayListViewModel : ViewModel() {
|
|||||||
private val _notificationRelays = MutableStateFlow<List<BasicRelaySetupInfo>>(emptyList())
|
private val _notificationRelays = MutableStateFlow<List<BasicRelaySetupInfo>>(emptyList())
|
||||||
val notificationRelays = _notificationRelays.asStateFlow()
|
val notificationRelays = _notificationRelays.asStateFlow()
|
||||||
|
|
||||||
|
private val _homeCountResults = MutableStateFlow<Map<NormalizedRelayUrl, RelayCountResult>>(emptyMap())
|
||||||
|
val homeCountResults = _homeCountResults.asStateFlow()
|
||||||
|
|
||||||
|
private val _notifCountResults = MutableStateFlow<Map<NormalizedRelayUrl, RelayCountResult>>(emptyMap())
|
||||||
|
val notifCountResults = _notifCountResults.asStateFlow()
|
||||||
|
|
||||||
var hasModified = false
|
var hasModified = false
|
||||||
|
|
||||||
fun init(accountViewModel: AccountViewModel) {
|
fun init(accountViewModel: AccountViewModel) {
|
||||||
@@ -58,6 +69,7 @@ class Nip65RelayListViewModel : ViewModel() {
|
|||||||
fun load() {
|
fun load() {
|
||||||
clear()
|
clear()
|
||||||
loadRelayDocuments()
|
loadRelayDocuments()
|
||||||
|
loadCounts()
|
||||||
}
|
}
|
||||||
|
|
||||||
fun create() {
|
fun create() {
|
||||||
@@ -111,6 +123,51 @@ class Nip65RelayListViewModel : ViewModel() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun loadCounts() {
|
||||||
|
_homeCountResults.value = emptyMap()
|
||||||
|
_notifCountResults.value = emptyMap()
|
||||||
|
|
||||||
|
val client = Amethyst.instance.client
|
||||||
|
|
||||||
|
_homeRelays.value.forEach { item ->
|
||||||
|
viewModelScope.launch(Dispatchers.IO) {
|
||||||
|
val result = client.queryCountSuspend(item.relay, Filter(authors = listOf(account.pubKey)))
|
||||||
|
if (result != null) {
|
||||||
|
val countResult =
|
||||||
|
RelayCountResult(
|
||||||
|
listOf(
|
||||||
|
RelayCountResult.CountEntry(
|
||||||
|
label = R.string.events,
|
||||||
|
count = result.count,
|
||||||
|
approximate = result.approximate,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
_homeCountResults.update { it + (item.relay to countResult) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_notificationRelays.value.forEach { item ->
|
||||||
|
viewModelScope.launch(Dispatchers.IO) {
|
||||||
|
val result = client.queryCountSuspend(item.relay, Filter(tags = mapOf("p" to listOf(account.pubKey))))
|
||||||
|
if (result != null) {
|
||||||
|
val countResult =
|
||||||
|
RelayCountResult(
|
||||||
|
listOf(
|
||||||
|
RelayCountResult.CountEntry(
|
||||||
|
label = R.string.events,
|
||||||
|
count = result.count,
|
||||||
|
approximate = result.approximate,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
_notifCountResults.update { it + (item.relay to countResult) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fun clear() {
|
fun clear() {
|
||||||
hasModified = false
|
hasModified = false
|
||||||
_homeRelays.update {
|
_homeRelays.update {
|
||||||
|
|||||||
+4
@@ -35,10 +35,12 @@ import com.vitorpamplona.amethyst.ui.navigation.navs.rememberExtendedNav
|
|||||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
||||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.common.BasicRelaySetupInfo
|
import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.common.BasicRelaySetupInfo
|
||||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.common.BasicRelaySetupInfoDialog
|
import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.common.BasicRelaySetupInfoDialog
|
||||||
|
import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.common.RelayCountResult
|
||||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.common.RelayUrlEditField
|
import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.common.RelayUrlEditField
|
||||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.common.relaySetupInfoBuilder
|
import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.common.relaySetupInfoBuilder
|
||||||
import com.vitorpamplona.amethyst.ui.theme.FeedPadding
|
import com.vitorpamplona.amethyst.ui.theme.FeedPadding
|
||||||
import com.vitorpamplona.amethyst.ui.theme.StdVertSpacer
|
import com.vitorpamplona.amethyst.ui.theme.StdVertSpacer
|
||||||
|
import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun ProxyRelayList(
|
fun ProxyRelayList(
|
||||||
@@ -65,12 +67,14 @@ fun LazyListScope.renderProxyItems(
|
|||||||
postViewModel: ProxyRelayListViewModel,
|
postViewModel: ProxyRelayListViewModel,
|
||||||
accountViewModel: AccountViewModel,
|
accountViewModel: AccountViewModel,
|
||||||
nav: INav,
|
nav: INav,
|
||||||
|
countResults: Map<NormalizedRelayUrl, RelayCountResult> = emptyMap(),
|
||||||
) {
|
) {
|
||||||
itemsIndexed(feedState, key = { _, item -> "Proxy" + item.relay.url }) { index, item ->
|
itemsIndexed(feedState, key = { _, item -> "Proxy" + item.relay.url }) { index, item ->
|
||||||
BasicRelaySetupInfoDialog(
|
BasicRelaySetupInfoDialog(
|
||||||
item,
|
item,
|
||||||
onDelete = { postViewModel.deleteRelay(item) },
|
onDelete = { postViewModel.deleteRelay(item) },
|
||||||
nip11CachedRetriever = Amethyst.instance.nip11Cache,
|
nip11CachedRetriever = Amethyst.instance.nip11Cache,
|
||||||
|
countResult = countResults[item.relay],
|
||||||
accountViewModel = accountViewModel,
|
accountViewModel = accountViewModel,
|
||||||
nav = nav,
|
nav = nav,
|
||||||
)
|
)
|
||||||
|
|||||||
+4
@@ -35,10 +35,12 @@ import com.vitorpamplona.amethyst.ui.navigation.navs.rememberExtendedNav
|
|||||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
||||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.common.BasicRelaySetupInfo
|
import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.common.BasicRelaySetupInfo
|
||||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.common.BasicRelaySetupInfoDialog
|
import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.common.BasicRelaySetupInfoDialog
|
||||||
|
import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.common.RelayCountResult
|
||||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.common.RelayUrlEditField
|
import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.common.RelayUrlEditField
|
||||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.common.relaySetupInfoBuilder
|
import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.common.relaySetupInfoBuilder
|
||||||
import com.vitorpamplona.amethyst.ui.theme.FeedPadding
|
import com.vitorpamplona.amethyst.ui.theme.FeedPadding
|
||||||
import com.vitorpamplona.amethyst.ui.theme.StdVertSpacer
|
import com.vitorpamplona.amethyst.ui.theme.StdVertSpacer
|
||||||
|
import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun SearchRelayList(
|
fun SearchRelayList(
|
||||||
@@ -65,12 +67,14 @@ fun LazyListScope.renderSearchItems(
|
|||||||
postViewModel: SearchRelayListViewModel,
|
postViewModel: SearchRelayListViewModel,
|
||||||
accountViewModel: AccountViewModel,
|
accountViewModel: AccountViewModel,
|
||||||
nav: INav,
|
nav: INav,
|
||||||
|
countResults: Map<NormalizedRelayUrl, RelayCountResult> = emptyMap(),
|
||||||
) {
|
) {
|
||||||
itemsIndexed(feedState, key = { _, item -> "Search" + item.relay.url }) { index, item ->
|
itemsIndexed(feedState, key = { _, item -> "Search" + item.relay.url }) { index, item ->
|
||||||
BasicRelaySetupInfoDialog(
|
BasicRelaySetupInfoDialog(
|
||||||
item,
|
item,
|
||||||
onDelete = { postViewModel.deleteRelay(item) },
|
onDelete = { postViewModel.deleteRelay(item) },
|
||||||
nip11CachedRetriever = Amethyst.instance.nip11Cache,
|
nip11CachedRetriever = Amethyst.instance.nip11Cache,
|
||||||
|
countResult = countResults[item.relay],
|
||||||
accountViewModel = accountViewModel,
|
accountViewModel = accountViewModel,
|
||||||
nav = nav,
|
nav = nav,
|
||||||
)
|
)
|
||||||
|
|||||||
+11
@@ -21,7 +21,10 @@
|
|||||||
package com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.search
|
package com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.search
|
||||||
|
|
||||||
import androidx.compose.runtime.Stable
|
import androidx.compose.runtime.Stable
|
||||||
|
import com.vitorpamplona.amethyst.R
|
||||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.common.BasicRelaySetupInfoModel
|
import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.common.BasicRelaySetupInfoModel
|
||||||
|
import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.common.CountFilter
|
||||||
|
import com.vitorpamplona.quartz.nip01Core.relay.filters.Filter
|
||||||
import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl
|
import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl
|
||||||
|
|
||||||
@Stable
|
@Stable
|
||||||
@@ -33,4 +36,12 @@ class SearchRelayListViewModel : BasicRelaySetupInfoModel() {
|
|||||||
override suspend fun saveRelayList(urlList: List<NormalizedRelayUrl>) {
|
override suspend fun saveRelayList(urlList: List<NormalizedRelayUrl>) {
|
||||||
account.saveSearchRelayList(urlList)
|
account.saveSearchRelayList(urlList)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun countFilters(relayUrl: NormalizedRelayUrl): List<CountFilter> =
|
||||||
|
listOf(
|
||||||
|
CountFilter(
|
||||||
|
label = R.string.events,
|
||||||
|
filter = Filter(),
|
||||||
|
),
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
+1
@@ -90,6 +90,7 @@ class SearchBarViewModel(
|
|||||||
|
|
||||||
val listState: LazyListState = LazyListState(0, 0)
|
val listState: LazyListState = LazyListState(0, 0)
|
||||||
|
|
||||||
|
@OptIn(kotlinx.coroutines.ExperimentalCoroutinesApi::class)
|
||||||
val directNip05Resolver: Flow<User?> =
|
val directNip05Resolver: Flow<User?> =
|
||||||
searchTerm
|
searchTerm
|
||||||
.debounce(400)
|
.debounce(400)
|
||||||
|
|||||||
+1
-1
@@ -55,7 +55,6 @@ import androidx.compose.runtime.setValue
|
|||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.graphics.Color
|
import androidx.compose.ui.graphics.Color
|
||||||
import androidx.compose.ui.platform.LocalLifecycleOwner
|
|
||||||
import androidx.compose.ui.text.font.FontWeight
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
import androidx.compose.ui.text.input.ImeAction
|
import androidx.compose.ui.text.input.ImeAction
|
||||||
import androidx.compose.ui.text.input.KeyboardCapitalization
|
import androidx.compose.ui.text.input.KeyboardCapitalization
|
||||||
@@ -63,6 +62,7 @@ import androidx.compose.ui.text.style.TextAlign
|
|||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.lifecycle.Lifecycle
|
import androidx.lifecycle.Lifecycle
|
||||||
import androidx.lifecycle.LifecycleEventObserver
|
import androidx.lifecycle.LifecycleEventObserver
|
||||||
|
import androidx.lifecycle.compose.LocalLifecycleOwner
|
||||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||||
import androidx.lifecycle.viewmodel.compose.viewModel
|
import androidx.lifecycle.viewmodel.compose.viewModel
|
||||||
import com.vitorpamplona.amethyst.R
|
import com.vitorpamplona.amethyst.R
|
||||||
|
|||||||
+5
-5
@@ -213,7 +213,7 @@ fun TranslateToSetting(accountViewModel: AccountViewModel) {
|
|||||||
verticalAlignment = Alignment.CenterVertically,
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
) {
|
) {
|
||||||
Text(
|
Text(
|
||||||
text = JavaLocale(currentTranslateTo).displayName,
|
text = JavaLocale.forLanguageTag(currentTranslateTo).displayName,
|
||||||
style = MaterialTheme.typography.bodyMedium,
|
style = MaterialTheme.typography.bodyMedium,
|
||||||
fontWeight = FontWeight.Medium,
|
fontWeight = FontWeight.Medium,
|
||||||
)
|
)
|
||||||
@@ -227,7 +227,7 @@ fun TranslateToSetting(accountViewModel: AccountViewModel) {
|
|||||||
SearchableLanguageList(
|
SearchableLanguageList(
|
||||||
languages = allLanguages,
|
languages = allLanguages,
|
||||||
onSelect = { locale ->
|
onSelect = { locale ->
|
||||||
accountViewModel.updateTranslateTo(locale)
|
accountViewModel.updateTranslateTo(locale.language)
|
||||||
showPicker = false
|
showPicker = false
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
@@ -265,7 +265,7 @@ fun DontTranslateFromSetting(accountViewModel: AccountViewModel) {
|
|||||||
InputChip(
|
InputChip(
|
||||||
selected = true,
|
selected = true,
|
||||||
onClick = { accountViewModel.removeDontTranslateFrom(languageCode) },
|
onClick = { accountViewModel.removeDontTranslateFrom(languageCode) },
|
||||||
label = { Text(JavaLocale(languageCode).displayName) },
|
label = { Text(JavaLocale.forLanguageTag(languageCode).displayName) },
|
||||||
trailingIcon = {
|
trailingIcon = {
|
||||||
Icon(
|
Icon(
|
||||||
imageVector = Icons.Default.Close,
|
imageVector = Icons.Default.Close,
|
||||||
@@ -367,8 +367,8 @@ private fun LanguagePreferenceCard(
|
|||||||
preference: String,
|
preference: String,
|
||||||
accountViewModel: AccountViewModel,
|
accountViewModel: AccountViewModel,
|
||||||
) {
|
) {
|
||||||
val sourceName = JavaLocale(source).displayName
|
val sourceName = JavaLocale.forLanguageTag(source).displayName
|
||||||
val targetName = JavaLocale(target).displayName
|
val targetName = JavaLocale.forLanguageTag(target).displayName
|
||||||
|
|
||||||
OutlinedCard(modifier = Modifier.fillMaxWidth()) {
|
OutlinedCard(modifier = Modifier.fillMaxWidth()) {
|
||||||
Column(modifier = Modifier.padding(12.dp)) {
|
Column(modifier = Modifier.padding(12.dp)) {
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user