Compare commits
136 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| cf4e3f5fc6 | |||
| 57eb919c83 | |||
| 85cfaf2bc9 | |||
| 25a69a8191 | |||
| 6feeb23b1f | |||
| 4b92ffe3c5 | |||
| 823a9c3271 | |||
| fe89df2aa3 | |||
| 97ff8ff802 | |||
| a10a9e7043 | |||
| 4f42abc2ff | |||
| fe042c88b8 | |||
| 55e3a31b61 | |||
| 5760be4313 | |||
| 2fd7556a52 | |||
| e8ed9cd379 | |||
| eeeb3c96d2 | |||
| 2da5dee6bd | |||
| a66193ff45 | |||
| 55131ba7ce | |||
| df6282d2ba | |||
| 6ebe792ce5 | |||
| 6c9bdb2ccd | |||
| bc94c705f3 | |||
| 2b9b4da2cc | |||
| 090070d1f9 | |||
| 16a73f27c9 | |||
| 82245d895c | |||
| 610b8dd171 | |||
| f5b1e91378 | |||
| 1de6d7a874 | |||
| b716f3f792 | |||
| 75053bbbb1 | |||
| f9c7ed4936 | |||
| 1f5be54cb1 | |||
| 0761cdd28f | |||
| 7e2a0e9d5f | |||
| 7ae887561d | |||
| baa1d49b3a | |||
| 58a6be911a | |||
| 368f0b048b | |||
| 10894e17a5 | |||
| ec8a7a40e2 | |||
| ce30820108 | |||
| 147c756cc1 | |||
| c7fb404404 | |||
| 2546146ca8 | |||
| ffa776fd42 | |||
| a59ffb8758 | |||
| 9e74c94871 | |||
| 77294e7f1c | |||
| 57f2f4a619 | |||
| 1df2284ea3 | |||
| 189af077e7 | |||
| 10e4d83bce | |||
| 5d6661f964 | |||
| e6e11bb8f2 | |||
| 0e65e834da | |||
| 19f532c12e | |||
| bfc997ba37 | |||
| 99966a976e | |||
| cd54bc2880 | |||
| ffdd689331 | |||
| af41d81981 | |||
| 10d28ed364 | |||
| b02f4bd53a | |||
| 7ce8e3dbe6 | |||
| 2446d5cdb8 | |||
| d015018a16 | |||
| 6231c75e34 | |||
| 2f3bc6cc6f | |||
| 16c6015919 | |||
| e6b291cc68 | |||
| ae523c1ca6 | |||
| 7c86c1477f | |||
| 71f162f20d | |||
| eeacaca725 | |||
| af52ee25eb | |||
| eef32ca11e | |||
| 1ae821bff8 | |||
| 65483a6ef0 | |||
| 606a9343d9 | |||
| 7dfa6538be | |||
| 476d010ebe | |||
| 96d2efebc8 | |||
| f60f5af424 | |||
| 3da0334083 | |||
| c970038943 | |||
| 4000477bdb | |||
| ba11d53922 | |||
| beef606024 | |||
| 2adf64da55 | |||
| fd3fb8573c | |||
| e0d94d9794 | |||
| 7d049150a0 | |||
| 527ef59adc | |||
| b39775daef | |||
| 4bdb21560a | |||
| 797a9c32aa | |||
| bc864b29f8 | |||
| 482121db5c | |||
| 0fa26c8d0a | |||
| f5c768d6a7 | |||
| c43544734a | |||
| 86d99916f7 | |||
| 135dbc8789 | |||
| fc14de9b0f | |||
| c77197d959 | |||
| 56dddbdd86 | |||
| cbafcf6939 | |||
| 4b156ee699 | |||
| a4e883b09a | |||
| b114a724e2 | |||
| 621c0d839c | |||
| 021c1fc7c4 | |||
| bda91080ab | |||
| a9828be25c | |||
| dde9dbfbfe | |||
| ca7d126a3c | |||
| 7f6450375b | |||
| c9954db3fe | |||
| 3d268f1f9d | |||
| 66a7a2a7af | |||
| 7823e1d803 | |||
| d5e91ce874 | |||
| 6f32c1932f | |||
| cb06c4e954 | |||
| 9188c0a8bc | |||
| 30653fe344 | |||
| 5bb55c453f | |||
| 3024e08ca5 | |||
| aaf1f25167 | |||
| aabbb758a4 | |||
| d824f928b5 | |||
| 445ed27eb8 | |||
| 21f3970ca8 |
+3
-2
@@ -1,6 +1,6 @@
|
|||||||
VITE_DEFAULT_PUBKEYS=06639a386c9c1014217622ccbcf40908c4f1a0c33e23f8d6d68f4abf655f8f71,266815e0c9210dfa324c6cba3573b14bee49da4209a9456f9484e5106cd408a5,391819e2f2f13b90cac7209419eb574ef7c0d1f4e81867fc24c47a3ce5e8a248,3bf0c63fcb93463407af97a5e5ee64fa883d107ef9e558472c4eb9aaaefa459d,3f770d65d3a764a9c5cb503ae123e62ec7598ad035d836e2a810f3877a745b24,55f04590674f3648f4cdc9dc8ce32da2a282074cd0b020596ee033d12d385185,58c741aa630c2da35a56a77c1d05381908bd10504fdd2d8b43f725efa6d23196,61066504617ee79387021e18c89fb79d1ddbc3e7bff19cf2298f40466f8715e9,6389be6491e7b693e9f368ece88fcd145f07c068d2c1bbae4247b9b5ef439d32,63fe6318dc58583cfe16810f86dd09e18bfd76aabc24a0081ce2856f330504ed,6e75f7972397ca3295e0f4ca0fbc6eb9cc79be85bafdd56bd378220ca8eee74e,76c71aae3a491f1d9eec47cba17e229cda4113a0bbb6e6ae1776d7643e29cafa,7fa56f5d6962ab1e3cd424e758c3002b8665f7b0d8dcee9fe9e288d7751ac194,82341f882b6eabcd2ba7f1ef90aad961cf074af15b9ef44a09f9d2a8fbfbe6a2,84dee6e676e5bb67b4ad4e042cf70cbd8681155db535942fcc6a0533858a7240,97c70a44366a6535c145b333f973ea86dfdc2d7a99da618c40c64705ad98e322,b676ded7c768d66a757aa3967b1243d90bf57afb09d1044d3219d8d424e4aea0,dace63b00c42e6e017d00dd190a9328386002ff597b841eb5ef91de4f1ce8491,eeb11961b25442b16389fe6c7ebea9adf0ac36dd596816ea7119e521b8821b9e,fe7f6bc6f7338b76bbf80db402ade65953e20b2f23e66e898204b63cc42539a3
|
VITE_DEFAULT_PUBKEYS=06639a386c9c1014217622ccbcf40908c4f1a0c33e23f8d6d68f4abf655f8f71,266815e0c9210dfa324c6cba3573b14bee49da4209a9456f9484e5106cd408a5,391819e2f2f13b90cac7209419eb574ef7c0d1f4e81867fc24c47a3ce5e8a248,3bf0c63fcb93463407af97a5e5ee64fa883d107ef9e558472c4eb9aaaefa459d,3f770d65d3a764a9c5cb503ae123e62ec7598ad035d836e2a810f3877a745b24,55f04590674f3648f4cdc9dc8ce32da2a282074cd0b020596ee033d12d385185,58c741aa630c2da35a56a77c1d05381908bd10504fdd2d8b43f725efa6d23196,61066504617ee79387021e18c89fb79d1ddbc3e7bff19cf2298f40466f8715e9,6389be6491e7b693e9f368ece88fcd145f07c068d2c1bbae4247b9b5ef439d32,63fe6318dc58583cfe16810f86dd09e18bfd76aabc24a0081ce2856f330504ed,6e75f7972397ca3295e0f4ca0fbc6eb9cc79be85bafdd56bd378220ca8eee74e,76c71aae3a491f1d9eec47cba17e229cda4113a0bbb6e6ae1776d7643e29cafa,7fa56f5d6962ab1e3cd424e758c3002b8665f7b0d8dcee9fe9e288d7751ac194,82341f882b6eabcd2ba7f1ef90aad961cf074af15b9ef44a09f9d2a8fbfbe6a2,84dee6e676e5bb67b4ad4e042cf70cbd8681155db535942fcc6a0533858a7240,97c70a44366a6535c145b333f973ea86dfdc2d7a99da618c40c64705ad98e322,b676ded7c768d66a757aa3967b1243d90bf57afb09d1044d3219d8d424e4aea0,dace63b00c42e6e017d00dd190a9328386002ff597b841eb5ef91de4f1ce8491,eeb11961b25442b16389fe6c7ebea9adf0ac36dd596816ea7119e521b8821b9e,fe7f6bc6f7338b76bbf80db402ade65953e20b2f23e66e898204b63cc42539a3
|
||||||
VITE_DEFAULT_BLOSSOM_SERVERS=https://blossom.primal.net/
|
VITE_DEFAULT_BLOSSOM_SERVERS=https://blossom.primal.net/
|
||||||
VITE_POMADE_SIGNERS=
|
VITE_POMADE_SIGNERS=https://pomade.coracle.social,https://pomade.fiatjaf.com,https://pomade.nostrver.se,https://pomade.scuttle.works
|
||||||
VITE_PLATFORM_URL=https://app.flotilla.social
|
VITE_PLATFORM_URL=https://app.flotilla.social
|
||||||
VITE_PLATFORM_TERMS=https://flotilla.social/terms
|
VITE_PLATFORM_TERMS=https://flotilla.social/terms
|
||||||
VITE_PLATFORM_PRIVACY=https://flotilla.social/privacy
|
VITE_PLATFORM_PRIVACY=https://flotilla.social/privacy
|
||||||
@@ -15,7 +15,8 @@ VITE_PUSH_BRIDGE=wss://npb.coracle.social/
|
|||||||
VITE_BLOCKED_RELAYS=brb.io,relay.nostr.band,nostr.mutinywallet.com,feeds.nostr.band,nostr.zbd.gg,wot.utxo.one,blastr.f7z.xyz,relay.current.fyi
|
VITE_BLOCKED_RELAYS=brb.io,relay.nostr.band,nostr.mutinywallet.com,feeds.nostr.band,nostr.zbd.gg,wot.utxo.one,blastr.f7z.xyz,relay.current.fyi
|
||||||
VITE_INDEXER_RELAYS=purplepag.es,relay.damus.io,indexer.coracle.social
|
VITE_INDEXER_RELAYS=purplepag.es,relay.damus.io,indexer.coracle.social
|
||||||
VITE_DEFAULT_RELAYS=relay.damus.io,relay.primal.net,nostr.mom
|
VITE_DEFAULT_RELAYS=relay.damus.io,relay.primal.net,nostr.mom
|
||||||
VITE_DEFAULT_MESSAGING_RELAYS=auth.nostr1.com
|
VITE_DEFAULT_SEARCH_RELAYS=relay.ditto.pub,antiprimal.net,relay.vertexlab.io
|
||||||
|
VITE_DEFAULT_MESSAGING_RELAYS=auth.nostr1.com,relay.keychat.io,relay.ditto.pub
|
||||||
VITE_SIGNER_RELAYS=relay.nsec.app,ephemeral.snowflare.cc,bucket.coracle.social
|
VITE_SIGNER_RELAYS=relay.nsec.app,ephemeral.snowflare.cc,bucket.coracle.social
|
||||||
VITE_VAPID_PUBLIC_KEY=BIt2D4BdgdbCowD_0d3Np6GbrIGHxd7aIEUeZNe3hQuRlHz02OhzvDaai0XSFoJYVzSzdMjdyW-QhvW9_yq8j4Y
|
VITE_VAPID_PUBLIC_KEY=BIt2D4BdgdbCowD_0d3Np6GbrIGHxd7aIEUeZNe3hQuRlHz02OhzvDaai0XSFoJYVzSzdMjdyW-QhvW9_yq8j4Y
|
||||||
VITE_GLITCHTIP_API_KEY=
|
VITE_GLITCHTIP_API_KEY=
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ on:
|
|||||||
|
|
||||||
env:
|
env:
|
||||||
REGISTRY: ghcr.io
|
REGISTRY: ghcr.io
|
||||||
IMAGE_NAME: ${{ github.repository }}
|
IMAGE_NAME: coracle-social/flotilla
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
build-and-push-image:
|
build-and-push-image:
|
||||||
|
|||||||
@@ -25,6 +25,7 @@ android/app/src/main/assets/public/
|
|||||||
|
|
||||||
# Web/JavaScript
|
# Web/JavaScript
|
||||||
node_modules/
|
node_modules/
|
||||||
|
.pnpm-store/
|
||||||
build/
|
build/
|
||||||
.svelte-kit/
|
.svelte-kit/
|
||||||
|
|
||||||
|
|||||||
@@ -169,6 +169,7 @@ src/
|
|||||||
- When creating forms, use `FieldInline` or `Field` instead of custom elements/tailwindcss
|
- When creating forms, use `FieldInline` or `Field` instead of custom elements/tailwindcss
|
||||||
- Do not define svelte event handlers inline, instead name them and put them in the script section of templates
|
- Do not define svelte event handlers inline, instead name them and put them in the script section of templates
|
||||||
- Avoid using `as`, except where necessary. Instead, annotate function parameters, and ensure upstream values are typed correctly.
|
- Avoid using `as`, except where necessary. Instead, annotate function parameters, and ensure upstream values are typed correctly.
|
||||||
|
- Instead of `getTag(tagName, event.tags)?.[1] || ""`, use `getTagValue(tagName, event.tags)`
|
||||||
|
|
||||||
**Human-First Simplicity (Jon Staab Style):**
|
**Human-First Simplicity (Jon Staab Style):**
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,32 @@
|
|||||||
# Changelog
|
# Changelog
|
||||||
|
|
||||||
|
# 1.7.2
|
||||||
|
|
||||||
|
* Fix race condition in nip 46
|
||||||
|
* Remove duplicate spaces button
|
||||||
|
* Combine discover and space list pages
|
||||||
|
* Fix some chat related bugs
|
||||||
|
* Fix bug with joining spaces
|
||||||
|
|
||||||
|
# 1.7.1
|
||||||
|
|
||||||
|
* Fix pomade registration fallback in case of offline signer
|
||||||
|
|
||||||
|
# 1.7.0
|
||||||
|
|
||||||
|
* Enable email/password login
|
||||||
|
* Add up/edit to direct messages
|
||||||
|
* Fix a number of UI bugs
|
||||||
|
* Improve navigation on mobile
|
||||||
|
* Improve performance and syncing reliability
|
||||||
|
* Add proof of work to DMs
|
||||||
|
* Detect blossom support using supported_nips
|
||||||
|
* Improve notification badges
|
||||||
|
* Add voice rooms (@mplorentz)
|
||||||
|
* Re-design relay onboarding and settings
|
||||||
|
* Add android fallback for push notifications
|
||||||
|
* Fix file uploads on android
|
||||||
|
|
||||||
# 1.6.5
|
# 1.6.5
|
||||||
|
|
||||||
* Attempt to fix permission grant for notifications
|
* Attempt to fix permission grant for notifications
|
||||||
|
|||||||
+3
-2
@@ -4,6 +4,8 @@
|
|||||||
|
|
||||||
FROM node:20-bookworm AS builder
|
FROM node:20-bookworm AS builder
|
||||||
|
|
||||||
|
RUN apt-get update && apt-get install -y --no-install-recommends curl
|
||||||
|
|
||||||
RUN npm install -g pnpm@latest
|
RUN npm install -g pnpm@latest
|
||||||
|
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
@@ -20,7 +22,6 @@ ENV VITE_BUILD_HASH=${VITE_BUILD_HASH}
|
|||||||
ENV NODE_OPTIONS=--max_old_space_size=16384
|
ENV NODE_OPTIONS=--max_old_space_size=16384
|
||||||
RUN pnpm run build
|
RUN pnpm run build
|
||||||
|
|
||||||
# Stage 2: Runtime
|
|
||||||
FROM node:20-alpine
|
FROM node:20-alpine
|
||||||
|
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
@@ -28,4 +29,4 @@ WORKDIR /app
|
|||||||
# Copy only the built output - no source, no .env, no dev deps
|
# Copy only the built output - no source, no .env, no dev deps
|
||||||
COPY --from=builder /app/build ./build
|
COPY --from=builder /app/build ./build
|
||||||
|
|
||||||
CMD ["npx", "serve", "build"]
|
CMD ["npx", "serve", "-s", "build"]
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ If you would like to be interoperable with Flotilla, please check out this guide
|
|||||||
|
|
||||||
## Environment
|
## Environment
|
||||||
|
|
||||||
You can also optionally create an `.env` file and populate it with the following environment variables (see `.env` for examples):
|
You can also optionally create an `.env.local` file and populate it with the following environment variables (see `.env.template` for examples):
|
||||||
|
|
||||||
- `VITE_DEFAULT_PUBKEYS` - A comma-separated list of hex pubkeys for bootstrapping web of trust
|
- `VITE_DEFAULT_PUBKEYS` - A comma-separated list of hex pubkeys for bootstrapping web of trust
|
||||||
- `VITE_PLATFORM_URL` - The url where the app will be hosted
|
- `VITE_PLATFORM_URL` - The url where the app will be hosted
|
||||||
@@ -29,7 +29,7 @@ To run your own Flotilla, it's as simple as:
|
|||||||
```sh
|
```sh
|
||||||
pnpm install
|
pnpm install
|
||||||
pnpm run build
|
pnpm run build
|
||||||
npx serve build
|
npx serve -s build
|
||||||
```
|
```
|
||||||
|
|
||||||
Or, if you prefer to use a container:
|
Or, if you prefer to use a container:
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
apply plugin: 'com.android.application'
|
apply plugin: 'com.android.application'
|
||||||
|
apply plugin: 'kotlin-android'
|
||||||
|
|
||||||
android {
|
android {
|
||||||
namespace = "social.flotilla"
|
namespace = "social.flotilla"
|
||||||
@@ -7,8 +8,8 @@ android {
|
|||||||
applicationId "social.flotilla"
|
applicationId "social.flotilla"
|
||||||
minSdk rootProject.ext.minSdkVersion
|
minSdk rootProject.ext.minSdkVersion
|
||||||
targetSdk rootProject.ext.targetSdkVersion
|
targetSdk rootProject.ext.targetSdkVersion
|
||||||
versionCode 41
|
versionCode 44
|
||||||
versionName "1.6.5"
|
versionName "1.7.2"
|
||||||
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
||||||
aaptOptions {
|
aaptOptions {
|
||||||
// Files and dirs to omit from the packaged assets dir, modified to accommodate modern web apps.
|
// Files and dirs to omit from the packaged assets dir, modified to accommodate modern web apps.
|
||||||
@@ -35,6 +36,10 @@ dependencies {
|
|||||||
implementation "androidx.appcompat:appcompat:$androidxAppCompatVersion"
|
implementation "androidx.appcompat:appcompat:$androidxAppCompatVersion"
|
||||||
implementation "androidx.coordinatorlayout:coordinatorlayout:$androidxCoordinatorLayoutVersion"
|
implementation "androidx.coordinatorlayout:coordinatorlayout:$androidxCoordinatorLayoutVersion"
|
||||||
implementation "androidx.core:core-splashscreen:$coreSplashScreenVersion"
|
implementation "androidx.core:core-splashscreen:$coreSplashScreenVersion"
|
||||||
|
implementation "androidx.work:work-runtime:2.10.3"
|
||||||
|
implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version"
|
||||||
|
implementation "com.squareup.okhttp3:okhttp:4.12.0"
|
||||||
|
implementation "fr.acinq.secp256k1:secp256k1-kmp-jni-android:0.22.0"
|
||||||
implementation project(':capacitor-android')
|
implementation project(':capacitor-android')
|
||||||
testImplementation "junit:junit:$junitVersion"
|
testImplementation "junit:junit:$junitVersion"
|
||||||
androidTestImplementation "androidx.test.ext:junit:$androidxJunitVersion"
|
androidTestImplementation "androidx.test.ext:junit:$androidxJunitVersion"
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ android {
|
|||||||
|
|
||||||
apply from: "../capacitor-cordova-android-plugins/cordova.variables.gradle"
|
apply from: "../capacitor-cordova-android-plugins/cordova.variables.gradle"
|
||||||
dependencies {
|
dependencies {
|
||||||
|
implementation project(':aparajita-capacitor-secure-storage')
|
||||||
implementation project(':capacitor-community-safe-area')
|
implementation project(':capacitor-community-safe-area')
|
||||||
implementation project(':capacitor-app')
|
implementation project(':capacitor-app')
|
||||||
implementation project(':capacitor-filesystem')
|
implementation project(':capacitor-filesystem')
|
||||||
|
|||||||
@@ -42,4 +42,6 @@
|
|||||||
|
|
||||||
<uses-permission android:name="android.permission.INTERNET" />
|
<uses-permission android:name="android.permission.INTERNET" />
|
||||||
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
|
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
|
||||||
|
<uses-permission android:name="android.permission.RECORD_AUDIO" />
|
||||||
|
<uses-permission android:name="android.permission.MODIFY_AUDIO_SETTINGS" />
|
||||||
</manifest>
|
</manifest>
|
||||||
|
|||||||
@@ -1,5 +1,15 @@
|
|||||||
package social.flotilla;
|
package social.flotilla;
|
||||||
|
|
||||||
|
import android.os.Bundle;
|
||||||
|
|
||||||
import com.getcapacitor.BridgeActivity;
|
import com.getcapacitor.BridgeActivity;
|
||||||
|
|
||||||
public class MainActivity extends BridgeActivity {}
|
import social.flotilla.notifications.AndroidPushFallbackPlugin;
|
||||||
|
|
||||||
|
public class MainActivity extends BridgeActivity {
|
||||||
|
@Override
|
||||||
|
public void onCreate(Bundle savedInstanceState) {
|
||||||
|
registerPlugin(AndroidPushFallbackPlugin.class);
|
||||||
|
super.onCreate(savedInstanceState);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -0,0 +1,99 @@
|
|||||||
|
package social.flotilla.notifications
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.content.SharedPreferences
|
||||||
|
import androidx.work.Constraints
|
||||||
|
import androidx.work.ExistingPeriodicWorkPolicy
|
||||||
|
import androidx.work.ExistingWorkPolicy
|
||||||
|
import androidx.work.NetworkType
|
||||||
|
import androidx.work.OneTimeWorkRequest
|
||||||
|
import androidx.work.PeriodicWorkRequest
|
||||||
|
import androidx.work.WorkManager
|
||||||
|
import com.getcapacitor.JSObject
|
||||||
|
import com.getcapacitor.Plugin
|
||||||
|
import com.getcapacitor.PluginCall
|
||||||
|
import com.getcapacitor.PluginMethod
|
||||||
|
import com.getcapacitor.annotation.CapacitorPlugin
|
||||||
|
import org.json.JSONArray
|
||||||
|
import org.json.JSONException
|
||||||
|
import org.json.JSONObject
|
||||||
|
import java.util.concurrent.TimeUnit
|
||||||
|
|
||||||
|
@CapacitorPlugin(name = "AndroidPushFallback")
|
||||||
|
class AndroidPushFallbackPlugin : Plugin() {
|
||||||
|
companion object {
|
||||||
|
const val PREFS_NAME = "CapacitorStorage"
|
||||||
|
const val KEY_STATE = "androidPushFallback.state"
|
||||||
|
const val UNIQUE_PERIODIC_WORK = "androidPushFallback.periodic"
|
||||||
|
const val UNIQUE_IMMEDIATE_WORK = "androidPushFallback.immediate"
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun getPrefs(): SharedPreferences {
|
||||||
|
return context.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE)
|
||||||
|
}
|
||||||
|
|
||||||
|
@PluginMethod
|
||||||
|
fun syncState(call: PluginCall) {
|
||||||
|
val state: JSObject? = call.getObject("state")
|
||||||
|
|
||||||
|
if (state != null) {
|
||||||
|
getPrefs().edit().putString(KEY_STATE, state.toString()).apply()
|
||||||
|
|
||||||
|
if (isEnabled(state.toString())) {
|
||||||
|
scheduleWork()
|
||||||
|
} else {
|
||||||
|
cancelWork()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
call.resolve()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun isEnabled(rawState: String?): Boolean {
|
||||||
|
if (rawState == null || rawState.isEmpty()) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
return try {
|
||||||
|
val state = JSONObject(rawState)
|
||||||
|
val subscriptions: JSONArray? = state.optJSONArray("subscriptions")
|
||||||
|
subscriptions != null && subscriptions.length() > 0
|
||||||
|
} catch (_: JSONException) {
|
||||||
|
false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun scheduleWork() {
|
||||||
|
val constraints = Constraints.Builder().setRequiredNetworkType(NetworkType.CONNECTED).build()
|
||||||
|
|
||||||
|
val workManager = WorkManager.getInstance(context)
|
||||||
|
|
||||||
|
val periodic = PeriodicWorkRequest.Builder(
|
||||||
|
AndroidPushFallbackWorker::class.java,
|
||||||
|
15,
|
||||||
|
TimeUnit.MINUTES,
|
||||||
|
).setConstraints(constraints).build()
|
||||||
|
|
||||||
|
val immediate = OneTimeWorkRequest.Builder(AndroidPushFallbackWorker::class.java)
|
||||||
|
.setConstraints(constraints)
|
||||||
|
.build()
|
||||||
|
|
||||||
|
workManager.enqueueUniquePeriodicWork(
|
||||||
|
UNIQUE_PERIODIC_WORK,
|
||||||
|
ExistingPeriodicWorkPolicy.UPDATE,
|
||||||
|
periodic,
|
||||||
|
)
|
||||||
|
|
||||||
|
workManager.enqueueUniqueWork(
|
||||||
|
UNIQUE_IMMEDIATE_WORK,
|
||||||
|
ExistingWorkPolicy.REPLACE,
|
||||||
|
immediate,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun cancelWork() {
|
||||||
|
val workManager = WorkManager.getInstance(context)
|
||||||
|
workManager.cancelUniqueWork(UNIQUE_IMMEDIATE_WORK)
|
||||||
|
workManager.cancelUniqueWork(UNIQUE_PERIODIC_WORK)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,862 @@
|
|||||||
|
package social.flotilla.notifications
|
||||||
|
|
||||||
|
import android.Manifest
|
||||||
|
import android.app.NotificationChannel
|
||||||
|
import android.app.NotificationManager
|
||||||
|
import android.util.Log
|
||||||
|
import android.app.PendingIntent
|
||||||
|
import android.content.Context
|
||||||
|
import android.content.Intent
|
||||||
|
import android.app.ActivityManager
|
||||||
|
import android.content.SharedPreferences
|
||||||
|
import android.content.pm.PackageManager
|
||||||
|
import android.database.Cursor
|
||||||
|
import android.net.Uri
|
||||||
|
import android.os.Build
|
||||||
|
import androidx.core.app.NotificationCompat
|
||||||
|
import androidx.core.app.NotificationManagerCompat
|
||||||
|
import androidx.core.content.ContextCompat
|
||||||
|
import androidx.work.Worker
|
||||||
|
import androidx.work.WorkerParameters
|
||||||
|
import fr.acinq.secp256k1.Secp256k1
|
||||||
|
import okhttp3.OkHttpClient
|
||||||
|
import okhttp3.Request
|
||||||
|
import okhttp3.Response
|
||||||
|
import okhttp3.WebSocket
|
||||||
|
import okhttp3.WebSocketListener
|
||||||
|
import org.json.JSONArray
|
||||||
|
import org.json.JSONObject
|
||||||
|
import social.flotilla.notifications.AndroidPushFallbackPlugin.Companion.KEY_STATE
|
||||||
|
import social.flotilla.notifications.AndroidPushFallbackPlugin.Companion.PREFS_NAME
|
||||||
|
import java.nio.charset.StandardCharsets
|
||||||
|
import java.security.MessageDigest
|
||||||
|
import java.security.SecureRandom
|
||||||
|
import java.util.Arrays
|
||||||
|
import java.util.UUID
|
||||||
|
import java.util.concurrent.ConcurrentHashMap
|
||||||
|
import java.util.concurrent.CountDownLatch
|
||||||
|
import java.util.concurrent.TimeUnit
|
||||||
|
import android.util.Base64
|
||||||
|
|
||||||
|
class AndroidPushFallbackWorker(context: Context, params: WorkerParameters) : Worker(context, params) {
|
||||||
|
companion object {
|
||||||
|
private const val TAG = "PushFallback"
|
||||||
|
private const val CHANNEL_ID = "flotilla_fallback"
|
||||||
|
private const val CURSOR_PREFIX = "androidPushFallback.cursor."
|
||||||
|
private const val SOCKET_TIMEOUT_SECONDS = 20L
|
||||||
|
private const val REJECTED = "__REJECTED__"
|
||||||
|
private const val KIND_RELAY_AUTH = 22242
|
||||||
|
private const val KIND_NIP46_RPC = 24133
|
||||||
|
private val SECP = Secp256k1.get()
|
||||||
|
}
|
||||||
|
|
||||||
|
private val prefs: SharedPreferences =
|
||||||
|
context.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE)
|
||||||
|
private val client: OkHttpClient = OkHttpClient.Builder().build()
|
||||||
|
|
||||||
|
// ---- Socket pool ----
|
||||||
|
|
||||||
|
// Opens each relay URL at most once; caller must invoke closeAll() when done.
|
||||||
|
private inner class SocketPool {
|
||||||
|
private val sockets = ConcurrentHashMap<String, WebSocket>()
|
||||||
|
|
||||||
|
fun open(url: String, listener: WebSocketListener): WebSocket =
|
||||||
|
sockets.getOrPut(url) {
|
||||||
|
client.newWebSocket(Request.Builder().url(url).build(), listener)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun closeAll() {
|
||||||
|
for ((_, ws) in sockets) ws.close(1000, "done")
|
||||||
|
sockets.clear()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun doWork(): Result {
|
||||||
|
if (isAppInForeground()) {
|
||||||
|
return Result.success()
|
||||||
|
}
|
||||||
|
|
||||||
|
val pool = SocketPool()
|
||||||
|
try {
|
||||||
|
val rawState = prefs.getString(KEY_STATE, "") ?: ""
|
||||||
|
if (rawState.isEmpty()) return Result.success()
|
||||||
|
|
||||||
|
val state = JSONObject(rawState)
|
||||||
|
val sessionInfo = getSessionInfo(state)
|
||||||
|
val subscriptions = parseSubscriptions(state)
|
||||||
|
if (subscriptions.isEmpty()) return Result.success()
|
||||||
|
|
||||||
|
val activeSince = state.optLong("activeSince", 0L)
|
||||||
|
val seen = mutableSetOf<String>()
|
||||||
|
var latestPair: Pair<String, JSONObject>? = null
|
||||||
|
|
||||||
|
for (sub in subscriptions) {
|
||||||
|
val cursorKey = CURSOR_PREFIX + sub.relay + ":" + sub.key
|
||||||
|
val since = maxOf(prefs.getLong(cursorKey, 0L), activeSince)
|
||||||
|
val result = pollRelay(sub, since, sessionInfo, pool)
|
||||||
|
|
||||||
|
if (result.lastCursor > prefs.getLong(cursorKey, 0L)) {
|
||||||
|
prefs.edit().putLong(cursorKey, result.lastCursor).apply()
|
||||||
|
}
|
||||||
|
|
||||||
|
for (event in result.events) {
|
||||||
|
val id = event.optString("id", "")
|
||||||
|
if (id.isNotEmpty() && seen.add(id)) {
|
||||||
|
val createdAt = event.optLong("created_at", 0L)
|
||||||
|
if (latestPair == null || createdAt > latestPair!!.second.optLong("created_at", 0L)) {
|
||||||
|
latestPair = Pair(sub.relay, event)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (latestPair != null) {
|
||||||
|
val (relay, event) = latestPair!!
|
||||||
|
postNotification(relay, event)
|
||||||
|
}
|
||||||
|
|
||||||
|
return Result.success()
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Log.e(TAG, "Worker failed", e)
|
||||||
|
return Result.success()
|
||||||
|
} finally {
|
||||||
|
pool.closeAll()
|
||||||
|
client.dispatcher.executorService.shutdown()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun isAppInForeground(): Boolean {
|
||||||
|
val am = applicationContext.getSystemService(ActivityManager::class.java) ?: return false
|
||||||
|
val tasks = am.getRunningAppProcesses() ?: return false
|
||||||
|
val pkg = applicationContext.packageName
|
||||||
|
return tasks.any { it.processName == pkg && it.importance == ActivityManager.RunningAppProcessInfo.IMPORTANCE_FOREGROUND }
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun getSessionInfo(state: JSONObject): SessionInfo {
|
||||||
|
val session = state.optJSONObject("session") ?: return SessionInfo("anonymous", "", JSONObject())
|
||||||
|
return SessionInfo(
|
||||||
|
session.optString("method", "anonymous"),
|
||||||
|
session.optString("pubkey", ""),
|
||||||
|
session,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun parseSubscriptions(state: JSONObject): List<Subscription> {
|
||||||
|
val result = mutableListOf<Subscription>()
|
||||||
|
val arr = state.optJSONArray("subscriptions") ?: return result
|
||||||
|
|
||||||
|
for (i in 0 until arr.length()) {
|
||||||
|
val item = arr.optJSONObject(i) ?: continue
|
||||||
|
val relay = item.optString("relay", "").trim()
|
||||||
|
|
||||||
|
if (!relay.startsWith("wss://") && !relay.startsWith("ws://")) continue
|
||||||
|
|
||||||
|
val filters = item.optJSONArray("filters")
|
||||||
|
if (filters == null || filters.length() == 0) continue
|
||||||
|
|
||||||
|
val key = item.optString("key", "").trim()
|
||||||
|
result.add(Subscription(relay, key, filters, item.optJSONArray("ignore")))
|
||||||
|
}
|
||||||
|
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun pollRelay(sub: Subscription, since: Long, sessionInfo: SessionInfo, pool: SocketPool): RelayResult {
|
||||||
|
val result = RelayResult()
|
||||||
|
val latch = CountDownLatch(1)
|
||||||
|
|
||||||
|
val listener = RelayListener(sub, since, sessionInfo, result, latch, pool)
|
||||||
|
pool.open(sub.relay, listener)
|
||||||
|
|
||||||
|
if (!latch.await(SOCKET_TIMEOUT_SECONDS, TimeUnit.SECONDS)) {
|
||||||
|
Log.d(TAG, "Relay ${sub.relay} timed out")
|
||||||
|
}
|
||||||
|
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun postNotification(relay: String, event: JSONObject) {
|
||||||
|
val context = applicationContext
|
||||||
|
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU &&
|
||||||
|
ContextCompat.checkSelfPermission(context, Manifest.permission.POST_NOTIFICATIONS) != PackageManager.PERMISSION_GRANTED
|
||||||
|
) return
|
||||||
|
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||||
|
val manager = context.getSystemService(NotificationManager::class.java)
|
||||||
|
if (manager != null && manager.getNotificationChannel(CHANNEL_ID) == null) {
|
||||||
|
val channel = NotificationChannel(CHANNEL_ID, "Fallback Notifications", NotificationManager.IMPORTANCE_DEFAULT)
|
||||||
|
channel.description = "Notifications delivered by Android background fallback"
|
||||||
|
manager.createNotificationChannel(channel)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val id = event.optString("id", "")
|
||||||
|
val encodedRelay = Uri.encode(relay)
|
||||||
|
val url = "https://app.flotilla.social/?relay=$encodedRelay&id=$id"
|
||||||
|
val intent = Intent(Intent.ACTION_VIEW, Uri.parse(url))
|
||||||
|
intent.setPackage(context.packageName)
|
||||||
|
intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TOP
|
||||||
|
|
||||||
|
val pendingIntent = PendingIntent.getActivity(
|
||||||
|
context, 0, intent,
|
||||||
|
PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE,
|
||||||
|
)
|
||||||
|
|
||||||
|
val body = "New activity"
|
||||||
|
|
||||||
|
val notification = NotificationCompat.Builder(context, CHANNEL_ID)
|
||||||
|
.setSmallIcon(android.R.drawable.stat_notify_chat)
|
||||||
|
.setContentTitle("Flotilla")
|
||||||
|
.setContentText(body)
|
||||||
|
.setAutoCancel(true)
|
||||||
|
.setPriority(NotificationCompat.PRIORITY_DEFAULT)
|
||||||
|
.setContentIntent(pendingIntent)
|
||||||
|
.build()
|
||||||
|
|
||||||
|
NotificationManagerCompat.from(context).notify(1, notification)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun matchesFilter(filter: JSONObject, event: JSONObject): Boolean {
|
||||||
|
val kinds = filter.optJSONArray("kinds")
|
||||||
|
if (kinds != null && kinds.length() > 0) {
|
||||||
|
val kind = event.optInt("kind", -1)
|
||||||
|
var found = false
|
||||||
|
for (i in 0 until kinds.length()) {
|
||||||
|
if (kinds.optInt(i, -1) == kind) { found = true; break }
|
||||||
|
}
|
||||||
|
if (!found) return false
|
||||||
|
}
|
||||||
|
|
||||||
|
val tags = event.optJSONArray("tags")
|
||||||
|
val iter = filter.keys()
|
||||||
|
while (iter.hasNext()) {
|
||||||
|
val key = iter.next()
|
||||||
|
if (!key.startsWith("#")) continue
|
||||||
|
val tagName = key.substring(1)
|
||||||
|
val allowed = filter.optJSONArray(key) ?: continue
|
||||||
|
if (allowed.length() == 0) continue
|
||||||
|
|
||||||
|
val allowedValues = mutableSetOf<String>()
|
||||||
|
for (i in 0 until allowed.length()) {
|
||||||
|
val v = allowed.optString(i, "")
|
||||||
|
if (v.isNotEmpty()) allowedValues.add(v)
|
||||||
|
}
|
||||||
|
|
||||||
|
var matched = false
|
||||||
|
if (tags != null) {
|
||||||
|
for (i in 0 until tags.length()) {
|
||||||
|
val tag = tags.optJSONArray(i) ?: continue
|
||||||
|
if (tag.optString(0, "") == tagName && allowedValues.contains(tag.optString(1, ""))) {
|
||||||
|
matched = true; break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!matched) return false
|
||||||
|
}
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---- Crypto helpers ----
|
||||||
|
|
||||||
|
private fun computeEventId(event: JSONObject): String {
|
||||||
|
return try {
|
||||||
|
val serialized = JSONArray()
|
||||||
|
serialized.put(0)
|
||||||
|
serialized.put(event.optString("pubkey", ""))
|
||||||
|
serialized.put(event.optLong("created_at", 0))
|
||||||
|
serialized.put(event.optInt("kind", 0))
|
||||||
|
serialized.put(event.optJSONArray("tags") ?: JSONArray())
|
||||||
|
serialized.put(event.optString("content", ""))
|
||||||
|
// JSONObject escapes forward slashes (/ -> \/), but NIP-01 event ID hashing
|
||||||
|
// requires unescaped slashes. Replace them before hashing.
|
||||||
|
val serializedStr = serialized.toString().replace("\\/", "/")
|
||||||
|
bytesToHex(sha256(serializedStr.toByteArray(StandardCharsets.UTF_8)))
|
||||||
|
} catch (_: Exception) {
|
||||||
|
""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun deriveXOnlyPubkey(secretHex: String): String {
|
||||||
|
val secret = hexToBytes(secretHex)
|
||||||
|
if (secret.size != 32 || !SECP.secKeyVerify(secret)) return ""
|
||||||
|
val pubkey65 = try { SECP.pubkeyCreate(secret) } catch (_: Exception) { return "" }
|
||||||
|
if (pubkey65.size != 65) return ""
|
||||||
|
return bytesToHex(Arrays.copyOfRange(pubkey65, 1, 33))
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun schnorrSign(secretHex: String, messageHex: String): String {
|
||||||
|
val sk = hexToBytes(secretHex)
|
||||||
|
val msg = hexToBytes(messageHex)
|
||||||
|
if (sk.size != 32 || msg.size != 32 || !SECP.secKeyVerify(sk)) return ""
|
||||||
|
val aux = ByteArray(32).also { SecureRandom().nextBytes(it) }
|
||||||
|
val sig = try { SECP.signSchnorr(msg, sk, aux) } catch (_: Exception) { return "" }
|
||||||
|
if (sig.size != 64) return ""
|
||||||
|
return bytesToHex(sig)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun sha256(input: ByteArray): ByteArray =
|
||||||
|
try { MessageDigest.getInstance("SHA-256").digest(input) } catch (_: Exception) { ByteArray(32) }
|
||||||
|
|
||||||
|
private fun hexToBytes(hex: String?): ByteArray {
|
||||||
|
var s = hex?.trim()?.lowercase() ?: ""
|
||||||
|
if (s.startsWith("0x")) s = s.substring(2)
|
||||||
|
if (s.length % 2 == 1) s = "0$s"
|
||||||
|
val bytes = ByteArray(s.length / 2)
|
||||||
|
var i = 0
|
||||||
|
while (i < s.length) {
|
||||||
|
val hi = Character.digit(s[i], 16)
|
||||||
|
val lo = Character.digit(s[i + 1], 16)
|
||||||
|
if (hi < 0 || lo < 0) return ByteArray(0)
|
||||||
|
bytes[i / 2] = ((hi shl 4) + lo).toByte()
|
||||||
|
i += 2
|
||||||
|
}
|
||||||
|
return bytes
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun bytesToHex(bytes: ByteArray): String {
|
||||||
|
val hex = "0123456789abcdef".toCharArray()
|
||||||
|
val chars = CharArray(bytes.size * 2)
|
||||||
|
for (i in bytes.indices) {
|
||||||
|
val v = bytes[i].toInt() and 0xFF
|
||||||
|
chars[i * 2] = hex[v ushr 4]
|
||||||
|
chars[i * 2 + 1] = hex[v and 0x0F]
|
||||||
|
}
|
||||||
|
return String(chars)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---- NIP-44 encryption (v2: ECDH + HKDF + ChaCha20 + HMAC-SHA256) ----
|
||||||
|
|
||||||
|
private fun nip44ConversationKey(clientSecret: String, theirPubkey: String): ByteArray {
|
||||||
|
val sk = hexToBytes(clientSecret)
|
||||||
|
val pk = hexToBytes("02$theirPubkey")
|
||||||
|
if (sk.size != 32 || pk.size != 33) return ByteArray(0)
|
||||||
|
val shared = try { SECP.pubKeyTweakMul(pk, sk) } catch (_: Exception) { return ByteArray(0) }
|
||||||
|
if (shared.size != 65) return ByteArray(0)
|
||||||
|
val sharedX = Arrays.copyOfRange(shared, 1, 33)
|
||||||
|
return hkdfExtract(sharedX, "nip44-v2".toByteArray(StandardCharsets.UTF_8))
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun hkdfExtract(ikm: ByteArray, salt: ByteArray): ByteArray {
|
||||||
|
val mac = javax.crypto.Mac.getInstance("HmacSHA256")
|
||||||
|
mac.init(javax.crypto.spec.SecretKeySpec(salt, "HmacSHA256"))
|
||||||
|
return mac.doFinal(ikm)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun hkdfExpand(prk: ByteArray, info: ByteArray, length: Int): ByteArray {
|
||||||
|
val mac = javax.crypto.Mac.getInstance("HmacSHA256")
|
||||||
|
val result = ByteArray(length)
|
||||||
|
var prev = ByteArray(0)
|
||||||
|
var offset = 0
|
||||||
|
var counter = 1
|
||||||
|
while (offset < length) {
|
||||||
|
mac.init(javax.crypto.spec.SecretKeySpec(prk, "HmacSHA256"))
|
||||||
|
mac.update(prev)
|
||||||
|
mac.update(info)
|
||||||
|
mac.update(counter.toByte())
|
||||||
|
prev = mac.doFinal()
|
||||||
|
val toCopy = minOf(prev.size, length - offset)
|
||||||
|
System.arraycopy(prev, 0, result, offset, toCopy)
|
||||||
|
offset += toCopy
|
||||||
|
counter++
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun hmacSha256(key: ByteArray, vararg parts: ByteArray): ByteArray {
|
||||||
|
val mac = javax.crypto.Mac.getInstance("HmacSHA256")
|
||||||
|
mac.init(javax.crypto.spec.SecretKeySpec(key, "HmacSHA256"))
|
||||||
|
for (part in parts) mac.update(part)
|
||||||
|
return mac.doFinal()
|
||||||
|
}
|
||||||
|
|
||||||
|
// ChaCha20 block function per RFC 8439
|
||||||
|
private fun chacha20Block(key: ByteArray, counter: Int, nonce: ByteArray): ByteArray {
|
||||||
|
fun Int.rotl(n: Int) = (this shl n) or (this ushr (32 - n))
|
||||||
|
val state = IntArray(16)
|
||||||
|
state[0] = 0x61707865; state[1] = 0x3320646e; state[2] = 0x79622d32; state[3] = 0x6b206574
|
||||||
|
for (i in 0..7) state[4 + i] = (key[i*4].toInt() and 0xFF) or
|
||||||
|
((key[i*4+1].toInt() and 0xFF) shl 8) or
|
||||||
|
((key[i*4+2].toInt() and 0xFF) shl 16) or
|
||||||
|
((key[i*4+3].toInt() and 0xFF) shl 24)
|
||||||
|
state[12] = counter
|
||||||
|
for (i in 0..2) state[13 + i] = (nonce[i*4].toInt() and 0xFF) or
|
||||||
|
((nonce[i*4+1].toInt() and 0xFF) shl 8) or
|
||||||
|
((nonce[i*4+2].toInt() and 0xFF) shl 16) or
|
||||||
|
((nonce[i*4+3].toInt() and 0xFF) shl 24)
|
||||||
|
val working = state.copyOf()
|
||||||
|
repeat(10) {
|
||||||
|
fun quarterRound(a: Int, b: Int, c: Int, d: Int) {
|
||||||
|
working[a] += working[b]; working[d] = (working[d] xor working[a]).rotl(16)
|
||||||
|
working[c] += working[d]; working[b] = (working[b] xor working[c]).rotl(12)
|
||||||
|
working[a] += working[b]; working[d] = (working[d] xor working[a]).rotl(8)
|
||||||
|
working[c] += working[d]; working[b] = (working[b] xor working[c]).rotl(7)
|
||||||
|
}
|
||||||
|
quarterRound(0,4,8,12); quarterRound(1,5,9,13); quarterRound(2,6,10,14); quarterRound(3,7,11,15)
|
||||||
|
quarterRound(0,5,10,15); quarterRound(1,6,11,12); quarterRound(2,7,8,13); quarterRound(3,4,9,14)
|
||||||
|
}
|
||||||
|
val out = ByteArray(64)
|
||||||
|
for (i in 0..15) {
|
||||||
|
val v = working[i] + state[i]
|
||||||
|
out[i*4] = v.toByte()
|
||||||
|
out[i*4+1] = (v ushr 8).toByte()
|
||||||
|
out[i*4+2] = (v ushr 16).toByte()
|
||||||
|
out[i*4+3] = (v ushr 24).toByte()
|
||||||
|
}
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun chacha20(key: ByteArray, nonce: ByteArray, data: ByteArray): ByteArray {
|
||||||
|
val out = ByteArray(data.size)
|
||||||
|
var counter = 0
|
||||||
|
var offset = 0
|
||||||
|
while (offset < data.size) {
|
||||||
|
val block = chacha20Block(key, counter, nonce)
|
||||||
|
val len = minOf(64, data.size - offset)
|
||||||
|
for (i in 0 until len) out[offset + i] = (data[offset + i].toInt() xor block[i].toInt()).toByte()
|
||||||
|
offset += len
|
||||||
|
counter++
|
||||||
|
}
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun nip44CalcPaddedLen(len: Int): Int {
|
||||||
|
if (len <= 32) return 32
|
||||||
|
val nextPower = 1 shl (Math.floor(Math.log((len - 1).toDouble()) / Math.log(2.0)).toInt() + 1)
|
||||||
|
val chunk = if (nextPower <= 256) 32 else nextPower / 8
|
||||||
|
return chunk * ((len - 1) / chunk + 1)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun nip44Pad(plaintext: String): ByteArray {
|
||||||
|
val unpadded = plaintext.toByteArray(StandardCharsets.UTF_8)
|
||||||
|
val len = unpadded.size
|
||||||
|
val padded = ByteArray(2 + nip44CalcPaddedLen(len))
|
||||||
|
padded[0] = (len ushr 8).toByte()
|
||||||
|
padded[1] = len.toByte()
|
||||||
|
System.arraycopy(unpadded, 0, padded, 2, len)
|
||||||
|
return padded
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun nip44Unpad(padded: ByteArray): String {
|
||||||
|
val len = ((padded[0].toInt() and 0xFF) shl 8) or (padded[1].toInt() and 0xFF)
|
||||||
|
if (len == 0 || len > padded.size - 2) return ""
|
||||||
|
return String(padded, 2, len, StandardCharsets.UTF_8)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun encryptNip44(plaintext: String, conversationKey: ByteArray): String {
|
||||||
|
return try {
|
||||||
|
val nonce = ByteArray(32).also { SecureRandom().nextBytes(it) }
|
||||||
|
val keys = hkdfExpand(conversationKey, nonce, 76)
|
||||||
|
val chachaKey = keys.sliceArray(0 until 32)
|
||||||
|
val chachaNonce = keys.sliceArray(32 until 44)
|
||||||
|
val hmacKey = keys.sliceArray(44 until 76)
|
||||||
|
val padded = nip44Pad(plaintext)
|
||||||
|
val ciphertext = chacha20(chachaKey, chachaNonce, padded)
|
||||||
|
val mac = hmacSha256(hmacKey, nonce, ciphertext)
|
||||||
|
val payload = ByteArray(1 + 32 + ciphertext.size + 32)
|
||||||
|
payload[0] = 2
|
||||||
|
System.arraycopy(nonce, 0, payload, 1, 32)
|
||||||
|
System.arraycopy(ciphertext, 0, payload, 33, ciphertext.size)
|
||||||
|
System.arraycopy(mac, 0, payload, 33 + ciphertext.size, 32)
|
||||||
|
Base64.encodeToString(payload, Base64.NO_WRAP)
|
||||||
|
} catch (_: Exception) {
|
||||||
|
""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun decryptNip44(payload: String, conversationKey: ByteArray): String {
|
||||||
|
return try {
|
||||||
|
if (payload.isEmpty() || payload[0] == '#') return ""
|
||||||
|
val data = Base64.decode(payload, Base64.NO_WRAP)
|
||||||
|
if (data.size < 99 || data[0] != 2.toByte()) return ""
|
||||||
|
val nonce = data.sliceArray(1 until 33)
|
||||||
|
val ciphertext = data.sliceArray(33 until data.size - 32)
|
||||||
|
val mac = data.sliceArray(data.size - 32 until data.size)
|
||||||
|
val keys = hkdfExpand(conversationKey, nonce, 76)
|
||||||
|
val chachaKey = keys.sliceArray(0 until 32)
|
||||||
|
val chachaNonce = keys.sliceArray(32 until 44)
|
||||||
|
val hmacKey = keys.sliceArray(44 until 76)
|
||||||
|
val expectedMac = hmacSha256(hmacKey, nonce, ciphertext)
|
||||||
|
if (!expectedMac.contentEquals(mac)) return ""
|
||||||
|
val padded = chacha20(chachaKey, chachaNonce, ciphertext)
|
||||||
|
nip44Unpad(padded)
|
||||||
|
} catch (_: Exception) {
|
||||||
|
""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---- Signing ----
|
||||||
|
|
||||||
|
private fun signWithNip01Secret(secretHex: String, eventJson: String, expectedPubkey: String): String {
|
||||||
|
return try {
|
||||||
|
val secret = hexToBytes(secretHex)
|
||||||
|
if (secret.size != 32) return ""
|
||||||
|
|
||||||
|
val event = JSONObject(eventJson)
|
||||||
|
var pubkey = event.optString("pubkey", expectedPubkey)
|
||||||
|
if (pubkey.isEmpty()) pubkey = deriveXOnlyPubkey(secretHex)
|
||||||
|
if (pubkey.isEmpty()) return ""
|
||||||
|
|
||||||
|
event.put("pubkey", pubkey)
|
||||||
|
val id = computeEventId(event)
|
||||||
|
if (id.isEmpty()) return ""
|
||||||
|
|
||||||
|
val sig = schnorrSign(secretHex, id)
|
||||||
|
if (sig.isEmpty()) return ""
|
||||||
|
|
||||||
|
event.put("id", id)
|
||||||
|
event.put("sig", sig)
|
||||||
|
event.toString()
|
||||||
|
} catch (_: Exception) {
|
||||||
|
""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun signWithNip55ContentResolver(packageName: String, eventJson: String, pubkey: String): String {
|
||||||
|
val uri = Uri.parse("content://$packageName.SIGN_EVENT")
|
||||||
|
var cursor: Cursor? = null
|
||||||
|
return try {
|
||||||
|
cursor = applicationContext.contentResolver.query(uri, arrayOf(eventJson, "", pubkey), "1", null, null)
|
||||||
|
if (cursor == null || !cursor.moveToFirst()) return ""
|
||||||
|
val rejIdx = cursor.getColumnIndex("rejected")
|
||||||
|
if (rejIdx >= 0) {
|
||||||
|
val v = cursor.getString(rejIdx)
|
||||||
|
if (v == "1" || v.equals("true", ignoreCase = true)) return REJECTED
|
||||||
|
}
|
||||||
|
val eventIdx = cursor.getColumnIndex("event")
|
||||||
|
if (eventIdx >= 0) cursor.getString(eventIdx) ?: "" else ""
|
||||||
|
} catch (_: Exception) {
|
||||||
|
""
|
||||||
|
} finally {
|
||||||
|
cursor?.close()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---- Data types ----
|
||||||
|
|
||||||
|
private data class SessionInfo(
|
||||||
|
val method: String,
|
||||||
|
val pubkey: String,
|
||||||
|
val session: JSONObject,
|
||||||
|
)
|
||||||
|
|
||||||
|
private data class Subscription(
|
||||||
|
val relay: String,
|
||||||
|
val key: String,
|
||||||
|
val filters: JSONArray,
|
||||||
|
val ignore: JSONArray?,
|
||||||
|
)
|
||||||
|
|
||||||
|
private class RelayResult {
|
||||||
|
val events = mutableListOf<JSONObject>()
|
||||||
|
var lastCursor = 0L
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---- Relay listener ----
|
||||||
|
|
||||||
|
private inner class RelayListener(
|
||||||
|
private val sub: Subscription,
|
||||||
|
private val since: Long,
|
||||||
|
private val sessionInfo: SessionInfo,
|
||||||
|
private val result: RelayResult,
|
||||||
|
private val latch: CountDownLatch,
|
||||||
|
private val pool: SocketPool,
|
||||||
|
) : WebSocketListener() {
|
||||||
|
private val subId = UUID.randomUUID().toString().replace("-", "")
|
||||||
|
private var done = false
|
||||||
|
private var authed = false
|
||||||
|
private var authEventId = ""
|
||||||
|
private var nip46InFlight = false
|
||||||
|
private var pendingDone = false
|
||||||
|
|
||||||
|
override fun onOpen(webSocket: WebSocket, response: Response) {
|
||||||
|
sendReq(webSocket)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun sendReq(webSocket: WebSocket) {
|
||||||
|
val req = JSONArray()
|
||||||
|
req.put("REQ")
|
||||||
|
req.put(subId)
|
||||||
|
|
||||||
|
for (i in 0 until sub.filters.length()) {
|
||||||
|
val filter = sub.filters.optJSONObject(i) ?: continue
|
||||||
|
val shaped = JSONObject(filter.toString())
|
||||||
|
if (since > 0) shaped.put("since", since + 1)
|
||||||
|
shaped.put("limit", 1)
|
||||||
|
req.put(shaped)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (req.length() <= 2) { finish(); return }
|
||||||
|
|
||||||
|
send(webSocket, req.toString())
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onMessage(webSocket: WebSocket, text: String) {
|
||||||
|
try {
|
||||||
|
val message = JSONArray(text)
|
||||||
|
Log.d(TAG, "Received message from ${sub.relay}: $text")
|
||||||
|
when (message.optString(0, "")) {
|
||||||
|
"EVENT" -> {
|
||||||
|
val event = message.optJSONObject(2) ?: return
|
||||||
|
if (!matchesAnyFilter(sub.filters, event)) return
|
||||||
|
if (isIgnored(event)) return
|
||||||
|
result.events.add(event)
|
||||||
|
val createdAt = event.optLong("created_at", 0L)
|
||||||
|
if (createdAt > result.lastCursor) result.lastCursor = createdAt
|
||||||
|
}
|
||||||
|
"AUTH" -> {
|
||||||
|
// Only auth once per connection
|
||||||
|
if (!authed) {
|
||||||
|
authed = true
|
||||||
|
tryAuth(webSocket, message.optString(1, ""))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
"OK" -> {
|
||||||
|
val okId = message.optString(1, "")
|
||||||
|
val accepted = message.optBoolean(2, false)
|
||||||
|
if (accepted && okId == authEventId) sendReq(webSocket)
|
||||||
|
}
|
||||||
|
"EOSE" -> {
|
||||||
|
send(webSocket, JSONArray().apply { put("CLOSE"); put(subId) }.toString())
|
||||||
|
finish()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (_: Exception) {
|
||||||
|
finish()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onFailure(webSocket: WebSocket, t: Throwable, response: Response?) = finish()
|
||||||
|
override fun onClosed(webSocket: WebSocket, code: Int, reason: String) = finish()
|
||||||
|
|
||||||
|
private fun finish() {
|
||||||
|
if (done) return
|
||||||
|
if (nip46InFlight) { pendingDone = true; return }
|
||||||
|
done = true
|
||||||
|
latch.countDown()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun isIgnored(event: JSONObject): Boolean {
|
||||||
|
val ignore = sub.ignore ?: return false
|
||||||
|
for (i in 0 until ignore.length()) {
|
||||||
|
val filter = ignore.optJSONObject(i) ?: continue
|
||||||
|
if (matchesFilter(filter, event)) return true
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun matchesAnyFilter(filters: JSONArray, event: JSONObject): Boolean {
|
||||||
|
for (i in 0 until filters.length()) {
|
||||||
|
val filter = filters.optJSONObject(i) ?: continue
|
||||||
|
if (matchesFilter(filter, event)) return true
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---- NIP-42 auth ----
|
||||||
|
|
||||||
|
private fun tryAuth(webSocket: WebSocket, challenge: String) {
|
||||||
|
if (challenge.isEmpty()) return
|
||||||
|
when (sessionInfo.method) {
|
||||||
|
"nip01" -> tryNip01Auth(webSocket, challenge)
|
||||||
|
"nip55" -> tryNip55Auth(webSocket, challenge)
|
||||||
|
"nip46" -> tryNip46Auth(webSocket, challenge)
|
||||||
|
// Pomade background auth is not supported: properly delegating to the Pomade signer
|
||||||
|
// from a background worker is complex, usage is rare, and relays that require auth
|
||||||
|
// may still be readable without it.
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun buildAuthEvent(challenge: String): JSONObject {
|
||||||
|
return JSONObject().apply {
|
||||||
|
put("kind", KIND_RELAY_AUTH)
|
||||||
|
put("pubkey", sessionInfo.pubkey)
|
||||||
|
put("created_at", System.currentTimeMillis() / 1000L)
|
||||||
|
put("content", "")
|
||||||
|
put("id", "")
|
||||||
|
put("sig", "")
|
||||||
|
put("tags", JSONArray().apply {
|
||||||
|
put(JSONArray().apply { put("relay"); put(sub.relay) })
|
||||||
|
put(JSONArray().apply { put("challenge"); put(challenge) })
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun sendAuthMessage(webSocket: WebSocket, signedEventJson: String): Boolean {
|
||||||
|
if (signedEventJson.isEmpty() || signedEventJson == REJECTED) return false
|
||||||
|
return try {
|
||||||
|
val event = JSONObject(signedEventJson)
|
||||||
|
authEventId = event.optString("id", "")
|
||||||
|
send(webSocket, JSONArray().apply { put("AUTH"); put(event) }.toString())
|
||||||
|
} catch (_: Exception) {
|
||||||
|
false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun send(webSocket: WebSocket, message: String): Boolean {
|
||||||
|
Log.d(TAG, "Sending message to ${webSocket.request().url}: $message")
|
||||||
|
return webSocket.send(message)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun tryNip01Auth(webSocket: WebSocket, challenge: String): Boolean {
|
||||||
|
val secret = sessionInfo.session.optString("secret", "")
|
||||||
|
if (secret.isEmpty()) return false
|
||||||
|
val signed = signWithNip01Secret(secret, buildAuthEvent(challenge).toString(), sessionInfo.pubkey)
|
||||||
|
return sendAuthMessage(webSocket, signed)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun tryNip55Auth(webSocket: WebSocket, challenge: String): Boolean {
|
||||||
|
val signerPackage = sessionInfo.session.optString("signer", "")
|
||||||
|
if (signerPackage.isEmpty()) return false
|
||||||
|
val signed = signWithNip55ContentResolver(signerPackage, buildAuthEvent(challenge).toString(), sessionInfo.pubkey)
|
||||||
|
return sendAuthMessage(webSocket, signed)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun tryNip46Auth(webSocket: WebSocket, challenge: String): Boolean {
|
||||||
|
val handler = sessionInfo.session.optJSONObject("handler") ?: return false
|
||||||
|
val clientSecret = sessionInfo.session.optString("secret", "")
|
||||||
|
val signerPubkey = handler.optString("pubkey", "")
|
||||||
|
val relays = handler.optJSONArray("relays")
|
||||||
|
|
||||||
|
if (clientSecret.isEmpty() || signerPubkey.isEmpty() || relays == null || relays.length() == 0) return false
|
||||||
|
|
||||||
|
val clientPubkey = deriveXOnlyPubkey(clientSecret)
|
||||||
|
if (clientPubkey.isEmpty()) return false
|
||||||
|
|
||||||
|
val authEventJson = buildAuthEvent(challenge).toString()
|
||||||
|
|
||||||
|
nip46InFlight = true
|
||||||
|
var success = false
|
||||||
|
try {
|
||||||
|
for (i in 0 until relays.length()) {
|
||||||
|
val signerRelay = relays.optString(i, "").trim()
|
||||||
|
if (!signerRelay.startsWith("wss://") && !signerRelay.startsWith("ws://")) continue
|
||||||
|
if (tryNip46ViaRelay(webSocket, signerRelay, clientSecret, clientPubkey, signerPubkey, authEventJson)) { success = true; break }
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
nip46InFlight = false
|
||||||
|
if (pendingDone) finish()
|
||||||
|
}
|
||||||
|
|
||||||
|
return success
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun tryNip46ViaRelay(
|
||||||
|
relaySocket: WebSocket,
|
||||||
|
signerRelay: String,
|
||||||
|
clientSecret: String,
|
||||||
|
clientPubkey: String,
|
||||||
|
signerPubkey: String,
|
||||||
|
authEventJson: String,
|
||||||
|
): Boolean {
|
||||||
|
val localLatch = CountDownLatch(1)
|
||||||
|
val signedEvent = StringBuilder()
|
||||||
|
val requestId = UUID.randomUUID().toString().replace("-", "")
|
||||||
|
|
||||||
|
val signerSocket = pool.open(signerRelay, object : WebSocketListener() {
|
||||||
|
private var done = false
|
||||||
|
|
||||||
|
override fun onOpen(webSocket: WebSocket, response: Response) {
|
||||||
|
try {
|
||||||
|
val rpcEnvelope = JSONObject().apply {
|
||||||
|
put("kind", KIND_NIP46_RPC)
|
||||||
|
put("pubkey", clientPubkey)
|
||||||
|
put("created_at", System.currentTimeMillis() / 1000L)
|
||||||
|
put("content", encryptNip44(
|
||||||
|
JSONObject().apply {
|
||||||
|
put("id", requestId)
|
||||||
|
put("method", "sign_event")
|
||||||
|
put("params", JSONArray().apply { put(authEventJson) })
|
||||||
|
}.toString(),
|
||||||
|
nip44ConversationKey(clientSecret, signerPubkey),
|
||||||
|
))
|
||||||
|
put("id", "")
|
||||||
|
put("sig", "")
|
||||||
|
put("tags", JSONArray().apply { put(JSONArray().apply { put("p"); put(signerPubkey) }) })
|
||||||
|
}
|
||||||
|
|
||||||
|
val signedEnvelope = signWithNip01Secret(clientSecret, rpcEnvelope.toString(), clientPubkey)
|
||||||
|
if (signedEnvelope.isEmpty()) { finish(); return }
|
||||||
|
|
||||||
|
val sentAt = System.currentTimeMillis() / 1000L
|
||||||
|
send(webSocket, JSONArray().apply { put("EVENT"); put(JSONObject(signedEnvelope)) }.toString())
|
||||||
|
send(webSocket, JSONArray().apply {
|
||||||
|
put("REQ")
|
||||||
|
put(requestId)
|
||||||
|
put(JSONObject().apply {
|
||||||
|
put("#p", JSONArray().apply { put(clientPubkey) })
|
||||||
|
put("kinds", JSONArray().apply { put(KIND_NIP46_RPC) })
|
||||||
|
put("since", sentAt)
|
||||||
|
put("limit", 10)
|
||||||
|
})
|
||||||
|
}.toString())
|
||||||
|
} catch (_: Exception) {
|
||||||
|
finish()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onMessage(webSocket: WebSocket, text: String) {
|
||||||
|
try {
|
||||||
|
val message = JSONArray(text)
|
||||||
|
val msgType = message.optString(0, "")
|
||||||
|
Log.d(TAG, "NIP-46 signer message: $msgType from ${webSocket.request().url}")
|
||||||
|
if (msgType != "EVENT") return
|
||||||
|
val event = message.optJSONObject(2) ?: return
|
||||||
|
|
||||||
|
val tags = event.optJSONArray("tags")
|
||||||
|
var hasP = false
|
||||||
|
if (tags != null) {
|
||||||
|
for (i in 0 until tags.length()) {
|
||||||
|
val tag = tags.optJSONArray(i) ?: continue
|
||||||
|
if (tag.optString(0, "") == "p" && tag.optString(1, "") == clientPubkey) { hasP = true; break }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!hasP) { Log.d(TAG, "NIP-46 event missing p tag for client"); return }
|
||||||
|
|
||||||
|
val decryptedContent = decryptNip44(event.optString("content", ""), nip44ConversationKey(clientSecret, signerPubkey))
|
||||||
|
Log.d(TAG, "NIP-46 decrypted response: $decryptedContent")
|
||||||
|
if (decryptedContent.isEmpty()) return
|
||||||
|
val payload = JSONObject(decryptedContent)
|
||||||
|
if (requestId == payload.optString("id", "")) {
|
||||||
|
val result = payload.optString("result", "")
|
||||||
|
if (result.isNotEmpty()) {
|
||||||
|
signedEvent.setLength(0)
|
||||||
|
signedEvent.append(result)
|
||||||
|
finish()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Log.e(TAG, "NIP-46 signer message error", e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onFailure(webSocket: WebSocket, t: Throwable, response: Response?) = finish()
|
||||||
|
override fun onClosed(webSocket: WebSocket, code: Int, reason: String) = finish()
|
||||||
|
|
||||||
|
private fun finish() {
|
||||||
|
if (!done) { done = true; localLatch.countDown() }
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
try {
|
||||||
|
localLatch.await(5, TimeUnit.SECONDS)
|
||||||
|
} catch (_: InterruptedException) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
if (signedEvent.isEmpty()) return false
|
||||||
|
|
||||||
|
val authEvent = JSONObject(signedEvent.toString())
|
||||||
|
authEventId = authEvent.optString("id", "")
|
||||||
|
val authMessage = JSONArray().apply { put("AUTH"); put(authEvent) }.toString()
|
||||||
|
Log.d(TAG, "NIP-46 sending AUTH to relay ${relaySocket.request().url}: $authMessage")
|
||||||
|
return try {
|
||||||
|
relaySocket.send(authMessage)
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Log.e(TAG, "NIP-46 failed to send AUTH", e)
|
||||||
|
false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
// Top-level build file where you can add configuration options common to all sub-projects/modules.
|
// Top-level build file where you can add configuration options common to all sub-projects/modules.
|
||||||
|
|
||||||
buildscript {
|
buildscript {
|
||||||
|
ext.kotlin_version = '2.2.20'
|
||||||
|
|
||||||
repositories {
|
repositories {
|
||||||
google()
|
google()
|
||||||
@@ -9,6 +10,7 @@ buildscript {
|
|||||||
dependencies {
|
dependencies {
|
||||||
classpath 'com.android.tools.build:gradle:8.13.2'
|
classpath 'com.android.tools.build:gradle:8.13.2'
|
||||||
classpath 'com.google.gms:google-services:4.4.4'
|
classpath 'com.google.gms:google-services:4.4.4'
|
||||||
|
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
|
||||||
|
|
||||||
// NOTE: Do not place your application dependencies here; they belong
|
// NOTE: Do not place your application dependencies here; they belong
|
||||||
// in the individual module build.gradle files
|
// in the individual module build.gradle files
|
||||||
|
|||||||
@@ -2,6 +2,9 @@
|
|||||||
include ':capacitor-android'
|
include ':capacitor-android'
|
||||||
project(':capacitor-android').projectDir = new File('../node_modules/.pnpm/@capacitor+android@8.0.1_@capacitor+core@8.0.1/node_modules/@capacitor/android/capacitor')
|
project(':capacitor-android').projectDir = new File('../node_modules/.pnpm/@capacitor+android@8.0.1_@capacitor+core@8.0.1/node_modules/@capacitor/android/capacitor')
|
||||||
|
|
||||||
|
include ':aparajita-capacitor-secure-storage'
|
||||||
|
project(':aparajita-capacitor-secure-storage').projectDir = new File('../node_modules/.pnpm/@aparajita+capacitor-secure-storage@8.0.0/node_modules/@aparajita/capacitor-secure-storage/android')
|
||||||
|
|
||||||
include ':capacitor-community-safe-area'
|
include ':capacitor-community-safe-area'
|
||||||
project(':capacitor-community-safe-area').projectDir = new File('../node_modules/.pnpm/@capacitor-community+safe-area@8.0.1_@capacitor+core@8.0.1/node_modules/@capacitor-community/safe-area/android')
|
project(':capacitor-community-safe-area').projectDir = new File('../node_modules/.pnpm/@capacitor-community+safe-area@8.0.1_@capacitor+core@8.0.1/node_modules/@capacitor-community/safe-area/android')
|
||||||
|
|
||||||
|
|||||||
@@ -4,6 +4,9 @@ const config: CapacitorConfig = {
|
|||||||
appId: "social.flotilla",
|
appId: "social.flotilla",
|
||||||
appName: "Flotilla",
|
appName: "Flotilla",
|
||||||
webDir: "build",
|
webDir: "build",
|
||||||
|
ios: {
|
||||||
|
scheme: "Flotilla Chat",
|
||||||
|
},
|
||||||
android: {
|
android: {
|
||||||
adjustMarginsForEdgeToEdge: true,
|
adjustMarginsForEdgeToEdge: true,
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -358,14 +358,14 @@
|
|||||||
CODE_SIGN_ENTITLEMENTS = "Flotilla Chat.entitlements";
|
CODE_SIGN_ENTITLEMENTS = "Flotilla Chat.entitlements";
|
||||||
CODE_SIGN_IDENTITY = "Apple Development";
|
CODE_SIGN_IDENTITY = "Apple Development";
|
||||||
CODE_SIGN_STYLE = Automatic;
|
CODE_SIGN_STYLE = Automatic;
|
||||||
CURRENT_PROJECT_VERSION = 32;
|
CURRENT_PROJECT_VERSION = 35;
|
||||||
DEVELOPMENT_TEAM = S26U9DYW3A;
|
DEVELOPMENT_TEAM = S26U9DYW3A;
|
||||||
INFOPLIST_FILE = App/Info.plist;
|
INFOPLIST_FILE = App/Info.plist;
|
||||||
INFOPLIST_KEY_CFBundleDisplayName = "Flotilla Chat";
|
INFOPLIST_KEY_CFBundleDisplayName = "Flotilla Chat";
|
||||||
INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.social-networking";
|
INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.social-networking";
|
||||||
IPHONEOS_DEPLOYMENT_TARGET = 15.0;
|
IPHONEOS_DEPLOYMENT_TARGET = 15.0;
|
||||||
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks";
|
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks";
|
||||||
MARKETING_VERSION = 1.6.5;
|
MARKETING_VERSION = 1.7.2;
|
||||||
OTHER_SWIFT_FLAGS = "$(inherited) \"-D\" \"COCOAPODS\" \"-DDEBUG\"";
|
OTHER_SWIFT_FLAGS = "$(inherited) \"-D\" \"COCOAPODS\" \"-DDEBUG\"";
|
||||||
PRODUCT_BUNDLE_IDENTIFIER = social.flotilla;
|
PRODUCT_BUNDLE_IDENTIFIER = social.flotilla;
|
||||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||||
@@ -385,14 +385,14 @@
|
|||||||
CODE_SIGN_ENTITLEMENTS = "Flotilla Chat.entitlements";
|
CODE_SIGN_ENTITLEMENTS = "Flotilla Chat.entitlements";
|
||||||
CODE_SIGN_IDENTITY = "Apple Development";
|
CODE_SIGN_IDENTITY = "Apple Development";
|
||||||
CODE_SIGN_STYLE = Automatic;
|
CODE_SIGN_STYLE = Automatic;
|
||||||
CURRENT_PROJECT_VERSION = 32;
|
CURRENT_PROJECT_VERSION = 35;
|
||||||
DEVELOPMENT_TEAM = S26U9DYW3A;
|
DEVELOPMENT_TEAM = S26U9DYW3A;
|
||||||
INFOPLIST_FILE = App/Info.plist;
|
INFOPLIST_FILE = App/Info.plist;
|
||||||
INFOPLIST_KEY_CFBundleDisplayName = "Flotilla Chat";
|
INFOPLIST_KEY_CFBundleDisplayName = "Flotilla Chat";
|
||||||
INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.social-networking";
|
INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.social-networking";
|
||||||
IPHONEOS_DEPLOYMENT_TARGET = 15.0;
|
IPHONEOS_DEPLOYMENT_TARGET = 15.0;
|
||||||
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks";
|
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks";
|
||||||
MARKETING_VERSION = 1.6.5;
|
MARKETING_VERSION = 1.7.5;
|
||||||
PRODUCT_BUNDLE_IDENTIFIER = social.flotilla;
|
PRODUCT_BUNDLE_IDENTIFIER = social.flotilla;
|
||||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||||
PROVISIONING_PROFILE_SPECIFIER = "";
|
PROVISIONING_PROFILE_SPECIFIER = "";
|
||||||
|
|||||||
@@ -20,8 +20,16 @@
|
|||||||
<string>$(MARKETING_VERSION)</string>
|
<string>$(MARKETING_VERSION)</string>
|
||||||
<key>CFBundleVersion</key>
|
<key>CFBundleVersion</key>
|
||||||
<string>$(CURRENT_PROJECT_VERSION)</string>
|
<string>$(CURRENT_PROJECT_VERSION)</string>
|
||||||
|
<key>ITSAppUsesNonExemptEncryption</key>
|
||||||
|
<false/>
|
||||||
<key>LSRequiresIPhoneOS</key>
|
<key>LSRequiresIPhoneOS</key>
|
||||||
<true/>
|
<true/>
|
||||||
|
<key>NSMicrophoneUsageDescription</key>
|
||||||
|
<string>Flotilla uses the microphone for voice chat in rooms.</string>
|
||||||
|
<key>UIBackgroundModes</key>
|
||||||
|
<array>
|
||||||
|
<string>remote-notification</string>
|
||||||
|
</array>
|
||||||
<key>UILaunchStoryboardName</key>
|
<key>UILaunchStoryboardName</key>
|
||||||
<string>LaunchScreen</string>
|
<string>LaunchScreen</string>
|
||||||
<key>UIMainStoryboardFile</key>
|
<key>UIMainStoryboardFile</key>
|
||||||
@@ -47,11 +55,5 @@
|
|||||||
</array>
|
</array>
|
||||||
<key>UIViewControllerBasedStatusBarAppearance</key>
|
<key>UIViewControllerBasedStatusBarAppearance</key>
|
||||||
<true/>
|
<true/>
|
||||||
<key>ITSAppUsesNonExemptEncryption</key>
|
|
||||||
<false/>
|
|
||||||
<key>UIBackgroundModes</key>
|
|
||||||
<array>
|
|
||||||
<string>remote-notification</string>
|
|
||||||
</array>
|
|
||||||
</dict>
|
</dict>
|
||||||
</plist>
|
</plist>
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ install! 'cocoapods', :disable_input_output_paths => true
|
|||||||
def capacitor_pods
|
def capacitor_pods
|
||||||
pod 'Capacitor', :path => '../../node_modules/.pnpm/@capacitor+ios@8.0.1_@capacitor+core@8.0.1/node_modules/@capacitor/ios'
|
pod 'Capacitor', :path => '../../node_modules/.pnpm/@capacitor+ios@8.0.1_@capacitor+core@8.0.1/node_modules/@capacitor/ios'
|
||||||
pod 'CapacitorCordova', :path => '../../node_modules/.pnpm/@capacitor+ios@8.0.1_@capacitor+core@8.0.1/node_modules/@capacitor/ios'
|
pod 'CapacitorCordova', :path => '../../node_modules/.pnpm/@capacitor+ios@8.0.1_@capacitor+core@8.0.1/node_modules/@capacitor/ios'
|
||||||
|
pod 'AparajitaCapacitorSecureStorage', :path => '../../node_modules/.pnpm/@aparajita+capacitor-secure-storage@8.0.0/node_modules/@aparajita/capacitor-secure-storage'
|
||||||
pod 'CapacitorCommunitySafeArea', :path => '../../node_modules/.pnpm/@capacitor-community+safe-area@8.0.1_@capacitor+core@8.0.1/node_modules/@capacitor-community/safe-area'
|
pod 'CapacitorCommunitySafeArea', :path => '../../node_modules/.pnpm/@capacitor-community+safe-area@8.0.1_@capacitor+core@8.0.1/node_modules/@capacitor-community/safe-area'
|
||||||
pod 'CapacitorApp', :path => '../../node_modules/.pnpm/@capacitor+app@8.0.0_@capacitor+core@8.0.1/node_modules/@capacitor/app'
|
pod 'CapacitorApp', :path => '../../node_modules/.pnpm/@capacitor+app@8.0.0_@capacitor+core@8.0.1/node_modules/@capacitor/app'
|
||||||
pod 'CapacitorFilesystem', :path => '../../node_modules/.pnpm/@capacitor+filesystem@8.1.0_@capacitor+core@8.0.1/node_modules/@capacitor/filesystem'
|
pod 'CapacitorFilesystem', :path => '../../node_modules/.pnpm/@capacitor+filesystem@8.1.0_@capacitor+core@8.0.1/node_modules/@capacitor/filesystem'
|
||||||
|
|||||||
+14
-12
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "flotilla",
|
"name": "flotilla",
|
||||||
"version": "1.6.5",
|
"version": "1.7.2",
|
||||||
"private": true,
|
"private": true,
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "vite dev",
|
"dev": "vite dev",
|
||||||
@@ -42,6 +42,7 @@
|
|||||||
},
|
},
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@aparajita/capacitor-secure-storage": "^8.0.0",
|
||||||
"@capacitor-community/safe-area": "^8.0.1",
|
"@capacitor-community/safe-area": "^8.0.1",
|
||||||
"@capacitor/android": "^8.0.1",
|
"@capacitor/android": "^8.0.1",
|
||||||
"@capacitor/app": "^8.0.0",
|
"@capacitor/app": "^8.0.0",
|
||||||
@@ -57,7 +58,7 @@
|
|||||||
"@getalby/lightning-tools": "^6.1.0",
|
"@getalby/lightning-tools": "^6.1.0",
|
||||||
"@getalby/sdk": "^5.1.2",
|
"@getalby/sdk": "^5.1.2",
|
||||||
"@noble/curves": "^1.9.7",
|
"@noble/curves": "^1.9.7",
|
||||||
"@pomade/core": "^0.2.1",
|
"@pomade/core": "^0.2.2",
|
||||||
"@poppanator/sveltekit-svg": "^4.2.1",
|
"@poppanator/sveltekit-svg": "^4.2.1",
|
||||||
"@sveltejs/adapter-static": "^3.0.10",
|
"@sveltejs/adapter-static": "^3.0.10",
|
||||||
"@tiptap/core": "^2.27.2",
|
"@tiptap/core": "^2.27.2",
|
||||||
@@ -65,16 +66,16 @@
|
|||||||
"@types/throttle-debounce": "^5.0.2",
|
"@types/throttle-debounce": "^5.0.2",
|
||||||
"@vite-pwa/assets-generator": "^0.2.6",
|
"@vite-pwa/assets-generator": "^0.2.6",
|
||||||
"@vite-pwa/sveltekit": "^0.6.8",
|
"@vite-pwa/sveltekit": "^0.6.8",
|
||||||
"@welshman/app": "^0.8.8",
|
"@welshman/app": "^0.8.12",
|
||||||
"@welshman/content": "^0.8.8",
|
"@welshman/content": "^0.8.12",
|
||||||
"@welshman/editor": "^0.8.8",
|
"@welshman/editor": "^0.8.12",
|
||||||
"@welshman/feeds": "^0.8.8",
|
"@welshman/feeds": "^0.8.12",
|
||||||
"@welshman/lib": "^0.8.8",
|
"@welshman/lib": "^0.8.12",
|
||||||
"@welshman/net": "^0.8.8",
|
"@welshman/net": "^0.8.12",
|
||||||
"@welshman/router": "^0.8.8",
|
"@welshman/router": "^0.8.12",
|
||||||
"@welshman/signer": "^0.8.8",
|
"@welshman/signer": "^0.8.12",
|
||||||
"@welshman/store": "^0.8.8",
|
"@welshman/store": "^0.8.12",
|
||||||
"@welshman/util": "^0.8.8",
|
"@welshman/util": "^0.8.12",
|
||||||
"compressorjs-next": "^1.1.2",
|
"compressorjs-next": "^1.1.2",
|
||||||
"daisyui": "^4.12.24",
|
"daisyui": "^4.12.24",
|
||||||
"date-picker-svelte": "^2.17.0",
|
"date-picker-svelte": "^2.17.0",
|
||||||
@@ -83,6 +84,7 @@
|
|||||||
"fuse.js": "^7.1.0",
|
"fuse.js": "^7.1.0",
|
||||||
"husky": "^9.1.7",
|
"husky": "^9.1.7",
|
||||||
"idb": "^8.0.3",
|
"idb": "^8.0.3",
|
||||||
|
"livekit-client": "^2.17.2",
|
||||||
"nostr-signer-capacitor-plugin": "github:coracle-social/nostr-signer-capacitor-plugin#main",
|
"nostr-signer-capacitor-plugin": "github:coracle-social/nostr-signer-capacitor-plugin#main",
|
||||||
"nostr-tools": "^2.19.4",
|
"nostr-tools": "^2.19.4",
|
||||||
"prettier-plugin-tailwindcss": "^0.6.14",
|
"prettier-plugin-tailwindcss": "^0.6.14",
|
||||||
|
|||||||
Generated
+285
-115
@@ -11,6 +11,9 @@ importers:
|
|||||||
|
|
||||||
.:
|
.:
|
||||||
dependencies:
|
dependencies:
|
||||||
|
'@aparajita/capacitor-secure-storage':
|
||||||
|
specifier: ^8.0.0
|
||||||
|
version: 8.0.0
|
||||||
'@capacitor-community/safe-area':
|
'@capacitor-community/safe-area':
|
||||||
specifier: ^8.0.1
|
specifier: ^8.0.1
|
||||||
version: 8.0.1(@capacitor/core@8.0.1)
|
version: 8.0.1(@capacitor/core@8.0.1)
|
||||||
@@ -57,8 +60,8 @@ importers:
|
|||||||
specifier: ^1.9.7
|
specifier: ^1.9.7
|
||||||
version: 1.9.7
|
version: 1.9.7
|
||||||
'@pomade/core':
|
'@pomade/core':
|
||||||
specifier: ^0.2.1
|
specifier: ^0.2.2
|
||||||
version: 0.2.1(@frostr/bifrost@1.0.7(typescript@5.9.3))(@noble/hashes@2.0.1)(@welshman/lib@0.8.8)(@welshman/net@0.8.8(@welshman/lib@0.8.8)(@welshman/util@0.8.8(@noble/curves@1.9.7)(@welshman/lib@0.8.8)(nostr-tools@2.20.0(typescript@5.9.3)))(ws@8.18.3))(@welshman/signer@0.8.8(@noble/curves@1.9.7)(@noble/hashes@2.0.1)(@welshman/lib@0.8.8)(@welshman/net@0.8.8(@welshman/lib@0.8.8)(@welshman/util@0.8.8(@noble/curves@1.9.7)(@welshman/lib@0.8.8)(nostr-tools@2.20.0(typescript@5.9.3)))(ws@8.18.3))(@welshman/util@0.8.8(@noble/curves@1.9.7)(@welshman/lib@0.8.8)(nostr-tools@2.20.0(typescript@5.9.3)))(nostr-signer-capacitor-plugin@https://codeload.github.com/coracle-social/nostr-signer-capacitor-plugin/tar.gz/be4bb90a1a15c8eec0934f2f66ce9e82ecc72d51(@capacitor/core@8.0.1))(nostr-tools@2.20.0(typescript@5.9.3)))(@welshman/util@0.8.8(@noble/curves@1.9.7)(@welshman/lib@0.8.8)(nostr-tools@2.20.0(typescript@5.9.3)))(nostr-tools@2.20.0(typescript@5.9.3))
|
version: 0.2.2(@frostr/bifrost@1.0.7(typescript@5.9.3))(@noble/hashes@2.0.1)(@welshman/lib@0.8.12)(@welshman/net@0.8.12(@welshman/lib@0.8.12)(@welshman/util@0.8.12(@noble/curves@1.9.7)(@welshman/lib@0.8.12)(nostr-tools@2.20.0(typescript@5.9.3)))(ws@8.18.3))(@welshman/signer@0.8.12(@noble/curves@1.9.7)(@noble/hashes@2.0.1)(@welshman/lib@0.8.12)(@welshman/net@0.8.12(@welshman/lib@0.8.12)(@welshman/util@0.8.12(@noble/curves@1.9.7)(@welshman/lib@0.8.12)(nostr-tools@2.20.0(typescript@5.9.3)))(ws@8.18.3))(@welshman/util@0.8.12(@noble/curves@1.9.7)(@welshman/lib@0.8.12)(nostr-tools@2.20.0(typescript@5.9.3)))(nostr-signer-capacitor-plugin@https://codeload.github.com/coracle-social/nostr-signer-capacitor-plugin/tar.gz/be4bb90a1a15c8eec0934f2f66ce9e82ecc72d51(@capacitor/core@8.0.1))(nostr-tools@2.20.0(typescript@5.9.3)))(@welshman/util@0.8.12(@noble/curves@1.9.7)(@welshman/lib@0.8.12)(nostr-tools@2.20.0(typescript@5.9.3)))(nostr-tools@2.20.0(typescript@5.9.3))
|
||||||
'@poppanator/sveltekit-svg':
|
'@poppanator/sveltekit-svg':
|
||||||
specifier: ^4.2.1
|
specifier: ^4.2.1
|
||||||
version: 4.2.1(rollup@2.80.0)(svelte@5.48.0)(svgo@3.3.2)(vite@5.4.21(@types/node@25.0.10)(terser@5.46.0))
|
version: 4.2.1(rollup@2.80.0)(svelte@5.48.0)(svgo@3.3.2)(vite@5.4.21(@types/node@25.0.10)(terser@5.46.0))
|
||||||
@@ -81,35 +84,35 @@ importers:
|
|||||||
specifier: ^0.6.8
|
specifier: ^0.6.8
|
||||||
version: 0.6.8(@sveltejs/kit@2.50.1(@sveltejs/vite-plugin-svelte@4.0.4(svelte@5.48.0)(vite@5.4.21(@types/node@25.0.10)(terser@5.46.0)))(svelte@5.48.0)(typescript@5.9.3)(vite@5.4.21(@types/node@25.0.10)(terser@5.46.0)))(@vite-pwa/assets-generator@0.2.6)(vite-plugin-pwa@0.21.2(@vite-pwa/assets-generator@0.2.6)(vite@5.4.21(@types/node@25.0.10)(terser@5.46.0))(workbox-build@7.3.0)(workbox-window@7.3.0))
|
version: 0.6.8(@sveltejs/kit@2.50.1(@sveltejs/vite-plugin-svelte@4.0.4(svelte@5.48.0)(vite@5.4.21(@types/node@25.0.10)(terser@5.46.0)))(svelte@5.48.0)(typescript@5.9.3)(vite@5.4.21(@types/node@25.0.10)(terser@5.46.0)))(@vite-pwa/assets-generator@0.2.6)(vite-plugin-pwa@0.21.2(@vite-pwa/assets-generator@0.2.6)(vite@5.4.21(@types/node@25.0.10)(terser@5.46.0))(workbox-build@7.3.0)(workbox-window@7.3.0))
|
||||||
'@welshman/app':
|
'@welshman/app':
|
||||||
specifier: ^0.8.8
|
specifier: ^0.8.12
|
||||||
version: 0.8.8(b90dd618d8ad3ba87405490e903259ce)
|
version: 0.8.12(3074ef6691f94dc03952d8dbc98013a7)
|
||||||
'@welshman/content':
|
'@welshman/content':
|
||||||
specifier: ^0.8.8
|
specifier: ^0.8.12
|
||||||
version: 0.8.8(nostr-tools@2.20.0(typescript@5.9.3))
|
version: 0.8.12(nostr-tools@2.20.0(typescript@5.9.3))
|
||||||
'@welshman/editor':
|
'@welshman/editor':
|
||||||
specifier: ^0.8.8
|
specifier: ^0.8.12
|
||||||
version: 0.8.8(@welshman/lib@0.8.8)(@welshman/util@0.8.8(@noble/curves@1.9.7)(@welshman/lib@0.8.8)(nostr-tools@2.20.0(typescript@5.9.3)))(nostr-editor@1.1.1(@tiptap/core@2.27.2(@tiptap/pm@2.27.2))(@tiptap/extension-image@2.27.1(@tiptap/core@2.27.2(@tiptap/pm@2.27.2)))(@tiptap/extension-link@2.27.1(@tiptap/core@2.27.2(@tiptap/pm@2.27.2))(@tiptap/pm@2.27.2))(@tiptap/pm@2.27.2)(linkifyjs@4.3.2)(nostr-tools@2.20.0(typescript@5.9.3))(prosemirror-markdown@1.13.3)(prosemirror-model@1.25.4)(prosemirror-state@1.4.4)(prosemirror-view@1.41.5)(tiptap-markdown@0.8.10(@tiptap/core@2.27.2(@tiptap/pm@2.27.2))))(nostr-tools@2.20.0(typescript@5.9.3))
|
version: 0.8.12(@welshman/lib@0.8.12)(@welshman/util@0.8.12(@noble/curves@1.9.7)(@welshman/lib@0.8.12)(nostr-tools@2.20.0(typescript@5.9.3)))(nostr-editor@1.1.1(@tiptap/core@2.27.2(@tiptap/pm@2.27.2))(@tiptap/extension-image@2.27.1(@tiptap/core@2.27.2(@tiptap/pm@2.27.2)))(@tiptap/extension-link@2.27.1(@tiptap/core@2.27.2(@tiptap/pm@2.27.2))(@tiptap/pm@2.27.2))(@tiptap/pm@2.27.2)(linkifyjs@4.3.2)(nostr-tools@2.20.0(typescript@5.9.3))(prosemirror-markdown@1.13.3)(prosemirror-model@1.25.4)(prosemirror-state@1.4.4)(prosemirror-view@1.41.5)(tiptap-markdown@0.8.10(@tiptap/core@2.27.2(@tiptap/pm@2.27.2))))(nostr-tools@2.20.0(typescript@5.9.3))
|
||||||
'@welshman/feeds':
|
'@welshman/feeds':
|
||||||
specifier: ^0.8.8
|
specifier: ^0.8.12
|
||||||
version: 0.8.8(827c582d718d0d373e9315813bab1085)
|
version: 0.8.12(d5b74f0c83250e052e0b96f7ff5804e8)
|
||||||
'@welshman/lib':
|
'@welshman/lib':
|
||||||
specifier: ^0.8.8
|
specifier: ^0.8.12
|
||||||
version: 0.8.8
|
version: 0.8.12
|
||||||
'@welshman/net':
|
'@welshman/net':
|
||||||
specifier: ^0.8.8
|
specifier: ^0.8.12
|
||||||
version: 0.8.8(@welshman/lib@0.8.8)(@welshman/util@0.8.8(@noble/curves@1.9.7)(@welshman/lib@0.8.8)(nostr-tools@2.20.0(typescript@5.9.3)))(ws@8.18.3)
|
version: 0.8.12(@welshman/lib@0.8.12)(@welshman/util@0.8.12(@noble/curves@1.9.7)(@welshman/lib@0.8.12)(nostr-tools@2.20.0(typescript@5.9.3)))(ws@8.18.3)
|
||||||
'@welshman/router':
|
'@welshman/router':
|
||||||
specifier: ^0.8.8
|
specifier: ^0.8.12
|
||||||
version: 0.8.8(@welshman/lib@0.8.8)(@welshman/net@0.8.8(@welshman/lib@0.8.8)(@welshman/util@0.8.8(@noble/curves@1.9.7)(@welshman/lib@0.8.8)(nostr-tools@2.20.0(typescript@5.9.3)))(ws@8.18.3))(@welshman/util@0.8.8(@noble/curves@1.9.7)(@welshman/lib@0.8.8)(nostr-tools@2.20.0(typescript@5.9.3)))
|
version: 0.8.12(@welshman/lib@0.8.12)(@welshman/net@0.8.12(@welshman/lib@0.8.12)(@welshman/util@0.8.12(@noble/curves@1.9.7)(@welshman/lib@0.8.12)(nostr-tools@2.20.0(typescript@5.9.3)))(ws@8.18.3))(@welshman/util@0.8.12(@noble/curves@1.9.7)(@welshman/lib@0.8.12)(nostr-tools@2.20.0(typescript@5.9.3)))
|
||||||
'@welshman/signer':
|
'@welshman/signer':
|
||||||
specifier: ^0.8.8
|
specifier: ^0.8.12
|
||||||
version: 0.8.8(@noble/curves@1.9.7)(@noble/hashes@2.0.1)(@welshman/lib@0.8.8)(@welshman/net@0.8.8(@welshman/lib@0.8.8)(@welshman/util@0.8.8(@noble/curves@1.9.7)(@welshman/lib@0.8.8)(nostr-tools@2.20.0(typescript@5.9.3)))(ws@8.18.3))(@welshman/util@0.8.8(@noble/curves@1.9.7)(@welshman/lib@0.8.8)(nostr-tools@2.20.0(typescript@5.9.3)))(nostr-signer-capacitor-plugin@https://codeload.github.com/coracle-social/nostr-signer-capacitor-plugin/tar.gz/be4bb90a1a15c8eec0934f2f66ce9e82ecc72d51(@capacitor/core@8.0.1))(nostr-tools@2.20.0(typescript@5.9.3))
|
version: 0.8.12(@noble/curves@1.9.7)(@noble/hashes@2.0.1)(@welshman/lib@0.8.12)(@welshman/net@0.8.12(@welshman/lib@0.8.12)(@welshman/util@0.8.12(@noble/curves@1.9.7)(@welshman/lib@0.8.12)(nostr-tools@2.20.0(typescript@5.9.3)))(ws@8.18.3))(@welshman/util@0.8.12(@noble/curves@1.9.7)(@welshman/lib@0.8.12)(nostr-tools@2.20.0(typescript@5.9.3)))(nostr-signer-capacitor-plugin@https://codeload.github.com/coracle-social/nostr-signer-capacitor-plugin/tar.gz/be4bb90a1a15c8eec0934f2f66ce9e82ecc72d51(@capacitor/core@8.0.1))(nostr-tools@2.20.0(typescript@5.9.3))
|
||||||
'@welshman/store':
|
'@welshman/store':
|
||||||
specifier: ^0.8.8
|
specifier: ^0.8.12
|
||||||
version: 0.8.8(@welshman/lib@0.8.8)(@welshman/net@0.8.8(@welshman/lib@0.8.8)(@welshman/util@0.8.8(@noble/curves@1.9.7)(@welshman/lib@0.8.8)(nostr-tools@2.20.0(typescript@5.9.3)))(ws@8.18.3))(@welshman/util@0.8.8(@noble/curves@1.9.7)(@welshman/lib@0.8.8)(nostr-tools@2.20.0(typescript@5.9.3)))(svelte@5.48.0)
|
version: 0.8.12(@welshman/lib@0.8.12)(@welshman/net@0.8.12(@welshman/lib@0.8.12)(@welshman/util@0.8.12(@noble/curves@1.9.7)(@welshman/lib@0.8.12)(nostr-tools@2.20.0(typescript@5.9.3)))(ws@8.18.3))(@welshman/util@0.8.12(@noble/curves@1.9.7)(@welshman/lib@0.8.12)(nostr-tools@2.20.0(typescript@5.9.3)))(svelte@5.48.0)
|
||||||
'@welshman/util':
|
'@welshman/util':
|
||||||
specifier: ^0.8.8
|
specifier: ^0.8.12
|
||||||
version: 0.8.8(@noble/curves@1.9.7)(@welshman/lib@0.8.8)(nostr-tools@2.20.0(typescript@5.9.3))
|
version: 0.8.12(@noble/curves@1.9.7)(@welshman/lib@0.8.12)(nostr-tools@2.20.0(typescript@5.9.3))
|
||||||
compressorjs-next:
|
compressorjs-next:
|
||||||
specifier: ^1.1.2
|
specifier: ^1.1.2
|
||||||
version: 1.1.2
|
version: 1.1.2
|
||||||
@@ -134,6 +137,9 @@ importers:
|
|||||||
idb:
|
idb:
|
||||||
specifier: ^8.0.3
|
specifier: ^8.0.3
|
||||||
version: 8.0.3
|
version: 8.0.3
|
||||||
|
livekit-client:
|
||||||
|
specifier: ^2.17.2
|
||||||
|
version: 2.17.3(@types/dom-mediacapture-record@1.0.22)
|
||||||
nostr-signer-capacitor-plugin:
|
nostr-signer-capacitor-plugin:
|
||||||
specifier: github:coracle-social/nostr-signer-capacitor-plugin#main
|
specifier: github:coracle-social/nostr-signer-capacitor-plugin#main
|
||||||
version: https://codeload.github.com/coracle-social/nostr-signer-capacitor-plugin/tar.gz/be4bb90a1a15c8eec0934f2f66ce9e82ecc72d51(@capacitor/core@8.0.1)
|
version: https://codeload.github.com/coracle-social/nostr-signer-capacitor-plugin/tar.gz/be4bb90a1a15c8eec0934f2f66ce9e82ecc72d51(@capacitor/core@8.0.1)
|
||||||
@@ -229,6 +235,10 @@ packages:
|
|||||||
'@antfu/utils@0.7.10':
|
'@antfu/utils@0.7.10':
|
||||||
resolution: {integrity: sha512-+562v9k4aI80m1+VuMHehNJWLOFjBnXn3tdOitzD0il5b7smkSBal4+a3oKiQTbrwMmN/TBUMDvbdoWDehgOww==}
|
resolution: {integrity: sha512-+562v9k4aI80m1+VuMHehNJWLOFjBnXn3tdOitzD0il5b7smkSBal4+a3oKiQTbrwMmN/TBUMDvbdoWDehgOww==}
|
||||||
|
|
||||||
|
'@aparajita/capacitor-secure-storage@8.0.0':
|
||||||
|
resolution: {integrity: sha512-oYnwSjdIh23aRNgz8982+TmFvQH/2yZkEdw1iIg+H2ziFJoOVELPTc7u6Ez2HwOuDIW5AGqBX75GvrzQ+D70Qg==}
|
||||||
|
engines: {node: '>=20.0.0'}
|
||||||
|
|
||||||
'@apideck/better-ajv-errors@0.3.6':
|
'@apideck/better-ajv-errors@0.3.6':
|
||||||
resolution: {integrity: sha512-P+ZygBLZtkp0qqOAJJVX4oX/sFo5JR3eBWwwuqHHhK0GIgQOKWrAfiAaWX0aArHkRWHMuggFEgAZNxVPwPZYaA==}
|
resolution: {integrity: sha512-P+ZygBLZtkp0qqOAJJVX4oX/sFo5JR3eBWwwuqHHhK0GIgQOKWrAfiAaWX0aArHkRWHMuggFEgAZNxVPwPZYaA==}
|
||||||
engines: {node: '>=10'}
|
engines: {node: '>=10'}
|
||||||
@@ -737,6 +747,9 @@ packages:
|
|||||||
'@braintree/sanitize-url@7.1.1':
|
'@braintree/sanitize-url@7.1.1':
|
||||||
resolution: {integrity: sha512-i1L7noDNxtFyL5DmZafWy1wRVhGehQmzZaz1HiN5e7iylJMSZR7ekOV7NsIqa5qBldlLrsKv4HbgFUVlQrz8Mw==}
|
resolution: {integrity: sha512-i1L7noDNxtFyL5DmZafWy1wRVhGehQmzZaz1HiN5e7iylJMSZR7ekOV7NsIqa5qBldlLrsKv4HbgFUVlQrz8Mw==}
|
||||||
|
|
||||||
|
'@bufbuild/protobuf@1.10.1':
|
||||||
|
resolution: {integrity: sha512-wJ8ReQbHxsAfXhrf9ixl0aYbZorRuOWpBNzm8pL8ftmSxQx/wnJD5Eg861NwJU/czy2VXFIebCeZnZrI9rktIQ==}
|
||||||
|
|
||||||
'@canvas/image-data@1.1.0':
|
'@canvas/image-data@1.1.0':
|
||||||
resolution: {integrity: sha512-QdObRRjRbcXGmM1tmJ+MrHcaz1MftF2+W7YI+MsphnsCrmtyfS0d5qJbk0MeSbUeyM/jCb0hmnkXPsy026L7dA==}
|
resolution: {integrity: sha512-QdObRRjRbcXGmM1tmJ+MrHcaz1MftF2+W7YI+MsphnsCrmtyfS0d5qJbk0MeSbUeyM/jCb0hmnkXPsy026L7dA==}
|
||||||
|
|
||||||
@@ -750,6 +763,11 @@ packages:
|
|||||||
peerDependencies:
|
peerDependencies:
|
||||||
'@capacitor/core': ^8.0.0
|
'@capacitor/core': ^8.0.0
|
||||||
|
|
||||||
|
'@capacitor/android@8.2.0':
|
||||||
|
resolution: {integrity: sha512-XLm5OsWLPfXQxDxzFS7SOdMEgGvW+2c7TGLXkTR2cSKdkWK5Abns4imlT5qghKYhjM9r74IrDkBWg/9ALUGNKQ==}
|
||||||
|
peerDependencies:
|
||||||
|
'@capacitor/core': ^8.2.0
|
||||||
|
|
||||||
'@capacitor/app@8.0.0':
|
'@capacitor/app@8.0.0':
|
||||||
resolution: {integrity: sha512-OwzIkUs4w433Bu9WWAEbEYngXEfJXZ9Wmdb8eoaqzYBgB0W9/3Ed/mh6sAYPNBAZlpyarmewgP7Nb+d3Vrh+xA==}
|
resolution: {integrity: sha512-OwzIkUs4w433Bu9WWAEbEYngXEfJXZ9Wmdb8eoaqzYBgB0W9/3Ed/mh6sAYPNBAZlpyarmewgP7Nb+d3Vrh+xA==}
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
@@ -773,6 +791,9 @@ packages:
|
|||||||
'@capacitor/core@8.0.1':
|
'@capacitor/core@8.0.1':
|
||||||
resolution: {integrity: sha512-5UqSWxGMp/B8KhYu7rAijqNtYslhcLh+TrbfU48PfdMDsPfaU/VY48sMNzC22xL8BmoFoql/3SKyP+pavTOvOA==}
|
resolution: {integrity: sha512-5UqSWxGMp/B8KhYu7rAijqNtYslhcLh+TrbfU48PfdMDsPfaU/VY48sMNzC22xL8BmoFoql/3SKyP+pavTOvOA==}
|
||||||
|
|
||||||
|
'@capacitor/core@8.2.0':
|
||||||
|
resolution: {integrity: sha512-oKaoNeNtH2iIZMDFVrb1atoyRECDGHcfLMunJ5KWN8DtvpVBeeA4c41e20NTuhMxw1cSYbpq2PV2hb+/9CJxlQ==}
|
||||||
|
|
||||||
'@capacitor/filesystem@8.1.0':
|
'@capacitor/filesystem@8.1.0':
|
||||||
resolution: {integrity: sha512-AfawIxQ8xBmKsEn/vEpgurGQB9+hFXRtwEiCXR+SSS0MkTw4bJrvLGnloZ/PblegYefvnay1q079Yz3PQ6y1dA==}
|
resolution: {integrity: sha512-AfawIxQ8xBmKsEn/vEpgurGQB9+hFXRtwEiCXR+SSS0MkTw4bJrvLGnloZ/PblegYefvnay1q079Yz3PQ6y1dA==}
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
@@ -783,6 +804,11 @@ packages:
|
|||||||
peerDependencies:
|
peerDependencies:
|
||||||
'@capacitor/core': ^8.0.0
|
'@capacitor/core': ^8.0.0
|
||||||
|
|
||||||
|
'@capacitor/ios@8.2.0':
|
||||||
|
resolution: {integrity: sha512-X2/VtM4qP/R1SM0VQ5W/VotEc6PS/KTooD33EijsfAHWBdee+xmBapW8SeNLnu16wJ+tsfWlvtipaJEyfKbRKQ==}
|
||||||
|
peerDependencies:
|
||||||
|
'@capacitor/core': ^8.2.0
|
||||||
|
|
||||||
'@capacitor/keyboard@8.0.0':
|
'@capacitor/keyboard@8.0.0':
|
||||||
resolution: {integrity: sha512-ycPW6iQyFwzDK95jihesj5EGiyyGSfbBqNek11iNp9tBOB7zDeYkUA2S/vPpOETt3dhP6pWr7a9gNVGuEfj11g==}
|
resolution: {integrity: sha512-ycPW6iQyFwzDK95jihesj5EGiyyGSfbBqNek11iNp9tBOB7zDeYkUA2S/vPpOETt3dhP6pWr7a9gNVGuEfj11g==}
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
@@ -1098,89 +1124,105 @@ packages:
|
|||||||
resolution: {integrity: sha512-HWpu3wRqss0vqze56Y/peCrMOsILjoorwU0ZiqF4dYQIl03dD4k71tHstC2/y+7KqNtgb7+ItSdXJydfwspDyA==}
|
resolution: {integrity: sha512-HWpu3wRqss0vqze56Y/peCrMOsILjoorwU0ZiqF4dYQIl03dD4k71tHstC2/y+7KqNtgb7+ItSdXJydfwspDyA==}
|
||||||
cpu: [arm64]
|
cpu: [arm64]
|
||||||
os: [linux]
|
os: [linux]
|
||||||
|
libc: [glibc]
|
||||||
|
|
||||||
'@img/sharp-libvips-linux-arm@1.3.0-rc.2':
|
'@img/sharp-libvips-linux-arm@1.3.0-rc.2':
|
||||||
resolution: {integrity: sha512-tyXAQ0WCfXZf2dwm7F+IN/t/s324EcdpbW3dh8rwh8NHIkijeHGyiAHs45Bs8SnsTM/RjR+uPigxFMF/QYAiTw==}
|
resolution: {integrity: sha512-tyXAQ0WCfXZf2dwm7F+IN/t/s324EcdpbW3dh8rwh8NHIkijeHGyiAHs45Bs8SnsTM/RjR+uPigxFMF/QYAiTw==}
|
||||||
cpu: [arm]
|
cpu: [arm]
|
||||||
os: [linux]
|
os: [linux]
|
||||||
|
libc: [glibc]
|
||||||
|
|
||||||
'@img/sharp-libvips-linux-ppc64@1.3.0-rc.2':
|
'@img/sharp-libvips-linux-ppc64@1.3.0-rc.2':
|
||||||
resolution: {integrity: sha512-lfJrlawp2PjxBu3Nh/2EOsgigNgr2o8MOG3XS2ibkKpJ3K/1YcUu9sTQV0S/n8+ak2R9MmJ3uTJqRVjdYkwWxg==}
|
resolution: {integrity: sha512-lfJrlawp2PjxBu3Nh/2EOsgigNgr2o8MOG3XS2ibkKpJ3K/1YcUu9sTQV0S/n8+ak2R9MmJ3uTJqRVjdYkwWxg==}
|
||||||
cpu: [ppc64]
|
cpu: [ppc64]
|
||||||
os: [linux]
|
os: [linux]
|
||||||
|
libc: [glibc]
|
||||||
|
|
||||||
'@img/sharp-libvips-linux-riscv64@1.3.0-rc.2':
|
'@img/sharp-libvips-linux-riscv64@1.3.0-rc.2':
|
||||||
resolution: {integrity: sha512-2WUcL/k7uk6i5ZSXCQmOGgGxwsfKEtJA28vNfFb6gT+Zv7vdAMbnjjGMFRtTT7RUcFHgN2olvhfnEjtIMY49MQ==}
|
resolution: {integrity: sha512-2WUcL/k7uk6i5ZSXCQmOGgGxwsfKEtJA28vNfFb6gT+Zv7vdAMbnjjGMFRtTT7RUcFHgN2olvhfnEjtIMY49MQ==}
|
||||||
cpu: [riscv64]
|
cpu: [riscv64]
|
||||||
os: [linux]
|
os: [linux]
|
||||||
|
libc: [glibc]
|
||||||
|
|
||||||
'@img/sharp-libvips-linux-s390x@1.3.0-rc.2':
|
'@img/sharp-libvips-linux-s390x@1.3.0-rc.2':
|
||||||
resolution: {integrity: sha512-wx8/WmVA+kwLfwEN6UzjFlRz6erSibq4nGItfH3Nv+OITCjx8pH3Sl67T0tbwjU3M24GOcDFBIJ6rB+2oXbMzQ==}
|
resolution: {integrity: sha512-wx8/WmVA+kwLfwEN6UzjFlRz6erSibq4nGItfH3Nv+OITCjx8pH3Sl67T0tbwjU3M24GOcDFBIJ6rB+2oXbMzQ==}
|
||||||
cpu: [s390x]
|
cpu: [s390x]
|
||||||
os: [linux]
|
os: [linux]
|
||||||
|
libc: [glibc]
|
||||||
|
|
||||||
'@img/sharp-libvips-linux-x64@1.3.0-rc.2':
|
'@img/sharp-libvips-linux-x64@1.3.0-rc.2':
|
||||||
resolution: {integrity: sha512-Rk8oOssrmTzhiuhDMPCw5Gadd4/mj5QPqrkxbg0R1VEaQeNo51d4YNbIokEDp2PqSTo+unUs6SHN8prkFKHP5A==}
|
resolution: {integrity: sha512-Rk8oOssrmTzhiuhDMPCw5Gadd4/mj5QPqrkxbg0R1VEaQeNo51d4YNbIokEDp2PqSTo+unUs6SHN8prkFKHP5A==}
|
||||||
cpu: [x64]
|
cpu: [x64]
|
||||||
os: [linux]
|
os: [linux]
|
||||||
|
libc: [glibc]
|
||||||
|
|
||||||
'@img/sharp-libvips-linuxmusl-arm64@1.3.0-rc.2':
|
'@img/sharp-libvips-linuxmusl-arm64@1.3.0-rc.2':
|
||||||
resolution: {integrity: sha512-K2TaPlrPox8uf3K01R8S+AfhPqVVWlEK6+RxkJNGasN0k1iFhu9hMhWl7+sEiWj2V30TKcppRqQWUv7H3qym9w==}
|
resolution: {integrity: sha512-K2TaPlrPox8uf3K01R8S+AfhPqVVWlEK6+RxkJNGasN0k1iFhu9hMhWl7+sEiWj2V30TKcppRqQWUv7H3qym9w==}
|
||||||
cpu: [arm64]
|
cpu: [arm64]
|
||||||
os: [linux]
|
os: [linux]
|
||||||
|
libc: [musl]
|
||||||
|
|
||||||
'@img/sharp-libvips-linuxmusl-x64@1.3.0-rc.2':
|
'@img/sharp-libvips-linuxmusl-x64@1.3.0-rc.2':
|
||||||
resolution: {integrity: sha512-88HtWiP7sBX6rb1Hw7cf3H+1ufkB+YocfeMFtmAg6oOc8hvNcby8MVPyE7GL+YD7LKPBadcScaeVnGzYWYRaMQ==}
|
resolution: {integrity: sha512-88HtWiP7sBX6rb1Hw7cf3H+1ufkB+YocfeMFtmAg6oOc8hvNcby8MVPyE7GL+YD7LKPBadcScaeVnGzYWYRaMQ==}
|
||||||
cpu: [x64]
|
cpu: [x64]
|
||||||
os: [linux]
|
os: [linux]
|
||||||
|
libc: [musl]
|
||||||
|
|
||||||
'@img/sharp-linux-arm64@0.35.0-rc.0':
|
'@img/sharp-linux-arm64@0.35.0-rc.0':
|
||||||
resolution: {integrity: sha512-6pdCj+H0+sNsX7vpbxF2FhDF+fe7Hc/mfjlG5caFVUrACIMMOMc962xTE0Y1+XE3EWM8hUmGp3y6hISjJDnwYA==}
|
resolution: {integrity: sha512-6pdCj+H0+sNsX7vpbxF2FhDF+fe7Hc/mfjlG5caFVUrACIMMOMc962xTE0Y1+XE3EWM8hUmGp3y6hISjJDnwYA==}
|
||||||
engines: {node: '>=20.9.0'}
|
engines: {node: '>=20.9.0'}
|
||||||
cpu: [arm64]
|
cpu: [arm64]
|
||||||
os: [linux]
|
os: [linux]
|
||||||
|
libc: [glibc]
|
||||||
|
|
||||||
'@img/sharp-linux-arm@0.35.0-rc.0':
|
'@img/sharp-linux-arm@0.35.0-rc.0':
|
||||||
resolution: {integrity: sha512-oy0+atKDov9vn9mMVlyS0V2BTKTFzR3cFhZ9ilF98vnGSvrWdevUlfjNdTE5pN/xZq6z0GnkIAUL5KJhwh5yzQ==}
|
resolution: {integrity: sha512-oy0+atKDov9vn9mMVlyS0V2BTKTFzR3cFhZ9ilF98vnGSvrWdevUlfjNdTE5pN/xZq6z0GnkIAUL5KJhwh5yzQ==}
|
||||||
engines: {node: '>=20.9.0'}
|
engines: {node: '>=20.9.0'}
|
||||||
cpu: [arm]
|
cpu: [arm]
|
||||||
os: [linux]
|
os: [linux]
|
||||||
|
libc: [glibc]
|
||||||
|
|
||||||
'@img/sharp-linux-ppc64@0.35.0-rc.0':
|
'@img/sharp-linux-ppc64@0.35.0-rc.0':
|
||||||
resolution: {integrity: sha512-SvX6E6h/i/zhhGheJiSHbFb3loglNDi+H6wEpnPdp5SGlyiJabLVco93kHBeo4lkfQAwYACRW+yFhRfNhhZO3Q==}
|
resolution: {integrity: sha512-SvX6E6h/i/zhhGheJiSHbFb3loglNDi+H6wEpnPdp5SGlyiJabLVco93kHBeo4lkfQAwYACRW+yFhRfNhhZO3Q==}
|
||||||
engines: {node: '>=20.9.0'}
|
engines: {node: '>=20.9.0'}
|
||||||
cpu: [ppc64]
|
cpu: [ppc64]
|
||||||
os: [linux]
|
os: [linux]
|
||||||
|
libc: [glibc]
|
||||||
|
|
||||||
'@img/sharp-linux-riscv64@0.35.0-rc.0':
|
'@img/sharp-linux-riscv64@0.35.0-rc.0':
|
||||||
resolution: {integrity: sha512-EY+6k/u87hfZgBXJaNNRwEN1ACPHUudcW2ObVmNA1eADnFircVvdPjvKUCc5bVwzjmG1imzNJWoapIB9wZheFQ==}
|
resolution: {integrity: sha512-EY+6k/u87hfZgBXJaNNRwEN1ACPHUudcW2ObVmNA1eADnFircVvdPjvKUCc5bVwzjmG1imzNJWoapIB9wZheFQ==}
|
||||||
engines: {node: '>=20.9.0'}
|
engines: {node: '>=20.9.0'}
|
||||||
cpu: [riscv64]
|
cpu: [riscv64]
|
||||||
os: [linux]
|
os: [linux]
|
||||||
|
libc: [glibc]
|
||||||
|
|
||||||
'@img/sharp-linux-s390x@0.35.0-rc.0':
|
'@img/sharp-linux-s390x@0.35.0-rc.0':
|
||||||
resolution: {integrity: sha512-owTWB3KiGs03QpcHFGNBToB5DIEuCsHugzVS7h8Za6f2W0Nw8qedJF5oSIr71nBj4jF4NmTMd5w7EFOOKn8pFA==}
|
resolution: {integrity: sha512-owTWB3KiGs03QpcHFGNBToB5DIEuCsHugzVS7h8Za6f2W0Nw8qedJF5oSIr71nBj4jF4NmTMd5w7EFOOKn8pFA==}
|
||||||
engines: {node: '>=20.9.0'}
|
engines: {node: '>=20.9.0'}
|
||||||
cpu: [s390x]
|
cpu: [s390x]
|
||||||
os: [linux]
|
os: [linux]
|
||||||
|
libc: [glibc]
|
||||||
|
|
||||||
'@img/sharp-linux-x64@0.35.0-rc.0':
|
'@img/sharp-linux-x64@0.35.0-rc.0':
|
||||||
resolution: {integrity: sha512-XQoXfEHwz0TTtf6DMT7rLXNc7qb7okjSe+8vzusdTuqVXBi+km+Jwvc9DKL9azMWKR4TR/ArvQ7Y5dFnckb9VA==}
|
resolution: {integrity: sha512-XQoXfEHwz0TTtf6DMT7rLXNc7qb7okjSe+8vzusdTuqVXBi+km+Jwvc9DKL9azMWKR4TR/ArvQ7Y5dFnckb9VA==}
|
||||||
engines: {node: '>=20.9.0'}
|
engines: {node: '>=20.9.0'}
|
||||||
cpu: [x64]
|
cpu: [x64]
|
||||||
os: [linux]
|
os: [linux]
|
||||||
|
libc: [glibc]
|
||||||
|
|
||||||
'@img/sharp-linuxmusl-arm64@0.35.0-rc.0':
|
'@img/sharp-linuxmusl-arm64@0.35.0-rc.0':
|
||||||
resolution: {integrity: sha512-hfCXVq35g/zA+Lpa3x1gUXHLi0rLWUPbVGpbPox2zyx2byfKXf5Lcq6xoMJUrQkmT+s8BaOP8TbmhG8ZQgUFyw==}
|
resolution: {integrity: sha512-hfCXVq35g/zA+Lpa3x1gUXHLi0rLWUPbVGpbPox2zyx2byfKXf5Lcq6xoMJUrQkmT+s8BaOP8TbmhG8ZQgUFyw==}
|
||||||
engines: {node: '>=20.9.0'}
|
engines: {node: '>=20.9.0'}
|
||||||
cpu: [arm64]
|
cpu: [arm64]
|
||||||
os: [linux]
|
os: [linux]
|
||||||
|
libc: [musl]
|
||||||
|
|
||||||
'@img/sharp-linuxmusl-x64@0.35.0-rc.0':
|
'@img/sharp-linuxmusl-x64@0.35.0-rc.0':
|
||||||
resolution: {integrity: sha512-TsV3KFF9i2wKHHsXnRz9N5H66swuDqpk+KPiTugbtdTo+um33BoFZC7F+Ty+DBoOWf4TT+j7IiJfpen5bC5FRw==}
|
resolution: {integrity: sha512-TsV3KFF9i2wKHHsXnRz9N5H66swuDqpk+KPiTugbtdTo+um33BoFZC7F+Ty+DBoOWf4TT+j7IiJfpen5bC5FRw==}
|
||||||
engines: {node: '>=20.9.0'}
|
engines: {node: '>=20.9.0'}
|
||||||
cpu: [x64]
|
cpu: [x64]
|
||||||
os: [linux]
|
os: [linux]
|
||||||
|
libc: [musl]
|
||||||
|
|
||||||
'@img/sharp-wasm32@0.35.0-rc.0':
|
'@img/sharp-wasm32@0.35.0-rc.0':
|
||||||
resolution: {integrity: sha512-5eRvTRqUbNDEd999tRRwXaEO5CERA1WDiVrNDgh+g0IlhCJ79jQkfTE+/dKEO8VbhUwVT6qWFjse+/3KjXhUKg==}
|
resolution: {integrity: sha512-5eRvTRqUbNDEd999tRRwXaEO5CERA1WDiVrNDgh+g0IlhCJ79jQkfTE+/dKEO8VbhUwVT6qWFjse+/3KjXhUKg==}
|
||||||
@@ -1283,6 +1325,12 @@ packages:
|
|||||||
'@jridgewell/trace-mapping@0.3.9':
|
'@jridgewell/trace-mapping@0.3.9':
|
||||||
resolution: {integrity: sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==}
|
resolution: {integrity: sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==}
|
||||||
|
|
||||||
|
'@livekit/mutex@1.1.1':
|
||||||
|
resolution: {integrity: sha512-EsshAucklmpuUAfkABPxJNhzj9v2sG7JuzFDL4ML1oJQSV14sqrpTYnsaOudMAw9yOaW53NU3QQTlUQoRs4czw==}
|
||||||
|
|
||||||
|
'@livekit/protocol@1.44.0':
|
||||||
|
resolution: {integrity: sha512-/vfhDUGcUKO8Q43r6i+5FrDhl5oZjm/X3U4x2Iciqvgn5C8qbj+57YPcWSJ1kyIZm5Cm6AV2nAPjMm3ETD/iyg==}
|
||||||
|
|
||||||
'@noble/ciphers@0.5.3':
|
'@noble/ciphers@0.5.3':
|
||||||
resolution: {integrity: sha512-B0+6IIHiqEs3BPMT0hcRmHvEj2QHOLu+uwt+tqDDeVd0oyVzh7BPrDcPjRnV1PV/5LaknXJJQvOuRGR0zQJz+w==}
|
resolution: {integrity: sha512-B0+6IIHiqEs3BPMT0hcRmHvEj2QHOLu+uwt+tqDDeVd0oyVzh7BPrDcPjRnV1PV/5LaknXJJQvOuRGR0zQJz+w==}
|
||||||
|
|
||||||
@@ -1376,9 +1424,9 @@ packages:
|
|||||||
'@polka/url@1.0.0-next.29':
|
'@polka/url@1.0.0-next.29':
|
||||||
resolution: {integrity: sha512-wwQAWhWSuHaag8c4q/KN/vCoeOJYshAIvMQwD4GpSb3OiZklFfvAgmj0VCBBImRpuF/aFgIRzllXlVX93Jevww==}
|
resolution: {integrity: sha512-wwQAWhWSuHaag8c4q/KN/vCoeOJYshAIvMQwD4GpSb3OiZklFfvAgmj0VCBBImRpuF/aFgIRzllXlVX93Jevww==}
|
||||||
|
|
||||||
'@pomade/core@0.2.1':
|
'@pomade/core@0.2.2':
|
||||||
resolution: {integrity: sha512-zXpPQPkhVe7OchmRDe2MbHdUxiCSeUuMwrHOyeOBs/xD1EfY093Mwj6Cu/OLfz0wxivBDSp1GMMmxqKbLWam3Q==}
|
resolution: {integrity: sha512-FoilLsO0gVjiKMW3LV63pmXU7x3gh8YVGVulyR6QJr4h47XrsBg8vPkZtKWr4+sH3sW31e2tNIPUb3ptiuhrMA==}
|
||||||
version: 0.2.1
|
version: 0.2.2
|
||||||
engines: {node: '>=12.0.0'}
|
engines: {node: '>=12.0.0'}
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
'@frostr/bifrost': ^1.0.7
|
'@frostr/bifrost': ^1.0.7
|
||||||
@@ -1488,66 +1536,79 @@ packages:
|
|||||||
resolution: {integrity: sha512-E8jKK87uOvLrrLN28jnAAAChNq5LeCd2mGgZF+fGF5D507WlG/Noct3lP/QzQ6MrqJ5BCKNwI9ipADB6jyiq2A==}
|
resolution: {integrity: sha512-E8jKK87uOvLrrLN28jnAAAChNq5LeCd2mGgZF+fGF5D507WlG/Noct3lP/QzQ6MrqJ5BCKNwI9ipADB6jyiq2A==}
|
||||||
cpu: [arm]
|
cpu: [arm]
|
||||||
os: [linux]
|
os: [linux]
|
||||||
|
libc: [glibc]
|
||||||
|
|
||||||
'@rollup/rollup-linux-arm-musleabihf@4.56.0':
|
'@rollup/rollup-linux-arm-musleabihf@4.56.0':
|
||||||
resolution: {integrity: sha512-jQosa5FMYF5Z6prEpTCCmzCXz6eKr/tCBssSmQGEeozA9tkRUty/5Vx06ibaOP9RCrW1Pvb8yp3gvZhHwTDsJw==}
|
resolution: {integrity: sha512-jQosa5FMYF5Z6prEpTCCmzCXz6eKr/tCBssSmQGEeozA9tkRUty/5Vx06ibaOP9RCrW1Pvb8yp3gvZhHwTDsJw==}
|
||||||
cpu: [arm]
|
cpu: [arm]
|
||||||
os: [linux]
|
os: [linux]
|
||||||
|
libc: [musl]
|
||||||
|
|
||||||
'@rollup/rollup-linux-arm64-gnu@4.56.0':
|
'@rollup/rollup-linux-arm64-gnu@4.56.0':
|
||||||
resolution: {integrity: sha512-uQVoKkrC1KGEV6udrdVahASIsaF8h7iLG0U0W+Xn14ucFwi6uS539PsAr24IEF9/FoDtzMeeJXJIBo5RkbNWvQ==}
|
resolution: {integrity: sha512-uQVoKkrC1KGEV6udrdVahASIsaF8h7iLG0U0W+Xn14ucFwi6uS539PsAr24IEF9/FoDtzMeeJXJIBo5RkbNWvQ==}
|
||||||
cpu: [arm64]
|
cpu: [arm64]
|
||||||
os: [linux]
|
os: [linux]
|
||||||
|
libc: [glibc]
|
||||||
|
|
||||||
'@rollup/rollup-linux-arm64-musl@4.56.0':
|
'@rollup/rollup-linux-arm64-musl@4.56.0':
|
||||||
resolution: {integrity: sha512-vLZ1yJKLxhQLFKTs42RwTwa6zkGln+bnXc8ueFGMYmBTLfNu58sl5/eXyxRa2RarTkJbXl8TKPgfS6V5ijNqEA==}
|
resolution: {integrity: sha512-vLZ1yJKLxhQLFKTs42RwTwa6zkGln+bnXc8ueFGMYmBTLfNu58sl5/eXyxRa2RarTkJbXl8TKPgfS6V5ijNqEA==}
|
||||||
cpu: [arm64]
|
cpu: [arm64]
|
||||||
os: [linux]
|
os: [linux]
|
||||||
|
libc: [musl]
|
||||||
|
|
||||||
'@rollup/rollup-linux-loong64-gnu@4.56.0':
|
'@rollup/rollup-linux-loong64-gnu@4.56.0':
|
||||||
resolution: {integrity: sha512-FWfHOCub564kSE3xJQLLIC/hbKqHSVxy8vY75/YHHzWvbJL7aYJkdgwD/xGfUlL5UV2SB7otapLrcCj2xnF1dg==}
|
resolution: {integrity: sha512-FWfHOCub564kSE3xJQLLIC/hbKqHSVxy8vY75/YHHzWvbJL7aYJkdgwD/xGfUlL5UV2SB7otapLrcCj2xnF1dg==}
|
||||||
cpu: [loong64]
|
cpu: [loong64]
|
||||||
os: [linux]
|
os: [linux]
|
||||||
|
libc: [glibc]
|
||||||
|
|
||||||
'@rollup/rollup-linux-loong64-musl@4.56.0':
|
'@rollup/rollup-linux-loong64-musl@4.56.0':
|
||||||
resolution: {integrity: sha512-z1EkujxIh7nbrKL1lmIpqFTc/sr0u8Uk0zK/qIEFldbt6EDKWFk/pxFq3gYj4Bjn3aa9eEhYRlL3H8ZbPT1xvA==}
|
resolution: {integrity: sha512-z1EkujxIh7nbrKL1lmIpqFTc/sr0u8Uk0zK/qIEFldbt6EDKWFk/pxFq3gYj4Bjn3aa9eEhYRlL3H8ZbPT1xvA==}
|
||||||
cpu: [loong64]
|
cpu: [loong64]
|
||||||
os: [linux]
|
os: [linux]
|
||||||
|
libc: [musl]
|
||||||
|
|
||||||
'@rollup/rollup-linux-ppc64-gnu@4.56.0':
|
'@rollup/rollup-linux-ppc64-gnu@4.56.0':
|
||||||
resolution: {integrity: sha512-iNFTluqgdoQC7AIE8Q34R3AuPrJGJirj5wMUErxj22deOcY7XwZRaqYmB6ZKFHoVGqRcRd0mqO+845jAibKCkw==}
|
resolution: {integrity: sha512-iNFTluqgdoQC7AIE8Q34R3AuPrJGJirj5wMUErxj22deOcY7XwZRaqYmB6ZKFHoVGqRcRd0mqO+845jAibKCkw==}
|
||||||
cpu: [ppc64]
|
cpu: [ppc64]
|
||||||
os: [linux]
|
os: [linux]
|
||||||
|
libc: [glibc]
|
||||||
|
|
||||||
'@rollup/rollup-linux-ppc64-musl@4.56.0':
|
'@rollup/rollup-linux-ppc64-musl@4.56.0':
|
||||||
resolution: {integrity: sha512-MtMeFVlD2LIKjp2sE2xM2slq3Zxf9zwVuw0jemsxvh1QOpHSsSzfNOTH9uYW9i1MXFxUSMmLpeVeUzoNOKBaWg==}
|
resolution: {integrity: sha512-MtMeFVlD2LIKjp2sE2xM2slq3Zxf9zwVuw0jemsxvh1QOpHSsSzfNOTH9uYW9i1MXFxUSMmLpeVeUzoNOKBaWg==}
|
||||||
cpu: [ppc64]
|
cpu: [ppc64]
|
||||||
os: [linux]
|
os: [linux]
|
||||||
|
libc: [musl]
|
||||||
|
|
||||||
'@rollup/rollup-linux-riscv64-gnu@4.56.0':
|
'@rollup/rollup-linux-riscv64-gnu@4.56.0':
|
||||||
resolution: {integrity: sha512-in+v6wiHdzzVhYKXIk5U74dEZHdKN9KH0Q4ANHOTvyXPG41bajYRsy7a8TPKbYPl34hU7PP7hMVHRvv/5aCSew==}
|
resolution: {integrity: sha512-in+v6wiHdzzVhYKXIk5U74dEZHdKN9KH0Q4ANHOTvyXPG41bajYRsy7a8TPKbYPl34hU7PP7hMVHRvv/5aCSew==}
|
||||||
cpu: [riscv64]
|
cpu: [riscv64]
|
||||||
os: [linux]
|
os: [linux]
|
||||||
|
libc: [glibc]
|
||||||
|
|
||||||
'@rollup/rollup-linux-riscv64-musl@4.56.0':
|
'@rollup/rollup-linux-riscv64-musl@4.56.0':
|
||||||
resolution: {integrity: sha512-yni2raKHB8m9NQpI9fPVwN754mn6dHQSbDTwxdr9SE0ks38DTjLMMBjrwvB5+mXrX+C0npX0CVeCUcvvvD8CNQ==}
|
resolution: {integrity: sha512-yni2raKHB8m9NQpI9fPVwN754mn6dHQSbDTwxdr9SE0ks38DTjLMMBjrwvB5+mXrX+C0npX0CVeCUcvvvD8CNQ==}
|
||||||
cpu: [riscv64]
|
cpu: [riscv64]
|
||||||
os: [linux]
|
os: [linux]
|
||||||
|
libc: [musl]
|
||||||
|
|
||||||
'@rollup/rollup-linux-s390x-gnu@4.56.0':
|
'@rollup/rollup-linux-s390x-gnu@4.56.0':
|
||||||
resolution: {integrity: sha512-zhLLJx9nQPu7wezbxt2ut+CI4YlXi68ndEve16tPc/iwoylWS9B3FxpLS2PkmfYgDQtosah07Mj9E0khc3Y+vQ==}
|
resolution: {integrity: sha512-zhLLJx9nQPu7wezbxt2ut+CI4YlXi68ndEve16tPc/iwoylWS9B3FxpLS2PkmfYgDQtosah07Mj9E0khc3Y+vQ==}
|
||||||
cpu: [s390x]
|
cpu: [s390x]
|
||||||
os: [linux]
|
os: [linux]
|
||||||
|
libc: [glibc]
|
||||||
|
|
||||||
'@rollup/rollup-linux-x64-gnu@4.56.0':
|
'@rollup/rollup-linux-x64-gnu@4.56.0':
|
||||||
resolution: {integrity: sha512-MVC6UDp16ZSH7x4rtuJPAEoE1RwS8N4oK9DLHy3FTEdFoUTCFVzMfJl/BVJ330C+hx8FfprA5Wqx4FhZXkj2Kw==}
|
resolution: {integrity: sha512-MVC6UDp16ZSH7x4rtuJPAEoE1RwS8N4oK9DLHy3FTEdFoUTCFVzMfJl/BVJ330C+hx8FfprA5Wqx4FhZXkj2Kw==}
|
||||||
cpu: [x64]
|
cpu: [x64]
|
||||||
os: [linux]
|
os: [linux]
|
||||||
|
libc: [glibc]
|
||||||
|
|
||||||
'@rollup/rollup-linux-x64-musl@4.56.0':
|
'@rollup/rollup-linux-x64-musl@4.56.0':
|
||||||
resolution: {integrity: sha512-ZhGH1eA4Qv0lxaV00azCIS1ChedK0V32952Md3FtnxSqZTBTd6tgil4nZT5cU8B+SIw3PFYkvyR4FKo2oyZIHA==}
|
resolution: {integrity: sha512-ZhGH1eA4Qv0lxaV00azCIS1ChedK0V32952Md3FtnxSqZTBTd6tgil4nZT5cU8B+SIw3PFYkvyR4FKo2oyZIHA==}
|
||||||
cpu: [x64]
|
cpu: [x64]
|
||||||
os: [linux]
|
os: [linux]
|
||||||
|
libc: [musl]
|
||||||
|
|
||||||
'@rollup/rollup-openbsd-x64@4.56.0':
|
'@rollup/rollup-openbsd-x64@4.56.0':
|
||||||
resolution: {integrity: sha512-O16XcmyDeFI9879pEcmtWvD/2nyxR9mF7Gs44lf1vGGx8Vg2DRNx11aVXBEqOQhWb92WN4z7fW/q4+2NYzCbBA==}
|
resolution: {integrity: sha512-O16XcmyDeFI9879pEcmtWvD/2nyxR9mF7Gs44lf1vGGx8Vg2DRNx11aVXBEqOQhWb92WN4z7fW/q4+2NYzCbBA==}
|
||||||
@@ -1670,30 +1731,35 @@ packages:
|
|||||||
engines: {node: '>= 10'}
|
engines: {node: '>= 10'}
|
||||||
cpu: [arm64]
|
cpu: [arm64]
|
||||||
os: [linux]
|
os: [linux]
|
||||||
|
libc: [glibc]
|
||||||
|
|
||||||
'@tauri-apps/cli-linux-arm64-musl@2.10.0':
|
'@tauri-apps/cli-linux-arm64-musl@2.10.0':
|
||||||
resolution: {integrity: sha512-GUoPdVJmrJRIXFfW3Rkt+eGK9ygOdyISACZfC/bCSfOnGt8kNdQIQr5WRH9QUaTVFIwxMlQyV3m+yXYP+xhSVA==}
|
resolution: {integrity: sha512-GUoPdVJmrJRIXFfW3Rkt+eGK9ygOdyISACZfC/bCSfOnGt8kNdQIQr5WRH9QUaTVFIwxMlQyV3m+yXYP+xhSVA==}
|
||||||
engines: {node: '>= 10'}
|
engines: {node: '>= 10'}
|
||||||
cpu: [arm64]
|
cpu: [arm64]
|
||||||
os: [linux]
|
os: [linux]
|
||||||
|
libc: [musl]
|
||||||
|
|
||||||
'@tauri-apps/cli-linux-riscv64-gnu@2.10.0':
|
'@tauri-apps/cli-linux-riscv64-gnu@2.10.0':
|
||||||
resolution: {integrity: sha512-JO7s3TlSxshwsoKNCDkyvsx5gw2QAs/Y2GbR5UE2d5kkU138ATKoPOtxn8G1fFT1aDW4LH0rYAAfBpGkDyJJnw==}
|
resolution: {integrity: sha512-JO7s3TlSxshwsoKNCDkyvsx5gw2QAs/Y2GbR5UE2d5kkU138ATKoPOtxn8G1fFT1aDW4LH0rYAAfBpGkDyJJnw==}
|
||||||
engines: {node: '>= 10'}
|
engines: {node: '>= 10'}
|
||||||
cpu: [riscv64]
|
cpu: [riscv64]
|
||||||
os: [linux]
|
os: [linux]
|
||||||
|
libc: [glibc]
|
||||||
|
|
||||||
'@tauri-apps/cli-linux-x64-gnu@2.10.0':
|
'@tauri-apps/cli-linux-x64-gnu@2.10.0':
|
||||||
resolution: {integrity: sha512-Uvh4SUUp4A6DVRSMWjelww0GnZI3PlVy7VS+DRF5napKuIehVjGl9XD0uKoCoxwAQBLctvipyEK+pDXpJeoHng==}
|
resolution: {integrity: sha512-Uvh4SUUp4A6DVRSMWjelww0GnZI3PlVy7VS+DRF5napKuIehVjGl9XD0uKoCoxwAQBLctvipyEK+pDXpJeoHng==}
|
||||||
engines: {node: '>= 10'}
|
engines: {node: '>= 10'}
|
||||||
cpu: [x64]
|
cpu: [x64]
|
||||||
os: [linux]
|
os: [linux]
|
||||||
|
libc: [glibc]
|
||||||
|
|
||||||
'@tauri-apps/cli-linux-x64-musl@2.10.0':
|
'@tauri-apps/cli-linux-x64-musl@2.10.0':
|
||||||
resolution: {integrity: sha512-AP0KRK6bJuTpQ8kMNWvhIpKUkQJfcPFeba7QshOQZjJ8wOS6emwTN4K5g/d3AbCMo0RRdnZWwu67MlmtJyxC1Q==}
|
resolution: {integrity: sha512-AP0KRK6bJuTpQ8kMNWvhIpKUkQJfcPFeba7QshOQZjJ8wOS6emwTN4K5g/d3AbCMo0RRdnZWwu67MlmtJyxC1Q==}
|
||||||
engines: {node: '>= 10'}
|
engines: {node: '>= 10'}
|
||||||
cpu: [x64]
|
cpu: [x64]
|
||||||
os: [linux]
|
os: [linux]
|
||||||
|
libc: [musl]
|
||||||
|
|
||||||
'@tauri-apps/cli-win32-arm64-msvc@2.10.0':
|
'@tauri-apps/cli-win32-arm64-msvc@2.10.0':
|
||||||
resolution: {integrity: sha512-97DXVU3dJystrq7W41IX+82JEorLNY+3+ECYxvXWqkq7DBN6FsA08x/EFGE8N/b0LTOui9X2dvpGGoeZKKV08g==}
|
resolution: {integrity: sha512-97DXVU3dJystrq7W41IX+82JEorLNY+3+ECYxvXWqkq7DBN6FsA08x/EFGE8N/b0LTOui9X2dvpGGoeZKKV08g==}
|
||||||
@@ -1823,6 +1889,9 @@ packages:
|
|||||||
'@types/cookie@0.6.0':
|
'@types/cookie@0.6.0':
|
||||||
resolution: {integrity: sha512-4Kh9a6B2bQciAhf7FSuMRRkUWecJgJu9nPnx3yzpsfXX/c50REIqpHY4C82bXP90qrLtXtkDxTZosYO3UpOwlA==}
|
resolution: {integrity: sha512-4Kh9a6B2bQciAhf7FSuMRRkUWecJgJu9nPnx3yzpsfXX/c50REIqpHY4C82bXP90qrLtXtkDxTZosYO3UpOwlA==}
|
||||||
|
|
||||||
|
'@types/dom-mediacapture-record@1.0.22':
|
||||||
|
resolution: {integrity: sha512-mUMZLK3NvwRLcAAT9qmcK+9p7tpU2FHdDsntR3YI4+GY88XrgG4XiE7u1Q2LAN2/FZOz/tdMDC3GQCR4T8nFuw==}
|
||||||
|
|
||||||
'@types/eslint@9.6.1':
|
'@types/eslint@9.6.1':
|
||||||
resolution: {integrity: sha512-FXx2pKgId/WyYo2jXw63kk7/+TY7u7AziEJxJAnSFzHlqTAS3Ync6SvgYAN/k4/PQpnnVuzoMuVnByKK2qp0ag==}
|
resolution: {integrity: sha512-FXx2pKgId/WyYo2jXw63kk7/+TY7u7AziEJxJAnSFzHlqTAS3Ync6SvgYAN/k4/PQpnnVuzoMuVnByKK2qp0ag==}
|
||||||
|
|
||||||
@@ -1967,83 +2036,83 @@ packages:
|
|||||||
'@vite-pwa/assets-generator':
|
'@vite-pwa/assets-generator':
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
'@welshman/app@0.8.8':
|
'@welshman/app@0.8.12':
|
||||||
resolution: {integrity: sha512-pyySouAJwGZ2RSC29egiFft38Ctuioodon6xWFxB7HvJ9Llsh5b53qjkrQcAYM7lUAzXwtalf2v4Z3EwYdUObg==}
|
resolution: {integrity: sha512-kRp+AVzn4i3FvZmdlyMknFUAb/5SnUz9A/cFKkDqWHsd+N3PbNcL2ZOlV9v5NI77GtsDF2ez6PEQfsZxWvkS/g==}
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
'@pomade/core': ^0.1.3
|
'@pomade/core': ^0.2.1
|
||||||
'@welshman/feeds': 0.8.8
|
'@welshman/feeds': 0.8.12
|
||||||
'@welshman/lib': 0.8.8
|
'@welshman/lib': 0.8.12
|
||||||
'@welshman/net': 0.8.8
|
'@welshman/net': 0.8.12
|
||||||
'@welshman/router': 0.8.8
|
'@welshman/router': 0.8.12
|
||||||
'@welshman/signer': 0.8.8
|
'@welshman/signer': 0.8.12
|
||||||
'@welshman/store': 0.8.8
|
'@welshman/store': 0.8.12
|
||||||
'@welshman/util': 0.8.8
|
'@welshman/util': 0.8.12
|
||||||
svelte: ^4.0.0 || ^5.0.0
|
svelte: ^4.0.0 || ^5.0.0
|
||||||
|
|
||||||
'@welshman/content@0.8.8':
|
'@welshman/content@0.8.12':
|
||||||
resolution: {integrity: sha512-5jh2YMoqINzkOEVSDZec6JbAqiC0WThwRuPwJOwiJlAFYQ4LC0MAT1HQ8z9pht/0TXdjYQUu2X+jngqqICNOiw==}
|
resolution: {integrity: sha512-hviVTXdyGf04Xq7mGo/82fq6lnbyuUYOGPkf8pJqPkfGh0f3i9nKof6gkzPvjZeEYSczneI2GpLIxkaZ3w1/tw==}
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
nostr-tools: ^2.19.4
|
nostr-tools: ^2.19.4
|
||||||
|
|
||||||
'@welshman/editor@0.8.8':
|
'@welshman/editor@0.8.12':
|
||||||
resolution: {integrity: sha512-54WD2d6HEEiuoPgl/LeE4eaLtF2/SrYObk+IE9UUrJjoXcK/BK3vt8ltzazvBLR8ntfKOQINc4DhkeuBxiiCpA==}
|
resolution: {integrity: sha512-CEXszH6pfM1kZHU9WnB6Z97YlxEOtgao6R3hhnBL/kRXy2tUTLpmFWyMONg2vu8Uzxtwz665eZhucLsju60U6w==}
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
'@welshman/lib': 0.8.8
|
'@welshman/lib': 0.8.12
|
||||||
'@welshman/util': 0.8.8
|
'@welshman/util': 0.8.12
|
||||||
nostr-editor: ^1.1.1
|
nostr-editor: ^1.1.1
|
||||||
nostr-tools: ^2.19.4
|
nostr-tools: ^2.19.4
|
||||||
|
|
||||||
'@welshman/feeds@0.8.8':
|
'@welshman/feeds@0.8.12':
|
||||||
resolution: {integrity: sha512-o5JuptpWSNr6wtbM0RfSxTJgZStaNxPz160tE9u0SZzs1/a9sq/Yzesw7s+g0nKukRjBbl70DOqpTqOqfXAEIw==}
|
resolution: {integrity: sha512-Dp/063qdrbe096z6IpIneazsqscfzSwsg09ZNHB6c3OsVb6iLEXLe7mEpATGcRsZ8memuFNLVLIP51eVkBqPcQ==}
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
'@welshman/lib': 0.8.8
|
'@welshman/lib': 0.8.12
|
||||||
'@welshman/net': 0.8.8
|
'@welshman/net': 0.8.12
|
||||||
'@welshman/router': 0.8.8
|
'@welshman/router': 0.8.12
|
||||||
'@welshman/signer': 0.8.8
|
'@welshman/signer': 0.8.12
|
||||||
'@welshman/util': 0.8.8
|
'@welshman/util': 0.8.12
|
||||||
|
|
||||||
'@welshman/lib@0.8.8':
|
'@welshman/lib@0.8.12':
|
||||||
resolution: {integrity: sha512-77ZfVtodV05276ceR8c+JdDFqhOpmy2W6PkgDYbnKstQzKb5TN6wBvcLKxJppTzWMeWbyi2JADsuOYvW1jpOSQ==}
|
resolution: {integrity: sha512-7Y1GjAcABquWF47A1Jni5JdP+k0GH2yRmEbVhIU+0R0TubCwPAKS38J2LTvtuE9CJMX6hPS9IKEZS6qTOAaVuw==}
|
||||||
engines: {node: '>=12.0.0'}
|
engines: {node: '>=12.0.0'}
|
||||||
|
|
||||||
'@welshman/net@0.8.8':
|
'@welshman/net@0.8.12':
|
||||||
resolution: {integrity: sha512-Rug3GzVzyABG21g++cCLOVXdjAieV6rJUZqstE8i/olZvOEWZpZ9R901DoUSDR07U2HTrAwHQrjgb1HmH4jiDQ==}
|
resolution: {integrity: sha512-Ba71jwb8BBwUfPPtWHKYLB0HqeDYK64oqwxfo5bYldtPfGYrlD+a7lHFSZvNyOhvyXygCaIMnaye1QxWAHP8ng==}
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
'@welshman/lib': 0.8.8
|
'@welshman/lib': 0.8.12
|
||||||
'@welshman/util': 0.8.8
|
'@welshman/util': 0.8.12
|
||||||
|
|
||||||
'@welshman/router@0.8.8':
|
'@welshman/router@0.8.12':
|
||||||
resolution: {integrity: sha512-j5O7F7KGQtOIvBJctEiUNcLfHBUnhHlYHxUx7ImPPurc1zLzt3JovvJJFubXMQoQ26D01DsK/AA1L5WZNebUhA==}
|
resolution: {integrity: sha512-Rr7ryBNTvTvjoLsDRMKPuoNJbBv2MgyqN2338p4vVhPMK6MOGM3Nx1og0LpHDGiLlCFuPM8RpParpIWfNnWbKw==}
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
'@welshman/lib': 0.8.8
|
'@welshman/lib': 0.8.12
|
||||||
'@welshman/net': 0.8.8
|
'@welshman/net': 0.8.12
|
||||||
'@welshman/util': 0.8.8
|
'@welshman/util': 0.8.12
|
||||||
|
|
||||||
'@welshman/signer@0.8.8':
|
'@welshman/signer@0.8.12':
|
||||||
resolution: {integrity: sha512-rswHrTdc1+yvAno2h3JELzjp+LCfiYfUr8ACvwSSHAqDwrtezppfh0WDEPaYBp2EVSJ6tKMM1sVey0quO63aMw==}
|
resolution: {integrity: sha512-eO4mw2QOR2d2oCS4zgptkCgjC1s8X+1vNnXfWDbtlEwhk7PD4ySSCkpNVMeElq+uluLOugmKvgDc4gfnIH3p2A==}
|
||||||
version: 0.8.8
|
version: 0.8.12
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
'@noble/curves': ^1.9.7
|
'@noble/curves': ^1.9.7
|
||||||
'@noble/hashes': ^2.0.1
|
'@noble/hashes': ^2.0.1
|
||||||
'@welshman/lib': 0.8.8
|
'@welshman/lib': 0.8.12
|
||||||
'@welshman/net': 0.8.8
|
'@welshman/net': 0.8.12
|
||||||
'@welshman/util': 0.8.8
|
'@welshman/util': 0.8.12
|
||||||
nostr-signer-capacitor-plugin: '*'
|
nostr-signer-capacitor-plugin: '*'
|
||||||
nostr-tools: ^2.19.4
|
nostr-tools: ^2.19.4
|
||||||
|
|
||||||
'@welshman/store@0.8.8':
|
'@welshman/store@0.8.12':
|
||||||
resolution: {integrity: sha512-mTFueKZi9CtrtvCZT5eT5QaLMs94LxQg4y7oO5PZp9wv8EGSnB9p7XIflM0OfpKwF7c0pu1RdXcjVlvMDsC6QQ==}
|
resolution: {integrity: sha512-3IUzPRMMVF6Pcw3DaKkfJ4S6bYzVtk6Ze3FnaHs1xJIxkQj9GyBO9VreiovULPW0djsnrP5+1UEN4mKZsG3hXg==}
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
'@welshman/lib': 0.8.8
|
'@welshman/lib': 0.8.12
|
||||||
'@welshman/net': 0.8.8
|
'@welshman/net': 0.8.12
|
||||||
'@welshman/util': 0.8.8
|
'@welshman/util': 0.8.12
|
||||||
svelte: ^4.0.0 || ^5.0.0
|
svelte: ^4.0.0 || ^5.0.0
|
||||||
|
|
||||||
'@welshman/util@0.8.8':
|
'@welshman/util@0.8.12':
|
||||||
resolution: {integrity: sha512-SNT1VXab6ce36EVfjs1A2uwWs5elYTI4eXi8SUuj42k8CqNIAtG+bOf/JFIxXNTfl3NSxxZdWzpLLZWBqgpAxQ==}
|
resolution: {integrity: sha512-lgftFt2moXZdN5fuL0RoAnAARV0n0d2+Q56gt7KrBSevjoCbtJgBVX5idvxL5PCEfh81veovJtty6eHxrhQv5A==}
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
'@noble/curves': ^1.9.7
|
'@noble/curves': ^1.9.7
|
||||||
'@welshman/lib': 0.8.8
|
'@welshman/lib': 0.8.12
|
||||||
nostr-tools: ^2.19.4
|
nostr-tools: ^2.19.4
|
||||||
|
|
||||||
'@xml-tools/parser@1.0.11':
|
'@xml-tools/parser@1.0.11':
|
||||||
@@ -3334,6 +3403,9 @@ packages:
|
|||||||
resolution: {integrity: sha512-/imKNG4EbWNrVjoNC/1H5/9GFy+tqjGBHCaSsN+P2RnPqjsLmv6UD3Ej+Kj8nBWaRAwyk7kK5ZUc+OEatnTR3A==}
|
resolution: {integrity: sha512-/imKNG4EbWNrVjoNC/1H5/9GFy+tqjGBHCaSsN+P2RnPqjsLmv6UD3Ej+Kj8nBWaRAwyk7kK5ZUc+OEatnTR3A==}
|
||||||
hasBin: true
|
hasBin: true
|
||||||
|
|
||||||
|
jose@6.2.1:
|
||||||
|
resolution: {integrity: sha512-jUaKr1yrbfaImV7R2TN/b3IcZzsw38/chqMpo2XJ7i2F8AfM/lA4G1goC3JVEwg0H7UldTmSt3P68nt31W7/mw==}
|
||||||
|
|
||||||
js-base64@3.7.8:
|
js-base64@3.7.8:
|
||||||
resolution: {integrity: sha512-hNngCeKxIUQiEUN3GPJOkz4wF/YvdUdbNL9hsBcMQTkKzboD7T/q3OYOuuPZLUE6dBxSGpwhk5mwuDud7JVAow==}
|
resolution: {integrity: sha512-hNngCeKxIUQiEUN3GPJOkz4wF/YvdUdbNL9hsBcMQTkKzboD7T/q3OYOuuPZLUE6dBxSGpwhk5mwuDud7JVAow==}
|
||||||
|
|
||||||
@@ -3438,6 +3510,11 @@ packages:
|
|||||||
linkifyjs@4.3.2:
|
linkifyjs@4.3.2:
|
||||||
resolution: {integrity: sha512-NT1CJtq3hHIreOianA8aSXn6Cw0JzYOuDQbOrSPe7gqFnCpKP++MQe3ODgO3oh2GJFORkAAdqredOa60z63GbA==}
|
resolution: {integrity: sha512-NT1CJtq3hHIreOianA8aSXn6Cw0JzYOuDQbOrSPe7gqFnCpKP++MQe3ODgO3oh2GJFORkAAdqredOa60z63GbA==}
|
||||||
|
|
||||||
|
livekit-client@2.17.3:
|
||||||
|
resolution: {integrity: sha512-htwsAL/BMylY/zwdcT/z00U789csbi9DldSW7DO+5tz7Q15pwu++E1X+ZdtZDfkmlysfQLLibdcqlyg9FY7veQ==}
|
||||||
|
peerDependencies:
|
||||||
|
'@types/dom-mediacapture-record': ^1
|
||||||
|
|
||||||
load-json-file@4.0.0:
|
load-json-file@4.0.0:
|
||||||
resolution: {integrity: sha512-Kx8hMakjX03tiGTLAIdJ+lL0htKnXjEZN6hk/tozf/WOuYGdZBJrZ+rCJRbVCugsjB3jMLn9746NsQIf5VjBMw==}
|
resolution: {integrity: sha512-Kx8hMakjX03tiGTLAIdJ+lL0htKnXjEZN6hk/tozf/WOuYGdZBJrZ+rCJRbVCugsjB3jMLn9746NsQIf5VjBMw==}
|
||||||
engines: {node: '>=4'}
|
engines: {node: '>=4'}
|
||||||
@@ -3472,6 +3549,10 @@ packages:
|
|||||||
lodash@4.17.23:
|
lodash@4.17.23:
|
||||||
resolution: {integrity: sha512-LgVTMpQtIopCi79SJeDiP0TfWi5CNEc/L/aRdTh3yIvmZXTnheWpKjSZhnvMl8iXbC1tFg9gdHHDMLoV7CnG+w==}
|
resolution: {integrity: sha512-LgVTMpQtIopCi79SJeDiP0TfWi5CNEc/L/aRdTh3yIvmZXTnheWpKjSZhnvMl8iXbC1tFg9gdHHDMLoV7CnG+w==}
|
||||||
|
|
||||||
|
loglevel@1.9.2:
|
||||||
|
resolution: {integrity: sha512-HgMmCqIJSAKqo68l0rS2AanEWfkxaZ5wNiEFb5ggm08lDs9Xl2KxBlX3PTcaD2chBM1gXAYf491/M2Rv8Jwayg==}
|
||||||
|
engines: {node: '>= 0.6.0'}
|
||||||
|
|
||||||
lru-cache@10.4.3:
|
lru-cache@10.4.3:
|
||||||
resolution: {integrity: sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==}
|
resolution: {integrity: sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==}
|
||||||
|
|
||||||
@@ -4273,6 +4354,9 @@ packages:
|
|||||||
run-parallel@1.2.0:
|
run-parallel@1.2.0:
|
||||||
resolution: {integrity: sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==}
|
resolution: {integrity: sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==}
|
||||||
|
|
||||||
|
rxjs@7.8.2:
|
||||||
|
resolution: {integrity: sha512-dhKf903U/PQZY6boNNtAGdWbG85WAbjT/1xYoZIC7FAY0yWapOBQVsVrDl58W86//e1VpMNBtRV4MaXfdMySFA==}
|
||||||
|
|
||||||
sade@1.8.1:
|
sade@1.8.1:
|
||||||
resolution: {integrity: sha512-xal3CZX1Xlo/k4ApwCFrHVACi9fBqJ7V+mwhBsuf/1IOKbBy098Fex+Wa/5QMubw09pSZ/u8EY8PWgevJsXp1A==}
|
resolution: {integrity: sha512-xal3CZX1Xlo/k4ApwCFrHVACi9fBqJ7V+mwhBsuf/1IOKbBy098Fex+Wa/5QMubw09pSZ/u8EY8PWgevJsXp1A==}
|
||||||
engines: {node: '>=6'}
|
engines: {node: '>=6'}
|
||||||
@@ -4302,6 +4386,13 @@ packages:
|
|||||||
resolution: {integrity: sha512-1n3r/tGXO6b6VXMdFT54SHzT9ytu9yr7TaELowdYpMqY/Ao7EnlQGmAQ1+RatX7Tkkdm6hONI2owqNx2aZj5Sw==}
|
resolution: {integrity: sha512-1n3r/tGXO6b6VXMdFT54SHzT9ytu9yr7TaELowdYpMqY/Ao7EnlQGmAQ1+RatX7Tkkdm6hONI2owqNx2aZj5Sw==}
|
||||||
engines: {node: '>=11.0.0'}
|
engines: {node: '>=11.0.0'}
|
||||||
|
|
||||||
|
sdp-transform@2.15.0:
|
||||||
|
resolution: {integrity: sha512-KrOH82c/W+GYQ0LHqtr3caRpM3ITglq3ljGUIb8LTki7ByacJZ9z+piSGiwZDsRyhQbYBOBJgr2k6X4BZXi3Kw==}
|
||||||
|
hasBin: true
|
||||||
|
|
||||||
|
sdp@3.2.1:
|
||||||
|
resolution: {integrity: sha512-lwsAIzOPlH8/7IIjjz3K0zYBk7aBVVcvjMwt3M4fLxpjMYyy7i3I97SLHebgn4YBjirkzfp3RvRDWSKsh/+WFw==}
|
||||||
|
|
||||||
semver@5.7.2:
|
semver@5.7.2:
|
||||||
resolution: {integrity: sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==}
|
resolution: {integrity: sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==}
|
||||||
hasBin: true
|
hasBin: true
|
||||||
@@ -4703,6 +4794,9 @@ packages:
|
|||||||
resolution: {integrity: sha512-3KS2b+kL7fsuk/eJZ7EQdnEmQoaho/r6KUef7hxvltNA5DR8NAUM+8wJMbJyZ4G9/7i3v5zPBIMN5aybAh2/Jg==}
|
resolution: {integrity: sha512-3KS2b+kL7fsuk/eJZ7EQdnEmQoaho/r6KUef7hxvltNA5DR8NAUM+8wJMbJyZ4G9/7i3v5zPBIMN5aybAh2/Jg==}
|
||||||
engines: {node: '>= 0.4'}
|
engines: {node: '>= 0.4'}
|
||||||
|
|
||||||
|
typed-emitter@2.1.0:
|
||||||
|
resolution: {integrity: sha512-g/KzbYKbH5C2vPkaXGu8DJlHrGKHLsM25Zg9WuC9pMGfuvT+X25tZQWo5fK1BjBm8+UrVE9LDCvaY0CQk+fXDA==}
|
||||||
|
|
||||||
typescript-eslint@8.53.1:
|
typescript-eslint@8.53.1:
|
||||||
resolution: {integrity: sha512-gB+EVQfP5RDElh9ittfXlhZJdjSU4jUSTyE2+ia8CYyNvet4ElfaLlAIqDvQV9JPknKx0jQH1racTYe/4LaLSg==}
|
resolution: {integrity: sha512-gB+EVQfP5RDElh9ittfXlhZJdjSU4jUSTyE2+ia8CYyNvet4ElfaLlAIqDvQV9JPknKx0jQH1racTYe/4LaLSg==}
|
||||||
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
|
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
|
||||||
@@ -4847,6 +4941,10 @@ packages:
|
|||||||
webidl-conversions@4.0.2:
|
webidl-conversions@4.0.2:
|
||||||
resolution: {integrity: sha512-YQ+BmxuTgd6UXZW3+ICGfyqRyHXVlD5GtQr5+qjiNW7bF0cqrzX500HVXPBOvgXb5YnzDd+h0zqyv61KUD7+Sg==}
|
resolution: {integrity: sha512-YQ+BmxuTgd6UXZW3+ICGfyqRyHXVlD5GtQr5+qjiNW7bF0cqrzX500HVXPBOvgXb5YnzDd+h0zqyv61KUD7+Sg==}
|
||||||
|
|
||||||
|
webrtc-adapter@9.0.4:
|
||||||
|
resolution: {integrity: sha512-5ZZY1+lGq8LEKuDlg9M2RPJHlH3R7OVwyHqMcUsLKCgd9Wvf+QrFTCItkXXYPmrJn8H6gRLXbSgxLLdexiqHxw==}
|
||||||
|
engines: {node: '>=6.0.0', npm: '>=3.10.0'}
|
||||||
|
|
||||||
whatwg-url@5.0.0:
|
whatwg-url@5.0.0:
|
||||||
resolution: {integrity: sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==}
|
resolution: {integrity: sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==}
|
||||||
|
|
||||||
@@ -5064,6 +5162,14 @@ snapshots:
|
|||||||
|
|
||||||
'@antfu/utils@0.7.10': {}
|
'@antfu/utils@0.7.10': {}
|
||||||
|
|
||||||
|
'@aparajita/capacitor-secure-storage@8.0.0':
|
||||||
|
dependencies:
|
||||||
|
'@capacitor/android': 8.2.0(@capacitor/core@8.2.0)
|
||||||
|
'@capacitor/app': 8.0.0(@capacitor/core@8.2.0)
|
||||||
|
'@capacitor/core': 8.2.0
|
||||||
|
'@capacitor/ios': 8.2.0(@capacitor/core@8.2.0)
|
||||||
|
'@capacitor/keyboard': 8.0.0(@capacitor/core@8.2.0)
|
||||||
|
|
||||||
'@apideck/better-ajv-errors@0.3.6(ajv@8.18.0)':
|
'@apideck/better-ajv-errors@0.3.6(ajv@8.18.0)':
|
||||||
dependencies:
|
dependencies:
|
||||||
ajv: 8.18.0
|
ajv: 8.18.0
|
||||||
@@ -5733,6 +5839,8 @@ snapshots:
|
|||||||
|
|
||||||
'@braintree/sanitize-url@7.1.1': {}
|
'@braintree/sanitize-url@7.1.1': {}
|
||||||
|
|
||||||
|
'@bufbuild/protobuf@1.10.1': {}
|
||||||
|
|
||||||
'@canvas/image-data@1.1.0': {}
|
'@canvas/image-data@1.1.0': {}
|
||||||
|
|
||||||
'@capacitor-community/safe-area@8.0.1(@capacitor/core@8.0.1)':
|
'@capacitor-community/safe-area@8.0.1(@capacitor/core@8.0.1)':
|
||||||
@@ -5743,10 +5851,18 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
'@capacitor/core': 8.0.1
|
'@capacitor/core': 8.0.1
|
||||||
|
|
||||||
|
'@capacitor/android@8.2.0(@capacitor/core@8.2.0)':
|
||||||
|
dependencies:
|
||||||
|
'@capacitor/core': 8.2.0
|
||||||
|
|
||||||
'@capacitor/app@8.0.0(@capacitor/core@8.0.1)':
|
'@capacitor/app@8.0.0(@capacitor/core@8.0.1)':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@capacitor/core': 8.0.1
|
'@capacitor/core': 8.0.1
|
||||||
|
|
||||||
|
'@capacitor/app@8.0.0(@capacitor/core@8.2.0)':
|
||||||
|
dependencies:
|
||||||
|
'@capacitor/core': 8.2.0
|
||||||
|
|
||||||
'@capacitor/assets@3.0.5(@types/node@25.0.10)(typescript@5.9.3)':
|
'@capacitor/assets@3.0.5(@types/node@25.0.10)(typescript@5.9.3)':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@capacitor/cli': 5.7.8
|
'@capacitor/cli': 5.7.8
|
||||||
@@ -5817,6 +5933,10 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
tslib: 2.8.1
|
tslib: 2.8.1
|
||||||
|
|
||||||
|
'@capacitor/core@8.2.0':
|
||||||
|
dependencies:
|
||||||
|
tslib: 2.8.1
|
||||||
|
|
||||||
'@capacitor/filesystem@8.1.0(@capacitor/core@8.0.1)':
|
'@capacitor/filesystem@8.1.0(@capacitor/core@8.0.1)':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@capacitor/core': 8.0.1
|
'@capacitor/core': 8.0.1
|
||||||
@@ -5826,10 +5946,18 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
'@capacitor/core': 8.0.1
|
'@capacitor/core': 8.0.1
|
||||||
|
|
||||||
|
'@capacitor/ios@8.2.0(@capacitor/core@8.2.0)':
|
||||||
|
dependencies:
|
||||||
|
'@capacitor/core': 8.2.0
|
||||||
|
|
||||||
'@capacitor/keyboard@8.0.0(@capacitor/core@8.0.1)':
|
'@capacitor/keyboard@8.0.0(@capacitor/core@8.0.1)':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@capacitor/core': 8.0.1
|
'@capacitor/core': 8.0.1
|
||||||
|
|
||||||
|
'@capacitor/keyboard@8.0.0(@capacitor/core@8.2.0)':
|
||||||
|
dependencies:
|
||||||
|
'@capacitor/core': 8.2.0
|
||||||
|
|
||||||
'@capacitor/preferences@8.0.0(@capacitor/core@8.0.1)':
|
'@capacitor/preferences@8.0.0(@capacitor/core@8.0.1)':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@capacitor/core': 8.0.1
|
'@capacitor/core': 8.0.1
|
||||||
@@ -6298,6 +6426,12 @@ snapshots:
|
|||||||
'@jridgewell/resolve-uri': 3.1.2
|
'@jridgewell/resolve-uri': 3.1.2
|
||||||
'@jridgewell/sourcemap-codec': 1.5.5
|
'@jridgewell/sourcemap-codec': 1.5.5
|
||||||
|
|
||||||
|
'@livekit/mutex@1.1.1': {}
|
||||||
|
|
||||||
|
'@livekit/protocol@1.44.0':
|
||||||
|
dependencies:
|
||||||
|
'@bufbuild/protobuf': 1.10.1
|
||||||
|
|
||||||
'@noble/ciphers@0.5.3': {}
|
'@noble/ciphers@0.5.3': {}
|
||||||
|
|
||||||
'@noble/ciphers@1.3.0': {}
|
'@noble/ciphers@1.3.0': {}
|
||||||
@@ -6436,15 +6570,15 @@ snapshots:
|
|||||||
|
|
||||||
'@polka/url@1.0.0-next.29': {}
|
'@polka/url@1.0.0-next.29': {}
|
||||||
|
|
||||||
'@pomade/core@0.2.1(@frostr/bifrost@1.0.7(typescript@5.9.3))(@noble/hashes@2.0.1)(@welshman/lib@0.8.8)(@welshman/net@0.8.8(@welshman/lib@0.8.8)(@welshman/util@0.8.8(@noble/curves@1.9.7)(@welshman/lib@0.8.8)(nostr-tools@2.20.0(typescript@5.9.3)))(ws@8.18.3))(@welshman/signer@0.8.8(@noble/curves@1.9.7)(@noble/hashes@2.0.1)(@welshman/lib@0.8.8)(@welshman/net@0.8.8(@welshman/lib@0.8.8)(@welshman/util@0.8.8(@noble/curves@1.9.7)(@welshman/lib@0.8.8)(nostr-tools@2.20.0(typescript@5.9.3)))(ws@8.18.3))(@welshman/util@0.8.8(@noble/curves@1.9.7)(@welshman/lib@0.8.8)(nostr-tools@2.20.0(typescript@5.9.3)))(nostr-signer-capacitor-plugin@https://codeload.github.com/coracle-social/nostr-signer-capacitor-plugin/tar.gz/be4bb90a1a15c8eec0934f2f66ce9e82ecc72d51(@capacitor/core@8.0.1))(nostr-tools@2.20.0(typescript@5.9.3)))(@welshman/util@0.8.8(@noble/curves@1.9.7)(@welshman/lib@0.8.8)(nostr-tools@2.20.0(typescript@5.9.3)))(nostr-tools@2.20.0(typescript@5.9.3))':
|
'@pomade/core@0.2.2(@frostr/bifrost@1.0.7(typescript@5.9.3))(@noble/hashes@2.0.1)(@welshman/lib@0.8.12)(@welshman/net@0.8.12(@welshman/lib@0.8.12)(@welshman/util@0.8.12(@noble/curves@1.9.7)(@welshman/lib@0.8.12)(nostr-tools@2.20.0(typescript@5.9.3)))(ws@8.18.3))(@welshman/signer@0.8.12(@noble/curves@1.9.7)(@noble/hashes@2.0.1)(@welshman/lib@0.8.12)(@welshman/net@0.8.12(@welshman/lib@0.8.12)(@welshman/util@0.8.12(@noble/curves@1.9.7)(@welshman/lib@0.8.12)(nostr-tools@2.20.0(typescript@5.9.3)))(ws@8.18.3))(@welshman/util@0.8.12(@noble/curves@1.9.7)(@welshman/lib@0.8.12)(nostr-tools@2.20.0(typescript@5.9.3)))(nostr-signer-capacitor-plugin@https://codeload.github.com/coracle-social/nostr-signer-capacitor-plugin/tar.gz/be4bb90a1a15c8eec0934f2f66ce9e82ecc72d51(@capacitor/core@8.0.1))(nostr-tools@2.20.0(typescript@5.9.3)))(@welshman/util@0.8.12(@noble/curves@1.9.7)(@welshman/lib@0.8.12)(nostr-tools@2.20.0(typescript@5.9.3)))(nostr-tools@2.20.0(typescript@5.9.3))':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@frostr/bifrost': 1.0.7(typescript@5.9.3)
|
'@frostr/bifrost': 1.0.7(typescript@5.9.3)
|
||||||
'@noble/hashes': 2.0.1
|
'@noble/hashes': 2.0.1
|
||||||
'@peculiar/x509': 1.14.3
|
'@peculiar/x509': 1.14.3
|
||||||
'@welshman/lib': 0.8.8
|
'@welshman/lib': 0.8.12
|
||||||
'@welshman/net': 0.8.8(@welshman/lib@0.8.8)(@welshman/util@0.8.8(@noble/curves@1.9.7)(@welshman/lib@0.8.8)(nostr-tools@2.20.0(typescript@5.9.3)))(ws@8.18.3)
|
'@welshman/net': 0.8.12(@welshman/lib@0.8.12)(@welshman/util@0.8.12(@noble/curves@1.9.7)(@welshman/lib@0.8.12)(nostr-tools@2.20.0(typescript@5.9.3)))(ws@8.18.3)
|
||||||
'@welshman/signer': 0.8.8(@noble/curves@1.9.7)(@noble/hashes@2.0.1)(@welshman/lib@0.8.8)(@welshman/net@0.8.8(@welshman/lib@0.8.8)(@welshman/util@0.8.8(@noble/curves@1.9.7)(@welshman/lib@0.8.8)(nostr-tools@2.20.0(typescript@5.9.3)))(ws@8.18.3))(@welshman/util@0.8.8(@noble/curves@1.9.7)(@welshman/lib@0.8.8)(nostr-tools@2.20.0(typescript@5.9.3)))(nostr-signer-capacitor-plugin@https://codeload.github.com/coracle-social/nostr-signer-capacitor-plugin/tar.gz/be4bb90a1a15c8eec0934f2f66ce9e82ecc72d51(@capacitor/core@8.0.1))(nostr-tools@2.20.0(typescript@5.9.3))
|
'@welshman/signer': 0.8.12(@noble/curves@1.9.7)(@noble/hashes@2.0.1)(@welshman/lib@0.8.12)(@welshman/net@0.8.12(@welshman/lib@0.8.12)(@welshman/util@0.8.12(@noble/curves@1.9.7)(@welshman/lib@0.8.12)(nostr-tools@2.20.0(typescript@5.9.3)))(ws@8.18.3))(@welshman/util@0.8.12(@noble/curves@1.9.7)(@welshman/lib@0.8.12)(nostr-tools@2.20.0(typescript@5.9.3)))(nostr-signer-capacitor-plugin@https://codeload.github.com/coracle-social/nostr-signer-capacitor-plugin/tar.gz/be4bb90a1a15c8eec0934f2f66ce9e82ecc72d51(@capacitor/core@8.0.1))(nostr-tools@2.20.0(typescript@5.9.3))
|
||||||
'@welshman/util': 0.8.8(@noble/curves@1.9.7)(@welshman/lib@0.8.8)(nostr-tools@2.20.0(typescript@5.9.3))
|
'@welshman/util': 0.8.12(@noble/curves@1.9.7)(@welshman/lib@0.8.12)(nostr-tools@2.20.0(typescript@5.9.3))
|
||||||
cbor-x: 1.6.0
|
cbor-x: 1.6.0
|
||||||
hash-wasm: 4.12.0
|
hash-wasm: 4.12.0
|
||||||
nostr-tools: 2.20.0(typescript@5.9.3)
|
nostr-tools: 2.20.0(typescript@5.9.3)
|
||||||
@@ -6859,6 +6993,8 @@ snapshots:
|
|||||||
|
|
||||||
'@types/cookie@0.6.0': {}
|
'@types/cookie@0.6.0': {}
|
||||||
|
|
||||||
|
'@types/dom-mediacapture-record@1.0.22': {}
|
||||||
|
|
||||||
'@types/eslint@9.6.1':
|
'@types/eslint@9.6.1':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@types/estree': 1.0.8
|
'@types/estree': 1.0.8
|
||||||
@@ -7031,26 +7167,26 @@ snapshots:
|
|||||||
optionalDependencies:
|
optionalDependencies:
|
||||||
'@vite-pwa/assets-generator': 0.2.6
|
'@vite-pwa/assets-generator': 0.2.6
|
||||||
|
|
||||||
'@welshman/app@0.8.8(b90dd618d8ad3ba87405490e903259ce)':
|
'@welshman/app@0.8.12(3074ef6691f94dc03952d8dbc98013a7)':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@pomade/core': 0.2.1(@frostr/bifrost@1.0.7(typescript@5.9.3))(@noble/hashes@2.0.1)(@welshman/lib@0.8.8)(@welshman/net@0.8.8(@welshman/lib@0.8.8)(@welshman/util@0.8.8(@noble/curves@1.9.7)(@welshman/lib@0.8.8)(nostr-tools@2.20.0(typescript@5.9.3)))(ws@8.18.3))(@welshman/signer@0.8.8(@noble/curves@1.9.7)(@noble/hashes@2.0.1)(@welshman/lib@0.8.8)(@welshman/net@0.8.8(@welshman/lib@0.8.8)(@welshman/util@0.8.8(@noble/curves@1.9.7)(@welshman/lib@0.8.8)(nostr-tools@2.20.0(typescript@5.9.3)))(ws@8.18.3))(@welshman/util@0.8.8(@noble/curves@1.9.7)(@welshman/lib@0.8.8)(nostr-tools@2.20.0(typescript@5.9.3)))(nostr-signer-capacitor-plugin@https://codeload.github.com/coracle-social/nostr-signer-capacitor-plugin/tar.gz/be4bb90a1a15c8eec0934f2f66ce9e82ecc72d51(@capacitor/core@8.0.1))(nostr-tools@2.20.0(typescript@5.9.3)))(@welshman/util@0.8.8(@noble/curves@1.9.7)(@welshman/lib@0.8.8)(nostr-tools@2.20.0(typescript@5.9.3)))(nostr-tools@2.20.0(typescript@5.9.3))
|
'@pomade/core': 0.2.2(@frostr/bifrost@1.0.7(typescript@5.9.3))(@noble/hashes@2.0.1)(@welshman/lib@0.8.12)(@welshman/net@0.8.12(@welshman/lib@0.8.12)(@welshman/util@0.8.12(@noble/curves@1.9.7)(@welshman/lib@0.8.12)(nostr-tools@2.20.0(typescript@5.9.3)))(ws@8.18.3))(@welshman/signer@0.8.12(@noble/curves@1.9.7)(@noble/hashes@2.0.1)(@welshman/lib@0.8.12)(@welshman/net@0.8.12(@welshman/lib@0.8.12)(@welshman/util@0.8.12(@noble/curves@1.9.7)(@welshman/lib@0.8.12)(nostr-tools@2.20.0(typescript@5.9.3)))(ws@8.18.3))(@welshman/util@0.8.12(@noble/curves@1.9.7)(@welshman/lib@0.8.12)(nostr-tools@2.20.0(typescript@5.9.3)))(nostr-signer-capacitor-plugin@https://codeload.github.com/coracle-social/nostr-signer-capacitor-plugin/tar.gz/be4bb90a1a15c8eec0934f2f66ce9e82ecc72d51(@capacitor/core@8.0.1))(nostr-tools@2.20.0(typescript@5.9.3)))(@welshman/util@0.8.12(@noble/curves@1.9.7)(@welshman/lib@0.8.12)(nostr-tools@2.20.0(typescript@5.9.3)))(nostr-tools@2.20.0(typescript@5.9.3))
|
||||||
'@welshman/feeds': 0.8.8(827c582d718d0d373e9315813bab1085)
|
'@welshman/feeds': 0.8.12(d5b74f0c83250e052e0b96f7ff5804e8)
|
||||||
'@welshman/lib': 0.8.8
|
'@welshman/lib': 0.8.12
|
||||||
'@welshman/net': 0.8.8(@welshman/lib@0.8.8)(@welshman/util@0.8.8(@noble/curves@1.9.7)(@welshman/lib@0.8.8)(nostr-tools@2.20.0(typescript@5.9.3)))(ws@8.18.3)
|
'@welshman/net': 0.8.12(@welshman/lib@0.8.12)(@welshman/util@0.8.12(@noble/curves@1.9.7)(@welshman/lib@0.8.12)(nostr-tools@2.20.0(typescript@5.9.3)))(ws@8.18.3)
|
||||||
'@welshman/router': 0.8.8(@welshman/lib@0.8.8)(@welshman/net@0.8.8(@welshman/lib@0.8.8)(@welshman/util@0.8.8(@noble/curves@1.9.7)(@welshman/lib@0.8.8)(nostr-tools@2.20.0(typescript@5.9.3)))(ws@8.18.3))(@welshman/util@0.8.8(@noble/curves@1.9.7)(@welshman/lib@0.8.8)(nostr-tools@2.20.0(typescript@5.9.3)))
|
'@welshman/router': 0.8.12(@welshman/lib@0.8.12)(@welshman/net@0.8.12(@welshman/lib@0.8.12)(@welshman/util@0.8.12(@noble/curves@1.9.7)(@welshman/lib@0.8.12)(nostr-tools@2.20.0(typescript@5.9.3)))(ws@8.18.3))(@welshman/util@0.8.12(@noble/curves@1.9.7)(@welshman/lib@0.8.12)(nostr-tools@2.20.0(typescript@5.9.3)))
|
||||||
'@welshman/signer': 0.8.8(@noble/curves@1.9.7)(@noble/hashes@2.0.1)(@welshman/lib@0.8.8)(@welshman/net@0.8.8(@welshman/lib@0.8.8)(@welshman/util@0.8.8(@noble/curves@1.9.7)(@welshman/lib@0.8.8)(nostr-tools@2.20.0(typescript@5.9.3)))(ws@8.18.3))(@welshman/util@0.8.8(@noble/curves@1.9.7)(@welshman/lib@0.8.8)(nostr-tools@2.20.0(typescript@5.9.3)))(nostr-signer-capacitor-plugin@https://codeload.github.com/coracle-social/nostr-signer-capacitor-plugin/tar.gz/be4bb90a1a15c8eec0934f2f66ce9e82ecc72d51(@capacitor/core@8.0.1))(nostr-tools@2.20.0(typescript@5.9.3))
|
'@welshman/signer': 0.8.12(@noble/curves@1.9.7)(@noble/hashes@2.0.1)(@welshman/lib@0.8.12)(@welshman/net@0.8.12(@welshman/lib@0.8.12)(@welshman/util@0.8.12(@noble/curves@1.9.7)(@welshman/lib@0.8.12)(nostr-tools@2.20.0(typescript@5.9.3)))(ws@8.18.3))(@welshman/util@0.8.12(@noble/curves@1.9.7)(@welshman/lib@0.8.12)(nostr-tools@2.20.0(typescript@5.9.3)))(nostr-signer-capacitor-plugin@https://codeload.github.com/coracle-social/nostr-signer-capacitor-plugin/tar.gz/be4bb90a1a15c8eec0934f2f66ce9e82ecc72d51(@capacitor/core@8.0.1))(nostr-tools@2.20.0(typescript@5.9.3))
|
||||||
'@welshman/store': 0.8.8(@welshman/lib@0.8.8)(@welshman/net@0.8.8(@welshman/lib@0.8.8)(@welshman/util@0.8.8(@noble/curves@1.9.7)(@welshman/lib@0.8.8)(nostr-tools@2.20.0(typescript@5.9.3)))(ws@8.18.3))(@welshman/util@0.8.8(@noble/curves@1.9.7)(@welshman/lib@0.8.8)(nostr-tools@2.20.0(typescript@5.9.3)))(svelte@5.48.0)
|
'@welshman/store': 0.8.12(@welshman/lib@0.8.12)(@welshman/net@0.8.12(@welshman/lib@0.8.12)(@welshman/util@0.8.12(@noble/curves@1.9.7)(@welshman/lib@0.8.12)(nostr-tools@2.20.0(typescript@5.9.3)))(ws@8.18.3))(@welshman/util@0.8.12(@noble/curves@1.9.7)(@welshman/lib@0.8.12)(nostr-tools@2.20.0(typescript@5.9.3)))(svelte@5.48.0)
|
||||||
'@welshman/util': 0.8.8(@noble/curves@1.9.7)(@welshman/lib@0.8.8)(nostr-tools@2.20.0(typescript@5.9.3))
|
'@welshman/util': 0.8.12(@noble/curves@1.9.7)(@welshman/lib@0.8.12)(nostr-tools@2.20.0(typescript@5.9.3))
|
||||||
fuse.js: 7.1.0
|
fuse.js: 7.1.0
|
||||||
svelte: 5.48.0
|
svelte: 5.48.0
|
||||||
throttle-debounce: 5.0.2
|
throttle-debounce: 5.0.2
|
||||||
|
|
||||||
'@welshman/content@0.8.8(nostr-tools@2.20.0(typescript@5.9.3))':
|
'@welshman/content@0.8.12(nostr-tools@2.20.0(typescript@5.9.3))':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@braintree/sanitize-url': 7.1.1
|
'@braintree/sanitize-url': 7.1.1
|
||||||
nostr-tools: 2.20.0(typescript@5.9.3)
|
nostr-tools: 2.20.0(typescript@5.9.3)
|
||||||
|
|
||||||
'@welshman/editor@0.8.8(@welshman/lib@0.8.8)(@welshman/util@0.8.8(@noble/curves@1.9.7)(@welshman/lib@0.8.8)(nostr-tools@2.20.0(typescript@5.9.3)))(nostr-editor@1.1.1(@tiptap/core@2.27.2(@tiptap/pm@2.27.2))(@tiptap/extension-image@2.27.1(@tiptap/core@2.27.2(@tiptap/pm@2.27.2)))(@tiptap/extension-link@2.27.1(@tiptap/core@2.27.2(@tiptap/pm@2.27.2))(@tiptap/pm@2.27.2))(@tiptap/pm@2.27.2)(linkifyjs@4.3.2)(nostr-tools@2.20.0(typescript@5.9.3))(prosemirror-markdown@1.13.3)(prosemirror-model@1.25.4)(prosemirror-state@1.4.4)(prosemirror-view@1.41.5)(tiptap-markdown@0.8.10(@tiptap/core@2.27.2(@tiptap/pm@2.27.2))))(nostr-tools@2.20.0(typescript@5.9.3))':
|
'@welshman/editor@0.8.12(@welshman/lib@0.8.12)(@welshman/util@0.8.12(@noble/curves@1.9.7)(@welshman/lib@0.8.12)(nostr-tools@2.20.0(typescript@5.9.3)))(nostr-editor@1.1.1(@tiptap/core@2.27.2(@tiptap/pm@2.27.2))(@tiptap/extension-image@2.27.1(@tiptap/core@2.27.2(@tiptap/pm@2.27.2)))(@tiptap/extension-link@2.27.1(@tiptap/core@2.27.2(@tiptap/pm@2.27.2))(@tiptap/pm@2.27.2))(@tiptap/pm@2.27.2)(linkifyjs@4.3.2)(nostr-tools@2.20.0(typescript@5.9.3))(prosemirror-markdown@1.13.3)(prosemirror-model@1.25.4)(prosemirror-state@1.4.4)(prosemirror-view@1.41.5)(tiptap-markdown@0.8.10(@tiptap/core@2.27.2(@tiptap/pm@2.27.2))))(nostr-tools@2.20.0(typescript@5.9.3))':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@tiptap/core': 2.27.2(@tiptap/pm@2.27.2)
|
'@tiptap/core': 2.27.2(@tiptap/pm@2.27.2)
|
||||||
'@tiptap/extension-code': 2.27.2(@tiptap/core@2.27.2(@tiptap/pm@2.27.2))
|
'@tiptap/extension-code': 2.27.2(@tiptap/core@2.27.2(@tiptap/pm@2.27.2))
|
||||||
@@ -7065,64 +7201,64 @@ snapshots:
|
|||||||
'@tiptap/extension-text': 2.27.2(@tiptap/core@2.27.2(@tiptap/pm@2.27.2))
|
'@tiptap/extension-text': 2.27.2(@tiptap/core@2.27.2(@tiptap/pm@2.27.2))
|
||||||
'@tiptap/pm': 2.27.2
|
'@tiptap/pm': 2.27.2
|
||||||
'@tiptap/suggestion': 2.27.2(@tiptap/core@2.27.2(@tiptap/pm@2.27.2))(@tiptap/pm@2.27.2)
|
'@tiptap/suggestion': 2.27.2(@tiptap/core@2.27.2(@tiptap/pm@2.27.2))(@tiptap/pm@2.27.2)
|
||||||
'@welshman/lib': 0.8.8
|
'@welshman/lib': 0.8.12
|
||||||
'@welshman/util': 0.8.8(@noble/curves@1.9.7)(@welshman/lib@0.8.8)(nostr-tools@2.20.0(typescript@5.9.3))
|
'@welshman/util': 0.8.12(@noble/curves@1.9.7)(@welshman/lib@0.8.12)(nostr-tools@2.20.0(typescript@5.9.3))
|
||||||
nostr-editor: 1.1.1(@tiptap/core@2.27.2(@tiptap/pm@2.27.2))(@tiptap/extension-image@2.27.1(@tiptap/core@2.27.2(@tiptap/pm@2.27.2)))(@tiptap/extension-link@2.27.1(@tiptap/core@2.27.2(@tiptap/pm@2.27.2))(@tiptap/pm@2.27.2))(@tiptap/pm@2.27.2)(linkifyjs@4.3.2)(nostr-tools@2.20.0(typescript@5.9.3))(prosemirror-markdown@1.13.3)(prosemirror-model@1.25.4)(prosemirror-state@1.4.4)(prosemirror-view@1.41.5)(tiptap-markdown@0.8.10(@tiptap/core@2.27.2(@tiptap/pm@2.27.2)))
|
nostr-editor: 1.1.1(@tiptap/core@2.27.2(@tiptap/pm@2.27.2))(@tiptap/extension-image@2.27.1(@tiptap/core@2.27.2(@tiptap/pm@2.27.2)))(@tiptap/extension-link@2.27.1(@tiptap/core@2.27.2(@tiptap/pm@2.27.2))(@tiptap/pm@2.27.2))(@tiptap/pm@2.27.2)(linkifyjs@4.3.2)(nostr-tools@2.20.0(typescript@5.9.3))(prosemirror-markdown@1.13.3)(prosemirror-model@1.25.4)(prosemirror-state@1.4.4)(prosemirror-view@1.41.5)(tiptap-markdown@0.8.10(@tiptap/core@2.27.2(@tiptap/pm@2.27.2)))
|
||||||
nostr-tools: 2.20.0(typescript@5.9.3)
|
nostr-tools: 2.20.0(typescript@5.9.3)
|
||||||
tippy.js: 6.3.7
|
tippy.js: 6.3.7
|
||||||
|
|
||||||
'@welshman/feeds@0.8.8(827c582d718d0d373e9315813bab1085)':
|
'@welshman/feeds@0.8.12(d5b74f0c83250e052e0b96f7ff5804e8)':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@welshman/lib': 0.8.8
|
'@welshman/lib': 0.8.12
|
||||||
'@welshman/net': 0.8.8(@welshman/lib@0.8.8)(@welshman/util@0.8.8(@noble/curves@1.9.7)(@welshman/lib@0.8.8)(nostr-tools@2.20.0(typescript@5.9.3)))(ws@8.18.3)
|
'@welshman/net': 0.8.12(@welshman/lib@0.8.12)(@welshman/util@0.8.12(@noble/curves@1.9.7)(@welshman/lib@0.8.12)(nostr-tools@2.20.0(typescript@5.9.3)))(ws@8.18.3)
|
||||||
'@welshman/router': 0.8.8(@welshman/lib@0.8.8)(@welshman/net@0.8.8(@welshman/lib@0.8.8)(@welshman/util@0.8.8(@noble/curves@1.9.7)(@welshman/lib@0.8.8)(nostr-tools@2.20.0(typescript@5.9.3)))(ws@8.18.3))(@welshman/util@0.8.8(@noble/curves@1.9.7)(@welshman/lib@0.8.8)(nostr-tools@2.20.0(typescript@5.9.3)))
|
'@welshman/router': 0.8.12(@welshman/lib@0.8.12)(@welshman/net@0.8.12(@welshman/lib@0.8.12)(@welshman/util@0.8.12(@noble/curves@1.9.7)(@welshman/lib@0.8.12)(nostr-tools@2.20.0(typescript@5.9.3)))(ws@8.18.3))(@welshman/util@0.8.12(@noble/curves@1.9.7)(@welshman/lib@0.8.12)(nostr-tools@2.20.0(typescript@5.9.3)))
|
||||||
'@welshman/signer': 0.8.8(@noble/curves@1.9.7)(@noble/hashes@2.0.1)(@welshman/lib@0.8.8)(@welshman/net@0.8.8(@welshman/lib@0.8.8)(@welshman/util@0.8.8(@noble/curves@1.9.7)(@welshman/lib@0.8.8)(nostr-tools@2.20.0(typescript@5.9.3)))(ws@8.18.3))(@welshman/util@0.8.8(@noble/curves@1.9.7)(@welshman/lib@0.8.8)(nostr-tools@2.20.0(typescript@5.9.3)))(nostr-signer-capacitor-plugin@https://codeload.github.com/coracle-social/nostr-signer-capacitor-plugin/tar.gz/be4bb90a1a15c8eec0934f2f66ce9e82ecc72d51(@capacitor/core@8.0.1))(nostr-tools@2.20.0(typescript@5.9.3))
|
'@welshman/signer': 0.8.12(@noble/curves@1.9.7)(@noble/hashes@2.0.1)(@welshman/lib@0.8.12)(@welshman/net@0.8.12(@welshman/lib@0.8.12)(@welshman/util@0.8.12(@noble/curves@1.9.7)(@welshman/lib@0.8.12)(nostr-tools@2.20.0(typescript@5.9.3)))(ws@8.18.3))(@welshman/util@0.8.12(@noble/curves@1.9.7)(@welshman/lib@0.8.12)(nostr-tools@2.20.0(typescript@5.9.3)))(nostr-signer-capacitor-plugin@https://codeload.github.com/coracle-social/nostr-signer-capacitor-plugin/tar.gz/be4bb90a1a15c8eec0934f2f66ce9e82ecc72d51(@capacitor/core@8.0.1))(nostr-tools@2.20.0(typescript@5.9.3))
|
||||||
'@welshman/util': 0.8.8(@noble/curves@1.9.7)(@welshman/lib@0.8.8)(nostr-tools@2.20.0(typescript@5.9.3))
|
'@welshman/util': 0.8.12(@noble/curves@1.9.7)(@welshman/lib@0.8.12)(nostr-tools@2.20.0(typescript@5.9.3))
|
||||||
trava: 1.2.1
|
trava: 1.2.1
|
||||||
|
|
||||||
'@welshman/lib@0.8.8':
|
'@welshman/lib@0.8.12':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@scure/base': 1.2.6
|
'@scure/base': 1.2.6
|
||||||
'@types/events': 3.0.3
|
'@types/events': 3.0.3
|
||||||
events: 3.3.0
|
events: 3.3.0
|
||||||
|
|
||||||
'@welshman/net@0.8.8(@welshman/lib@0.8.8)(@welshman/util@0.8.8(@noble/curves@1.9.7)(@welshman/lib@0.8.8)(nostr-tools@2.20.0(typescript@5.9.3)))(ws@8.18.3)':
|
'@welshman/net@0.8.12(@welshman/lib@0.8.12)(@welshman/util@0.8.12(@noble/curves@1.9.7)(@welshman/lib@0.8.12)(nostr-tools@2.20.0(typescript@5.9.3)))(ws@8.18.3)':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@welshman/lib': 0.8.8
|
'@welshman/lib': 0.8.12
|
||||||
'@welshman/util': 0.8.8(@noble/curves@1.9.7)(@welshman/lib@0.8.8)(nostr-tools@2.20.0(typescript@5.9.3))
|
'@welshman/util': 0.8.12(@noble/curves@1.9.7)(@welshman/lib@0.8.12)(nostr-tools@2.20.0(typescript@5.9.3))
|
||||||
events: 3.3.0
|
events: 3.3.0
|
||||||
isomorphic-ws: 5.0.0(ws@8.18.3)
|
isomorphic-ws: 5.0.0(ws@8.18.3)
|
||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- ws
|
- ws
|
||||||
|
|
||||||
'@welshman/router@0.8.8(@welshman/lib@0.8.8)(@welshman/net@0.8.8(@welshman/lib@0.8.8)(@welshman/util@0.8.8(@noble/curves@1.9.7)(@welshman/lib@0.8.8)(nostr-tools@2.20.0(typescript@5.9.3)))(ws@8.18.3))(@welshman/util@0.8.8(@noble/curves@1.9.7)(@welshman/lib@0.8.8)(nostr-tools@2.20.0(typescript@5.9.3)))':
|
'@welshman/router@0.8.12(@welshman/lib@0.8.12)(@welshman/net@0.8.12(@welshman/lib@0.8.12)(@welshman/util@0.8.12(@noble/curves@1.9.7)(@welshman/lib@0.8.12)(nostr-tools@2.20.0(typescript@5.9.3)))(ws@8.18.3))(@welshman/util@0.8.12(@noble/curves@1.9.7)(@welshman/lib@0.8.12)(nostr-tools@2.20.0(typescript@5.9.3)))':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@welshman/lib': 0.8.8
|
'@welshman/lib': 0.8.12
|
||||||
'@welshman/net': 0.8.8(@welshman/lib@0.8.8)(@welshman/util@0.8.8(@noble/curves@1.9.7)(@welshman/lib@0.8.8)(nostr-tools@2.20.0(typescript@5.9.3)))(ws@8.18.3)
|
'@welshman/net': 0.8.12(@welshman/lib@0.8.12)(@welshman/util@0.8.12(@noble/curves@1.9.7)(@welshman/lib@0.8.12)(nostr-tools@2.20.0(typescript@5.9.3)))(ws@8.18.3)
|
||||||
'@welshman/util': 0.8.8(@noble/curves@1.9.7)(@welshman/lib@0.8.8)(nostr-tools@2.20.0(typescript@5.9.3))
|
'@welshman/util': 0.8.12(@noble/curves@1.9.7)(@welshman/lib@0.8.12)(nostr-tools@2.20.0(typescript@5.9.3))
|
||||||
|
|
||||||
'@welshman/signer@0.8.8(@noble/curves@1.9.7)(@noble/hashes@2.0.1)(@welshman/lib@0.8.8)(@welshman/net@0.8.8(@welshman/lib@0.8.8)(@welshman/util@0.8.8(@noble/curves@1.9.7)(@welshman/lib@0.8.8)(nostr-tools@2.20.0(typescript@5.9.3)))(ws@8.18.3))(@welshman/util@0.8.8(@noble/curves@1.9.7)(@welshman/lib@0.8.8)(nostr-tools@2.20.0(typescript@5.9.3)))(nostr-signer-capacitor-plugin@https://codeload.github.com/coracle-social/nostr-signer-capacitor-plugin/tar.gz/be4bb90a1a15c8eec0934f2f66ce9e82ecc72d51(@capacitor/core@8.0.1))(nostr-tools@2.20.0(typescript@5.9.3))':
|
'@welshman/signer@0.8.12(@noble/curves@1.9.7)(@noble/hashes@2.0.1)(@welshman/lib@0.8.12)(@welshman/net@0.8.12(@welshman/lib@0.8.12)(@welshman/util@0.8.12(@noble/curves@1.9.7)(@welshman/lib@0.8.12)(nostr-tools@2.20.0(typescript@5.9.3)))(ws@8.18.3))(@welshman/util@0.8.12(@noble/curves@1.9.7)(@welshman/lib@0.8.12)(nostr-tools@2.20.0(typescript@5.9.3)))(nostr-signer-capacitor-plugin@https://codeload.github.com/coracle-social/nostr-signer-capacitor-plugin/tar.gz/be4bb90a1a15c8eec0934f2f66ce9e82ecc72d51(@capacitor/core@8.0.1))(nostr-tools@2.20.0(typescript@5.9.3))':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@noble/curves': 1.9.7
|
'@noble/curves': 1.9.7
|
||||||
'@noble/hashes': 2.0.1
|
'@noble/hashes': 2.0.1
|
||||||
'@welshman/lib': 0.8.8
|
'@welshman/lib': 0.8.12
|
||||||
'@welshman/net': 0.8.8(@welshman/lib@0.8.8)(@welshman/util@0.8.8(@noble/curves@1.9.7)(@welshman/lib@0.8.8)(nostr-tools@2.20.0(typescript@5.9.3)))(ws@8.18.3)
|
'@welshman/net': 0.8.12(@welshman/lib@0.8.12)(@welshman/util@0.8.12(@noble/curves@1.9.7)(@welshman/lib@0.8.12)(nostr-tools@2.20.0(typescript@5.9.3)))(ws@8.18.3)
|
||||||
'@welshman/util': 0.8.8(@noble/curves@1.9.7)(@welshman/lib@0.8.8)(nostr-tools@2.20.0(typescript@5.9.3))
|
'@welshman/util': 0.8.12(@noble/curves@1.9.7)(@welshman/lib@0.8.12)(nostr-tools@2.20.0(typescript@5.9.3))
|
||||||
nostr-signer-capacitor-plugin: https://codeload.github.com/coracle-social/nostr-signer-capacitor-plugin/tar.gz/be4bb90a1a15c8eec0934f2f66ce9e82ecc72d51(@capacitor/core@8.0.1)
|
nostr-signer-capacitor-plugin: https://codeload.github.com/coracle-social/nostr-signer-capacitor-plugin/tar.gz/be4bb90a1a15c8eec0934f2f66ce9e82ecc72d51(@capacitor/core@8.0.1)
|
||||||
nostr-tools: 2.20.0(typescript@5.9.3)
|
nostr-tools: 2.20.0(typescript@5.9.3)
|
||||||
|
|
||||||
'@welshman/store@0.8.8(@welshman/lib@0.8.8)(@welshman/net@0.8.8(@welshman/lib@0.8.8)(@welshman/util@0.8.8(@noble/curves@1.9.7)(@welshman/lib@0.8.8)(nostr-tools@2.20.0(typescript@5.9.3)))(ws@8.18.3))(@welshman/util@0.8.8(@noble/curves@1.9.7)(@welshman/lib@0.8.8)(nostr-tools@2.20.0(typescript@5.9.3)))(svelte@5.48.0)':
|
'@welshman/store@0.8.12(@welshman/lib@0.8.12)(@welshman/net@0.8.12(@welshman/lib@0.8.12)(@welshman/util@0.8.12(@noble/curves@1.9.7)(@welshman/lib@0.8.12)(nostr-tools@2.20.0(typescript@5.9.3)))(ws@8.18.3))(@welshman/util@0.8.12(@noble/curves@1.9.7)(@welshman/lib@0.8.12)(nostr-tools@2.20.0(typescript@5.9.3)))(svelte@5.48.0)':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@welshman/lib': 0.8.8
|
'@welshman/lib': 0.8.12
|
||||||
'@welshman/net': 0.8.8(@welshman/lib@0.8.8)(@welshman/util@0.8.8(@noble/curves@1.9.7)(@welshman/lib@0.8.8)(nostr-tools@2.20.0(typescript@5.9.3)))(ws@8.18.3)
|
'@welshman/net': 0.8.12(@welshman/lib@0.8.12)(@welshman/util@0.8.12(@noble/curves@1.9.7)(@welshman/lib@0.8.12)(nostr-tools@2.20.0(typescript@5.9.3)))(ws@8.18.3)
|
||||||
'@welshman/util': 0.8.8(@noble/curves@1.9.7)(@welshman/lib@0.8.8)(nostr-tools@2.20.0(typescript@5.9.3))
|
'@welshman/util': 0.8.12(@noble/curves@1.9.7)(@welshman/lib@0.8.12)(nostr-tools@2.20.0(typescript@5.9.3))
|
||||||
svelte: 5.48.0
|
svelte: 5.48.0
|
||||||
|
|
||||||
'@welshman/util@0.8.8(@noble/curves@1.9.7)(@welshman/lib@0.8.8)(nostr-tools@2.20.0(typescript@5.9.3))':
|
'@welshman/util@0.8.12(@noble/curves@1.9.7)(@welshman/lib@0.8.12)(nostr-tools@2.20.0(typescript@5.9.3))':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@noble/curves': 1.9.7
|
'@noble/curves': 1.9.7
|
||||||
'@types/ws': 8.18.1
|
'@types/ws': 8.18.1
|
||||||
'@welshman/lib': 0.8.8
|
'@welshman/lib': 0.8.12
|
||||||
js-base64: 3.7.8
|
js-base64: 3.7.8
|
||||||
nostr-tools: 2.20.0(typescript@5.9.3)
|
nostr-tools: 2.20.0(typescript@5.9.3)
|
||||||
nostr-wasm: 0.1.0
|
nostr-wasm: 0.1.0
|
||||||
@@ -8530,6 +8666,8 @@ snapshots:
|
|||||||
|
|
||||||
jiti@1.21.7: {}
|
jiti@1.21.7: {}
|
||||||
|
|
||||||
|
jose@6.2.1: {}
|
||||||
|
|
||||||
js-base64@3.7.8: {}
|
js-base64@3.7.8: {}
|
||||||
|
|
||||||
js-tokens@4.0.0: {}
|
js-tokens@4.0.0: {}
|
||||||
@@ -8605,6 +8743,19 @@ snapshots:
|
|||||||
|
|
||||||
linkifyjs@4.3.2: {}
|
linkifyjs@4.3.2: {}
|
||||||
|
|
||||||
|
livekit-client@2.17.3(@types/dom-mediacapture-record@1.0.22):
|
||||||
|
dependencies:
|
||||||
|
'@livekit/mutex': 1.1.1
|
||||||
|
'@livekit/protocol': 1.44.0
|
||||||
|
'@types/dom-mediacapture-record': 1.0.22
|
||||||
|
events: 3.3.0
|
||||||
|
jose: 6.2.1
|
||||||
|
loglevel: 1.9.2
|
||||||
|
sdp-transform: 2.15.0
|
||||||
|
tslib: 2.8.1
|
||||||
|
typed-emitter: 2.1.0
|
||||||
|
webrtc-adapter: 9.0.4
|
||||||
|
|
||||||
load-json-file@4.0.0:
|
load-json-file@4.0.0:
|
||||||
dependencies:
|
dependencies:
|
||||||
graceful-fs: 4.2.11
|
graceful-fs: 4.2.11
|
||||||
@@ -8637,6 +8788,8 @@ snapshots:
|
|||||||
|
|
||||||
lodash@4.17.23: {}
|
lodash@4.17.23: {}
|
||||||
|
|
||||||
|
loglevel@1.9.2: {}
|
||||||
|
|
||||||
lru-cache@10.4.3: {}
|
lru-cache@10.4.3: {}
|
||||||
|
|
||||||
lru-cache@11.2.4: {}
|
lru-cache@11.2.4: {}
|
||||||
@@ -9427,6 +9580,11 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
queue-microtask: 1.2.3
|
queue-microtask: 1.2.3
|
||||||
|
|
||||||
|
rxjs@7.8.2:
|
||||||
|
dependencies:
|
||||||
|
tslib: 2.8.1
|
||||||
|
optional: true
|
||||||
|
|
||||||
sade@1.8.1:
|
sade@1.8.1:
|
||||||
dependencies:
|
dependencies:
|
||||||
mri: 1.2.0
|
mri: 1.2.0
|
||||||
@@ -9458,6 +9616,10 @@ snapshots:
|
|||||||
|
|
||||||
sax@1.4.4: {}
|
sax@1.4.4: {}
|
||||||
|
|
||||||
|
sdp-transform@2.15.0: {}
|
||||||
|
|
||||||
|
sdp@3.2.1: {}
|
||||||
|
|
||||||
semver@5.7.2: {}
|
semver@5.7.2: {}
|
||||||
|
|
||||||
semver@6.3.1: {}
|
semver@6.3.1: {}
|
||||||
@@ -9982,6 +10144,10 @@ snapshots:
|
|||||||
possible-typed-array-names: 1.1.0
|
possible-typed-array-names: 1.1.0
|
||||||
reflect.getprototypeof: 1.0.10
|
reflect.getprototypeof: 1.0.10
|
||||||
|
|
||||||
|
typed-emitter@2.1.0:
|
||||||
|
optionalDependencies:
|
||||||
|
rxjs: 7.8.2
|
||||||
|
|
||||||
typescript-eslint@8.53.1(eslint@9.39.2(jiti@1.21.7))(typescript@5.9.3):
|
typescript-eslint@8.53.1(eslint@9.39.2(jiti@1.21.7))(typescript@5.9.3):
|
||||||
dependencies:
|
dependencies:
|
||||||
'@typescript-eslint/eslint-plugin': 8.53.1(@typescript-eslint/parser@8.53.1(eslint@9.39.2(jiti@1.21.7))(typescript@5.9.3))(eslint@9.39.2(jiti@1.21.7))(typescript@5.9.3)
|
'@typescript-eslint/eslint-plugin': 8.53.1(@typescript-eslint/parser@8.53.1(eslint@9.39.2(jiti@1.21.7))(typescript@5.9.3))(eslint@9.39.2(jiti@1.21.7))(typescript@5.9.3)
|
||||||
@@ -10090,6 +10256,10 @@ snapshots:
|
|||||||
|
|
||||||
webidl-conversions@4.0.2: {}
|
webidl-conversions@4.0.2: {}
|
||||||
|
|
||||||
|
webrtc-adapter@9.0.4:
|
||||||
|
dependencies:
|
||||||
|
sdp: 3.2.1
|
||||||
|
|
||||||
whatwg-url@5.0.0:
|
whatwg-url@5.0.0:
|
||||||
dependencies:
|
dependencies:
|
||||||
tr46: 0.0.3
|
tr46: 0.0.3
|
||||||
|
|||||||
@@ -422,6 +422,14 @@ body.keyboard-open .hide-on-keyboard {
|
|||||||
@apply cb cw fixed z-compose;
|
@apply cb cw fixed z-compose;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.chat__compose-zone {
|
||||||
|
@apply cb cw fixed z-compose;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chat__compose-zone .chat__compose-inner {
|
||||||
|
@apply min-w-0;
|
||||||
|
}
|
||||||
|
|
||||||
.chat__scroll-down {
|
.chat__scroll-down {
|
||||||
@apply pb-sai fixed bottom-28 right-4 z-feature md:bottom-16;
|
@apply pb-sai fixed bottom-28 right-4 z-feature md:bottom-16;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,12 +1,11 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import type {Snippet} from "svelte"
|
import type {Snippet} from "svelte"
|
||||||
import {page} from "$app/stores"
|
|
||||||
import {pubkey} from "@welshman/app"
|
import {pubkey} from "@welshman/app"
|
||||||
import Dialog from "@lib/components/Dialog.svelte"
|
import Dialog from "@lib/components/Dialog.svelte"
|
||||||
import Landing from "@app/components/Landing.svelte"
|
import Landing from "@app/components/Landing.svelte"
|
||||||
import Toast from "@app/components/Toast.svelte"
|
import Toast from "@app/components/Toast.svelte"
|
||||||
import PrimaryNav from "@app/components/PrimaryNav.svelte"
|
import PrimaryNav from "@app/components/PrimaryNav.svelte"
|
||||||
import {modals} from "@app/util/modal"
|
import {modal} from "@app/util/modal"
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
children: Snippet
|
children: Snippet
|
||||||
@@ -20,8 +19,8 @@
|
|||||||
<PrimaryNav>
|
<PrimaryNav>
|
||||||
{@render children?.()}
|
{@render children?.()}
|
||||||
</PrimaryNav>
|
</PrimaryNav>
|
||||||
{:else if !$modals[$page.url.hash.slice(1)]}
|
{:else if !$modal}
|
||||||
<Dialog children={{component: Landing, props: {}}} />
|
<Dialog noEscape children={{component: Landing, props: {}}} />
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
<Toast />
|
<Toast />
|
||||||
|
|||||||
+173
-125
@@ -1,10 +1,13 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import type {Snippet} from "svelte"
|
import type {Snippet} from "svelte"
|
||||||
import {onMount} from "svelte"
|
import {onMount} from "svelte"
|
||||||
|
import {goto} from "$app/navigation"
|
||||||
import {
|
import {
|
||||||
|
ago,
|
||||||
int,
|
int,
|
||||||
ms,
|
ms,
|
||||||
partition,
|
partition,
|
||||||
|
ifLet,
|
||||||
spec,
|
spec,
|
||||||
nthEq,
|
nthEq,
|
||||||
nthNe,
|
nthNe,
|
||||||
@@ -32,25 +35,27 @@
|
|||||||
messagingRelayListsByPubkey,
|
messagingRelayListsByPubkey,
|
||||||
} from "@welshman/app"
|
} from "@welshman/app"
|
||||||
import Danger from "@assets/icons/danger-triangle.svg?dataurl"
|
import Danger from "@assets/icons/danger-triangle.svg?dataurl"
|
||||||
|
import ArrowLeft from "@assets/icons/arrow-left.svg?dataurl"
|
||||||
import Icon from "@lib/components/Icon.svelte"
|
import Icon from "@lib/components/Icon.svelte"
|
||||||
import Link from "@lib/components/Link.svelte"
|
|
||||||
import Spinner from "@lib/components/Spinner.svelte"
|
import Spinner from "@lib/components/Spinner.svelte"
|
||||||
import PageBar from "@lib/components/PageBar.svelte"
|
import PageBar from "@lib/components/PageBar.svelte"
|
||||||
import PageContent from "@lib/components/PageContent.svelte"
|
import PageContent from "@lib/components/PageContent.svelte"
|
||||||
import Divider from "@lib/components/Divider.svelte"
|
import Divider from "@lib/components/Divider.svelte"
|
||||||
import Button from "@lib/components/Button.svelte"
|
import Button from "@lib/components/Button.svelte"
|
||||||
import ProfileName from "@app/components/ProfileName.svelte"
|
import ProfileName from "@app/components/ProfileName.svelte"
|
||||||
|
import ProfileLink from "@app/components/ProfileLink.svelte"
|
||||||
import ProfileCircle from "@app/components/ProfileCircle.svelte"
|
import ProfileCircle from "@app/components/ProfileCircle.svelte"
|
||||||
import ProfileCircles from "@app/components/ProfileCircles.svelte"
|
import ProfileCircles from "@app/components/ProfileCircles.svelte"
|
||||||
import ProfileDetail from "@app/components/ProfileDetail.svelte"
|
import ProfileDetail from "@app/components/ProfileDetail.svelte"
|
||||||
import ChatMembers from "@app/components/ChatMembers.svelte"
|
import ChatMembers from "@app/components/ChatMembers.svelte"
|
||||||
import ChatMessage from "@app/components/ChatMessage.svelte"
|
import ChatMessage from "@app/components/ChatMessage.svelte"
|
||||||
import ChatCompose from "@app/components/ChatCompose.svelte"
|
import ChatCompose from "@app/components/ChatCompose.svelte"
|
||||||
|
import ChatComposeEdit from "@app/components/ChatComposeEdit.svelte"
|
||||||
import ChatComposeParent from "@app/components/ChatComposeParent.svelte"
|
import ChatComposeParent from "@app/components/ChatComposeParent.svelte"
|
||||||
import ThunkToast from "@app/components/ThunkToast.svelte"
|
import ThunkToast from "@app/components/ThunkToast.svelte"
|
||||||
import {userSettingsValues, PLATFORM_NAME, deriveChat} from "@app/core/state"
|
import {userSettingsValues, deriveChat} from "@app/core/state"
|
||||||
import {pushModal} from "@app/util/modal"
|
import {pushModal} from "@app/util/modal"
|
||||||
import {prependParent} from "@app/core/commands"
|
import {makeDelete, prependParent} from "@app/core/commands"
|
||||||
import {pushToast} from "@app/util/toast"
|
import {pushToast} from "@app/util/toast"
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
@@ -62,13 +67,15 @@
|
|||||||
|
|
||||||
const chat = deriveChat(pubkeys)
|
const chat = deriveChat(pubkeys)
|
||||||
const others = remove($pubkey!, pubkeys)
|
const others = remove($pubkey!, pubkeys)
|
||||||
const missingRelayLists = $derived(pubkeys.filter(pk => !$messagingRelayListsByPubkey.has(pk)))
|
const missingRelayLists = $derived(others.filter(pk => !$messagingRelayListsByPubkey.has(pk)))
|
||||||
|
|
||||||
const showMembers = () =>
|
const showMembers = () =>
|
||||||
others.length === 1
|
others.length === 1
|
||||||
? pushModal(ProfileDetail, {pubkey: others[0]})
|
? pushModal(ProfileDetail, {pubkey: others[0]})
|
||||||
: pushModal(ChatMembers, {pubkeys: others})
|
: pushModal(ChatMembers, {pubkeys: others})
|
||||||
|
|
||||||
|
const back = () => goto("/chat")
|
||||||
|
|
||||||
const replyTo = (event: TrustedEvent) => {
|
const replyTo = (event: TrustedEvent) => {
|
||||||
parent = event
|
parent = event
|
||||||
compose?.focus()
|
compose?.focus()
|
||||||
@@ -78,73 +85,117 @@
|
|||||||
parent = undefined
|
parent = undefined
|
||||||
}
|
}
|
||||||
|
|
||||||
const onSubmit = async (params: EventContent) => {
|
const clearEventToEdit = () => {
|
||||||
const ptags = remove($pubkey!, pubkeys).map(tagPubkey)
|
eventToEdit = undefined
|
||||||
|
|
||||||
// Remove p tags since they result in forking the conversation
|
|
||||||
params.tags = params.tags.filter(nthNe(0, "p"))
|
|
||||||
|
|
||||||
// Add our reply quote to content
|
|
||||||
params = prependParent(parent, params)
|
|
||||||
|
|
||||||
const [imetaTags, tags] = partition(nthEq(0, "imeta"), params.tags)
|
|
||||||
const imetas = getTags("imeta", imetaTags).map(tagsFromIMeta)
|
|
||||||
const templates: EventTemplate[] = []
|
|
||||||
const buffer = []
|
|
||||||
|
|
||||||
const addTemplate = (kind: number, content: string, tags: string[][]) => {
|
|
||||||
content = content.trim()
|
|
||||||
|
|
||||||
if (content) {
|
|
||||||
templates.push(makeEvent(kind, {content, tags: [...tags, ...ptags]}))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
for (const p of parse(params)) {
|
|
||||||
const imeta = isLink(p)
|
|
||||||
? imetas.find(tags => tags.find(spec(["url", p.value.url.toString()])))
|
|
||||||
: undefined
|
|
||||||
|
|
||||||
if (isLink(p) && imeta) {
|
|
||||||
addTemplate(DIRECT_MESSAGE, buffer.splice(0).join(""), tags)
|
|
||||||
addTemplate(
|
|
||||||
DIRECT_MESSAGE_FILE,
|
|
||||||
p.value.url.toString(),
|
|
||||||
imeta.slice(1).filter(nthNe(0, "url")),
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
buffer.push(p.raw)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
addTemplate(DIRECT_MESSAGE, buffer.splice(0).join(""), tags)
|
|
||||||
|
|
||||||
// Split the message into multiple pieces so that we can use kind 15 to send images per nip 17
|
|
||||||
// Sleep 1 second between each one to make sure timestamps are distinct
|
|
||||||
const thunks = await Promise.all(
|
|
||||||
Array.from(enumerate(templates)).map(([i, event]) =>
|
|
||||||
sendWrapped({
|
|
||||||
event,
|
|
||||||
recipients: pubkeys,
|
|
||||||
delay: $userSettingsValues.send_delay + ms(i),
|
|
||||||
}),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
|
|
||||||
pushToast({
|
|
||||||
timeout: 30_000,
|
|
||||||
children: {
|
|
||||||
component: ThunkToast,
|
|
||||||
props: {thunk: mergeThunks(thunks)},
|
|
||||||
},
|
|
||||||
})
|
|
||||||
|
|
||||||
clearParent()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const onSubmit = async (params: EventContent) => {
|
||||||
|
try {
|
||||||
|
const ptags = remove($pubkey!, pubkeys).map(tagPubkey)
|
||||||
|
|
||||||
|
// Remove p tags since they result in forking the conversation
|
||||||
|
params.tags = params.tags.filter(nthNe(0, "p"))
|
||||||
|
|
||||||
|
// Add our reply quote to content
|
||||||
|
params = prependParent(parent, params)
|
||||||
|
|
||||||
|
if (eventToEdit) {
|
||||||
|
if (eventToEdit.content === params.content) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
await sendWrapped({
|
||||||
|
event: makeDelete({event: eventToEdit, protect: false}),
|
||||||
|
recipients: pubkeys,
|
||||||
|
pow: 16,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const [imetaTags, tags] = partition(nthEq(0, "imeta"), params.tags)
|
||||||
|
const imetas = getTags("imeta", imetaTags).map(tagsFromIMeta)
|
||||||
|
const templates: EventTemplate[] = []
|
||||||
|
const buffer = []
|
||||||
|
|
||||||
|
const addTemplate = (kind: number, content: string, tags: string[][]) => {
|
||||||
|
content = content.trim()
|
||||||
|
|
||||||
|
if (content) {
|
||||||
|
templates.push(
|
||||||
|
makeEvent(kind, {
|
||||||
|
content,
|
||||||
|
tags: [...tags, ...ptags],
|
||||||
|
created_at: eventToEdit?.created_at,
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const p of parse(params)) {
|
||||||
|
const imeta = isLink(p)
|
||||||
|
? imetas.find(tags => tags.find(spec(["url", p.value.url.toString()])))
|
||||||
|
: undefined
|
||||||
|
|
||||||
|
if (isLink(p) && imeta) {
|
||||||
|
addTemplate(DIRECT_MESSAGE, buffer.splice(0).join(""), tags)
|
||||||
|
addTemplate(
|
||||||
|
DIRECT_MESSAGE_FILE,
|
||||||
|
p.value.url.toString(),
|
||||||
|
imeta.slice(1).filter(nthNe(0, "url")),
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
buffer.push(p.raw)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
addTemplate(DIRECT_MESSAGE, buffer.splice(0).join(""), tags)
|
||||||
|
|
||||||
|
// Split the message into multiple pieces so that we can use kind 15 to send images per nip 17
|
||||||
|
// Sleep 1 second between each one to make sure timestamps are distinct
|
||||||
|
const thunks = await Promise.all(
|
||||||
|
Array.from(enumerate(templates)).map(([i, event]) =>
|
||||||
|
sendWrapped({
|
||||||
|
event,
|
||||||
|
recipients: pubkeys,
|
||||||
|
delay: $userSettingsValues.send_delay + ms(i),
|
||||||
|
pow: 16,
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
pushToast({
|
||||||
|
timeout: 30_000,
|
||||||
|
children: {
|
||||||
|
component: ThunkToast,
|
||||||
|
props: {thunk: mergeThunks(thunks)},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
} finally {
|
||||||
|
clearParent()
|
||||||
|
clearEventToEdit()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const onEscape = () => {
|
||||||
|
clearParent()
|
||||||
|
clearEventToEdit()
|
||||||
|
}
|
||||||
|
|
||||||
|
const canEditEvent = (event: TrustedEvent) =>
|
||||||
|
event.pubkey === $pubkey &&
|
||||||
|
event.kind === DIRECT_MESSAGE &&
|
||||||
|
event.created_at >= ago(500, MINUTE)
|
||||||
|
|
||||||
|
const onEditEvent = (event: TrustedEvent) => {
|
||||||
|
clearParent()
|
||||||
|
eventToEdit = event
|
||||||
|
}
|
||||||
|
|
||||||
|
const onEditPrevious = () => ifLet($chat?.messages.toReversed().find(canEditEvent), onEditEvent)
|
||||||
|
|
||||||
let loading = $state(true)
|
let loading = $state(true)
|
||||||
let compose: ChatCompose | undefined = $state()
|
let compose: ChatCompose | undefined = $state()
|
||||||
let parent: TrustedEvent | undefined = $state()
|
let parent: TrustedEvent | undefined = $state()
|
||||||
|
let eventToEdit: TrustedEvent | undefined = $state()
|
||||||
let chatCompose: HTMLElement | undefined = $state()
|
let chatCompose: HTMLElement | undefined = $state()
|
||||||
let dynamicPadding: HTMLElement | undefined = $state()
|
let dynamicPadding: HTMLElement | undefined = $state()
|
||||||
|
|
||||||
@@ -204,75 +255,59 @@
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<PageBar>
|
<PageBar>
|
||||||
{#snippet title()}
|
<div class="flex">
|
||||||
<Button class="flex flex-col gap-1 sm:flex-row sm:gap-2" onclick={showMembers}>
|
<Button onclick={back} class="place-self-start pr-3 md:hidden flex items-center">
|
||||||
{#if others.length === 0}
|
<Icon icon={ArrowLeft} size={7} />
|
||||||
<div class="row-2">
|
|
||||||
<ProfileCircle pubkey={$pubkey!} size={5} />
|
|
||||||
<ProfileName pubkey={$pubkey!} />
|
|
||||||
</div>
|
|
||||||
{:else if others.length === 1}
|
|
||||||
<div class="row-2">
|
|
||||||
<ProfileCircle pubkey={others[0]} size={5} />
|
|
||||||
<ProfileName pubkey={others[0]} />
|
|
||||||
</div>
|
|
||||||
{:else}
|
|
||||||
<div class="flex items-center gap-2">
|
|
||||||
<ProfileCircles pubkeys={others} size={5} />
|
|
||||||
<p class="overflow-hidden text-ellipsis whitespace-nowrap">
|
|
||||||
<ProfileName pubkey={others[0]} />
|
|
||||||
and
|
|
||||||
{#if others.length === 2}
|
|
||||||
<ProfileName pubkey={others[1]} />
|
|
||||||
{:else}
|
|
||||||
{others.length - 1}
|
|
||||||
{others.length > 2 ? "others" : "other"}
|
|
||||||
{/if}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
{/if}
|
|
||||||
</Button>
|
</Button>
|
||||||
{/snippet}
|
<div class="flex items-center justify-between gap-4">
|
||||||
{#snippet action()}
|
<div class="ellipsize flex items-center gap-4 whitespace-nowrap">
|
||||||
{#if remove($pubkey, missingRelayLists).length > 0}
|
<Button class="flex flex-col gap-1 sm:flex-row sm:gap-2" onclick={showMembers}>
|
||||||
{@const count = remove($pubkey, missingRelayLists).length}
|
{#if others.length === 0}
|
||||||
{@const label = count > 1 ? "lists are" : "list is"}
|
<div class="row-2">
|
||||||
<div
|
<ProfileCircle pubkey={$pubkey!} size={5} />
|
||||||
class="row-2 badge badge-error badge-lg tooltip tooltip-left cursor-pointer"
|
<ProfileName pubkey={$pubkey!} />
|
||||||
data-tip="{count} messaging {label} not configured.">
|
</div>
|
||||||
<Icon icon={Danger} />
|
{:else if others.length === 1}
|
||||||
{count}
|
<div class="row-2">
|
||||||
|
<ProfileCircle pubkey={others[0]} size={5} />
|
||||||
|
<ProfileName pubkey={others[0]} />
|
||||||
|
</div>
|
||||||
|
{:else}
|
||||||
|
<div class="flex items-center gap-2">
|
||||||
|
<ProfileCircles pubkeys={others} size={5} />
|
||||||
|
<p class="overflow-hidden text-ellipsis whitespace-nowrap">
|
||||||
|
<ProfileName pubkey={others[0]} />
|
||||||
|
and
|
||||||
|
{#if others.length === 2}
|
||||||
|
<ProfileName pubkey={others[1]} />
|
||||||
|
{:else}
|
||||||
|
{others.length - 1}
|
||||||
|
{others.length > 2 ? "others" : "other"}
|
||||||
|
{/if}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
</div>
|
||||||
{/snippet}
|
</div>
|
||||||
</PageBar>
|
</PageBar>
|
||||||
|
|
||||||
<PageContent class="flex flex-col-reverse gap-2 pt-4">
|
<PageContent class="flex flex-col-reverse gap-2 pt-4">
|
||||||
<div bind:this={dynamicPadding}></div>
|
<div bind:this={dynamicPadding}></div>
|
||||||
{#if missingRelayLists.includes($pubkey!)}
|
{#if missingRelayLists.length > 0}
|
||||||
<div class="py-12">
|
<div class="py-12">
|
||||||
<div class="card2 col-2 m-auto max-w-md items-center text-center">
|
<div class="card2 col-2 m-auto max-w-md items-center text-center">
|
||||||
<p class="row-2 text-lg text-error">
|
<p class="row-2 text-lg text-error">
|
||||||
<Icon icon={Danger} />
|
<Icon icon={Danger} />
|
||||||
Your messaging relays are not configured.
|
Direct messages are not enabled
|
||||||
</p>
|
</p>
|
||||||
<p>
|
<p>
|
||||||
In order to deliver messages, {PLATFORM_NAME} needs to know where to send them. Please visit
|
Ask
|
||||||
your <Link class="link" href="/settings/relays">relay settings page</Link> to receive messages.
|
{#each missingRelayLists as pubkey (pubkey)}
|
||||||
</p>
|
<ProfileLink {pubkey} />
|
||||||
</div>
|
{/each}
|
||||||
</div>
|
to enable direct messaging by opening this conversation in their app.
|
||||||
{:else if missingRelayLists.length > 0}
|
|
||||||
<div class="py-12">
|
|
||||||
<div class="card2 col-2 m-auto max-w-md items-center text-center">
|
|
||||||
<p class="row-2 text-lg text-error">
|
|
||||||
<Icon icon={Danger} />
|
|
||||||
{missingRelayLists.length} messaging
|
|
||||||
{missingRelayLists.length > 1 ? "lists are" : "list is"} not configured.
|
|
||||||
</p>
|
|
||||||
<p>
|
|
||||||
In order to deliver messages, {PLATFORM_NAME} needs to know where to send them. Please make
|
|
||||||
sure everyone in this conversation has set up their messaging relays.
|
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -285,7 +320,9 @@
|
|||||||
event={$state.snapshot(value as TrustedEvent)}
|
event={$state.snapshot(value as TrustedEvent)}
|
||||||
{pubkeys}
|
{pubkeys}
|
||||||
{showPubkey}
|
{showPubkey}
|
||||||
{replyTo} />
|
{replyTo}
|
||||||
|
canEdit={canEditEvent}
|
||||||
|
onEdit={onEditEvent} />
|
||||||
{/if}
|
{/if}
|
||||||
{/each}
|
{/each}
|
||||||
<p class="m-auto flex h-10 max-w-sm flex-col items-center justify-center gap-4 py-20 text-center">
|
<p class="m-auto flex h-10 max-w-sm flex-col items-center justify-center gap-4 py-20 text-center">
|
||||||
@@ -305,6 +342,17 @@
|
|||||||
{#if parent}
|
{#if parent}
|
||||||
<ChatComposeParent event={parent} clear={clearParent} verb="Replying to" />
|
<ChatComposeParent event={parent} clear={clearParent} verb="Replying to" />
|
||||||
{/if}
|
{/if}
|
||||||
|
{#if eventToEdit}
|
||||||
|
<ChatComposeEdit clear={clearEventToEdit} />
|
||||||
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
<ChatCompose bind:this={compose} {onSubmit} />
|
{#key eventToEdit}
|
||||||
|
<ChatCompose
|
||||||
|
bind:this={compose}
|
||||||
|
{onSubmit}
|
||||||
|
{onEscape}
|
||||||
|
{onEditPrevious}
|
||||||
|
content={eventToEdit?.content}
|
||||||
|
disabled={Boolean(missingRelayLists.length)} />
|
||||||
|
{/key}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
|
import {onDestroy, onMount} from "svelte"
|
||||||
import {writable} from "svelte/store"
|
import {writable} from "svelte/store"
|
||||||
|
import cx from "classnames"
|
||||||
import type {EventContent} from "@welshman/util"
|
import type {EventContent} from "@welshman/util"
|
||||||
import {isMobile, preventDefault} from "@lib/html"
|
import {isMobile, preventDefault} from "@lib/html"
|
||||||
import GallerySend from "@assets/icons/gallery-send.svg?dataurl"
|
import GallerySend from "@assets/icons/gallery-send.svg?dataurl"
|
||||||
@@ -10,21 +12,44 @@
|
|||||||
import {makeEditor} from "@app/editor"
|
import {makeEditor} from "@app/editor"
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
|
content?: string
|
||||||
|
disabled?: boolean
|
||||||
|
onEscape?: () => void
|
||||||
|
onEditPrevious?: () => void
|
||||||
onSubmit: (event: EventContent) => void
|
onSubmit: (event: EventContent) => void
|
||||||
}
|
}
|
||||||
|
|
||||||
const {onSubmit}: Props = $props()
|
const {content, disabled = false, onEscape, onEditPrevious, onSubmit}: Props = $props()
|
||||||
|
|
||||||
const autofocus = !isMobile
|
const autofocus = !isMobile && !disabled
|
||||||
|
|
||||||
const uploading = writable(false)
|
const uploading = writable(false)
|
||||||
|
|
||||||
|
const editorClass = $derived(
|
||||||
|
cx("chat-editor flex-grow overflow-hidden", {
|
||||||
|
"pointer-events-none opacity-50": disabled,
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
|
||||||
export const focus = () => editor.then(ed => ed.chain().focus().run())
|
export const focus = () => editor.then(ed => ed.chain().focus().run())
|
||||||
|
|
||||||
|
export const canEnterEditPrevious = () =>
|
||||||
|
editor.then(ed => ed.getText({blockSeparator: "\n"}) === "")
|
||||||
|
|
||||||
|
const handleKeyDown = async (event: KeyboardEvent) => {
|
||||||
|
if (event.key === "Escape") {
|
||||||
|
onEscape?.()
|
||||||
|
}
|
||||||
|
|
||||||
|
if (event.key === "ArrowUp" && (await canEnterEditPrevious())) {
|
||||||
|
onEditPrevious?.()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const uploadFiles = () => editor.then(ed => ed.chain().selectFiles().run())
|
const uploadFiles = () => editor.then(ed => ed.chain().selectFiles().run())
|
||||||
|
|
||||||
const submit = async () => {
|
const submit = async () => {
|
||||||
if ($uploading) return
|
if ($uploading || disabled) return
|
||||||
|
|
||||||
const ed = await editor
|
const ed = await editor
|
||||||
const content = ed.getText({blockSeparator: "\n"}).trim()
|
const content = ed.getText({blockSeparator: "\n"}).trim()
|
||||||
@@ -38,19 +63,30 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
const editor = makeEditor({
|
const editor = makeEditor({
|
||||||
|
content,
|
||||||
autofocus,
|
autofocus,
|
||||||
submit,
|
submit,
|
||||||
uploading,
|
uploading,
|
||||||
aggressive: true,
|
aggressive: true,
|
||||||
encryptFiles: true,
|
encryptFiles: true,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
onMount(async () => {
|
||||||
|
const ed = await editor
|
||||||
|
ed.view.dom.addEventListener("keydown", handleKeyDown)
|
||||||
|
})
|
||||||
|
|
||||||
|
onDestroy(async () => {
|
||||||
|
const ed = await editor
|
||||||
|
ed?.view?.dom.removeEventListener("keydown", handleKeyDown)
|
||||||
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<form class="relative z-feature flex gap-2 p-2" onsubmit={preventDefault(submit)}>
|
<form class="relative z-feature flex gap-2 p-2" onsubmit={preventDefault(submit)}>
|
||||||
<Button
|
<Button
|
||||||
data-tip="Add an image"
|
data-tip="Add an image"
|
||||||
class="center tooltip tooltip-right h-10 w-10 min-w-10 rounded-box bg-base-300 transition-colors hover:bg-base-200"
|
class="center tooltip tooltip-right h-10 w-10 min-w-10 rounded-box bg-base-300 transition-colors hover:bg-base-200"
|
||||||
disabled={$uploading}
|
disabled={$uploading || disabled}
|
||||||
onclick={uploadFiles}>
|
onclick={uploadFiles}>
|
||||||
{#if $uploading}
|
{#if $uploading}
|
||||||
<span class="loading loading-spinner loading-xs"></span>
|
<span class="loading loading-spinner loading-xs"></span>
|
||||||
@@ -58,13 +94,13 @@
|
|||||||
<Icon icon={GallerySend} />
|
<Icon icon={GallerySend} />
|
||||||
{/if}
|
{/if}
|
||||||
</Button>
|
</Button>
|
||||||
<div class="chat-editor flex-grow overflow-hidden">
|
<div class={editorClass} aria-disabled={disabled}>
|
||||||
<EditorContent {editor} />
|
<EditorContent {editor} />
|
||||||
</div>
|
</div>
|
||||||
<Button
|
<Button
|
||||||
data-tip="{window.navigator.platform.includes('Mac') ? 'cmd' : 'ctrl'}+enter to send"
|
data-tip="{window.navigator.platform.includes('Mac') ? 'cmd' : 'ctrl'}+enter to send"
|
||||||
class="center tooltip tooltip-left absolute right-4 h-10 w-10 min-w-10 rounded-full"
|
class="center tooltip tooltip-left absolute right-4 h-10 w-10 min-w-10 rounded-full"
|
||||||
disabled={$uploading}
|
disabled={$uploading || disabled}
|
||||||
onclick={submit}>
|
onclick={submit}>
|
||||||
<Icon icon={Plane} />
|
<Icon icon={Plane} />
|
||||||
</Button>
|
</Button>
|
||||||
|
|||||||
@@ -0,0 +1,21 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import {slide} from "@lib/transition"
|
||||||
|
import CloseCircle from "@assets/icons/close-circle.svg?dataurl"
|
||||||
|
import Icon from "@lib/components/Icon.svelte"
|
||||||
|
import Button from "@lib/components/Button.svelte"
|
||||||
|
|
||||||
|
const {
|
||||||
|
clear,
|
||||||
|
}: {
|
||||||
|
clear: () => void
|
||||||
|
} = $props()
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div
|
||||||
|
class="relative flex h-8 items-center justify-between border-l-2 border-solid border-primary bg-base-300 px-2 pr-7 text-xs"
|
||||||
|
transition:slide>
|
||||||
|
<p class="text-primary">Editing message</p>
|
||||||
|
<Button onclick={clear} class="flex items-center">
|
||||||
|
<Icon icon={CloseCircle} />
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
@@ -0,0 +1,72 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import {getRelaysFromList} from "@welshman/util"
|
||||||
|
import {waitForThunkError, setMessagingRelays, userRelayList, setRelays} from "@welshman/app"
|
||||||
|
import {preventDefault} from "@lib/html"
|
||||||
|
import AltArrowLeft from "@assets/icons/alt-arrow-left.svg?dataurl"
|
||||||
|
import AltArrowRight from "@assets/icons/alt-arrow-right.svg?dataurl"
|
||||||
|
import Icon from "@lib/components/Icon.svelte"
|
||||||
|
import Button from "@lib/components/Button.svelte"
|
||||||
|
import Spinner from "@lib/components/Spinner.svelte"
|
||||||
|
import Modal from "@lib/components/Modal.svelte"
|
||||||
|
import ModalBody from "@lib/components/ModalBody.svelte"
|
||||||
|
import ModalHeader from "@lib/components/ModalHeader.svelte"
|
||||||
|
import ModalTitle from "@lib/components/ModalTitle.svelte"
|
||||||
|
import ModalFooter from "@lib/components/ModalFooter.svelte"
|
||||||
|
import {DEFAULT_RELAYS, DEFAULT_MESSAGING_RELAYS} from "@app/core/state"
|
||||||
|
import {pushToast} from "@app/util/toast"
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
next: () => void
|
||||||
|
}
|
||||||
|
|
||||||
|
const {next}: Props = $props()
|
||||||
|
|
||||||
|
let loading = $state(false)
|
||||||
|
|
||||||
|
const back = () => history.back()
|
||||||
|
|
||||||
|
const enable = async () => {
|
||||||
|
loading = true
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (getRelaysFromList($userRelayList).length === 0) {
|
||||||
|
const error = await waitForThunkError(await setRelays(DEFAULT_RELAYS.map(r => ["r", r])))
|
||||||
|
|
||||||
|
if (error) {
|
||||||
|
pushToast({theme: "error", message: error})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const error = await waitForThunkError(await setMessagingRelays(DEFAULT_MESSAGING_RELAYS))
|
||||||
|
|
||||||
|
if (error) {
|
||||||
|
pushToast({theme: "error", message: error})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
await next()
|
||||||
|
} finally {
|
||||||
|
loading = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<Modal tag="form" onsubmit={preventDefault(enable)}>
|
||||||
|
<ModalBody>
|
||||||
|
<ModalHeader>
|
||||||
|
<ModalTitle>Enable direct messaging?</ModalTitle>
|
||||||
|
</ModalHeader>
|
||||||
|
<p>Direct messaging isn't currently enabled. Would you like to turn it on?</p>
|
||||||
|
</ModalBody>
|
||||||
|
<ModalFooter>
|
||||||
|
<Button class="btn btn-link" onclick={back}>
|
||||||
|
<Icon icon={AltArrowLeft} />
|
||||||
|
Go back
|
||||||
|
</Button>
|
||||||
|
<Button type="submit" class="btn btn-primary" disabled={loading}>
|
||||||
|
<Spinner {loading}>Enable direct messaging</Spinner>
|
||||||
|
<Icon icon={AltArrowRight} />
|
||||||
|
</Button>
|
||||||
|
</ModalFooter>
|
||||||
|
</Modal>
|
||||||
@@ -5,11 +5,11 @@
|
|||||||
import type {TrustedEvent} from "@welshman/util"
|
import type {TrustedEvent} from "@welshman/util"
|
||||||
import {pubkey, loadMessagingRelayList} from "@welshman/app"
|
import {pubkey, loadMessagingRelayList} from "@welshman/app"
|
||||||
import {fade} from "@lib/transition"
|
import {fade} from "@lib/transition"
|
||||||
import Link from "@lib/components/Link.svelte"
|
import Button from "@lib/components/Button.svelte"
|
||||||
import ProfileName from "@app/components/ProfileName.svelte"
|
import ProfileName from "@app/components/ProfileName.svelte"
|
||||||
import ProfileCircle from "@app/components/ProfileCircle.svelte"
|
import ProfileCircle from "@app/components/ProfileCircle.svelte"
|
||||||
import ProfileCircles from "@app/components/ProfileCircles.svelte"
|
import ProfileCircles from "@app/components/ProfileCircles.svelte"
|
||||||
import {makeChatPath} from "@app/util/routes"
|
import {makeChatPath, goToChat} from "@app/util/routes"
|
||||||
import {notifications} from "@app/util/notifications"
|
import {notifications} from "@app/util/notifications"
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
@@ -24,6 +24,7 @@
|
|||||||
const others = uniq(remove($pubkey!, props.pubkeys))
|
const others = uniq(remove($pubkey!, props.pubkeys))
|
||||||
const active = $derived($page.params.chat === props.id)
|
const active = $derived($page.params.chat === props.id)
|
||||||
const path = makeChatPath(props.pubkeys)
|
const path = makeChatPath(props.pubkeys)
|
||||||
|
const openChat = () => goToChat(props.pubkeys)
|
||||||
|
|
||||||
onMount(() => {
|
onMount(() => {
|
||||||
for (const pk of others) {
|
for (const pk of others) {
|
||||||
@@ -32,7 +33,7 @@
|
|||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<Link class="flex flex-col justify-start gap-1" href={makeChatPath(props.pubkeys)}>
|
<Button class="flex flex-col justify-start gap-1 w-full" onclick={openChat}>
|
||||||
<div
|
<div
|
||||||
class="cursor-pointer border-t border-solid border-base-100 px-6 py-2 transition-colors hover:bg-base-100 {props.class}"
|
class="cursor-pointer border-t border-solid border-base-100 px-6 py-2 transition-colors hover:bg-base-100 {props.class}"
|
||||||
class:bg-base-100={active}>
|
class:bg-base-100={active}>
|
||||||
@@ -71,4 +72,4 @@
|
|||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</Link>
|
</Button>
|
||||||
|
|||||||
@@ -23,11 +23,13 @@
|
|||||||
interface Props {
|
interface Props {
|
||||||
event: TrustedEvent
|
event: TrustedEvent
|
||||||
replyTo: (event: TrustedEvent) => void
|
replyTo: (event: TrustedEvent) => void
|
||||||
|
canEdit?: (event: TrustedEvent) => boolean
|
||||||
|
onEdit?: (event: TrustedEvent) => void
|
||||||
pubkeys: string[]
|
pubkeys: string[]
|
||||||
showPubkey?: boolean
|
showPubkey?: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
const {event, replyTo, pubkeys, showPubkey = false}: Props = $props()
|
const {event, replyTo, canEdit, onEdit, pubkeys, showPubkey = false}: Props = $props()
|
||||||
|
|
||||||
const isOwn = event.pubkey === $pubkey
|
const isOwn = event.pubkey === $pubkey
|
||||||
const profileDisplay = deriveProfileDisplay(event.pubkey)
|
const profileDisplay = deriveProfileDisplay(event.pubkey)
|
||||||
@@ -35,16 +37,21 @@
|
|||||||
const [_, colorValue] = colors[hash(event.pubkey) % colors.length]
|
const [_, colorValue] = colors[hash(event.pubkey) % colors.length]
|
||||||
|
|
||||||
const reply = () => replyTo(event)
|
const reply = () => replyTo(event)
|
||||||
|
const edit = canEdit?.(event) ? () => onEdit?.(event) : undefined
|
||||||
|
|
||||||
const deleteReaction = (event: TrustedEvent) =>
|
const deleteReaction = (event: TrustedEvent) =>
|
||||||
sendWrapped({event: makeDelete({event, protect: false}), recipients: pubkeys})
|
sendWrapped({event: makeDelete({event, protect: false}), recipients: pubkeys, pow: 16})
|
||||||
|
|
||||||
const createReaction = (template: EventContent) =>
|
const createReaction = (template: EventContent) =>
|
||||||
sendWrapped({event: makeReaction({event, protect: false, ...template}), recipients: pubkeys})
|
sendWrapped({
|
||||||
|
event: makeReaction({event, protect: false, ...template}),
|
||||||
|
recipients: pubkeys,
|
||||||
|
pow: 16,
|
||||||
|
})
|
||||||
|
|
||||||
const openProfile = () => pushModal(ProfileDetail, {pubkey: event.pubkey})
|
const openProfile = () => pushModal(ProfileDetail, {pubkey: event.pubkey})
|
||||||
|
|
||||||
const showMobileMenu = () => pushModal(ChatMessageMenuMobile, {event, pubkeys, reply})
|
const showMobileMenu = () => pushModal(ChatMessageMenuMobile, {event, pubkeys, reply, edit})
|
||||||
|
|
||||||
const togglePopover = () => {
|
const togglePopover = () => {
|
||||||
if (popoverIsVisible) {
|
if (popoverIsVisible) {
|
||||||
@@ -71,7 +78,7 @@
|
|||||||
<Tippy
|
<Tippy
|
||||||
bind:popover
|
bind:popover
|
||||||
component={ChatMessageMenu}
|
component={ChatMessageMenu}
|
||||||
props={{event, pubkeys, popover, replyTo}}
|
props={{event, pubkeys, popover, replyTo, edit}}
|
||||||
params={{
|
params={{
|
||||||
interactive: true,
|
interactive: true,
|
||||||
trigger: "manual",
|
trigger: "manual",
|
||||||
@@ -93,7 +100,7 @@
|
|||||||
{/if}
|
{/if}
|
||||||
<div class="flex min-w-0 flex-col" class:items-end={isOwn}>
|
<div class="flex min-w-0 flex-col" class:items-end={isOwn}>
|
||||||
<TapTarget
|
<TapTarget
|
||||||
class="bg-alt chat-bubble mx-1 mb-2 flex cursor-auto flex-col gap-1 text-left lg:max-w-2xl"
|
class="bg-alt chat-bubble mx-1 mb-2 flex cursor-auto flex-col gap-1 text-left lg:max-w-2xl min-w-[100px]"
|
||||||
onTap={showMobileMenu}>
|
onTap={showMobileMenu}>
|
||||||
{#if showPubkey}
|
{#if showPubkey}
|
||||||
<div class="flex items-center gap-2">
|
<div class="flex items-center gap-2">
|
||||||
|
|||||||
@@ -18,6 +18,7 @@
|
|||||||
sendWrapped({
|
sendWrapped({
|
||||||
event: makeReaction({event, content: emoji.unicode, protect: false}),
|
event: makeReaction({event, content: emoji.unicode, protect: false}),
|
||||||
recipients: pubkeys,
|
recipients: pubkeys,
|
||||||
|
pow: 16,
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|||||||
@@ -4,12 +4,14 @@
|
|||||||
import ChatMessageEmojiButton from "@app/components/ChatMessageEmojiButton.svelte"
|
import ChatMessageEmojiButton from "@app/components/ChatMessageEmojiButton.svelte"
|
||||||
import EventInfo from "@app/components/EventInfo.svelte"
|
import EventInfo from "@app/components/EventInfo.svelte"
|
||||||
import {pushModal} from "@app/util/modal"
|
import {pushModal} from "@app/util/modal"
|
||||||
|
import Pen from "@assets/icons/pen.svg?dataurl"
|
||||||
import Reply from "@assets/icons/reply-2.svg?dataurl"
|
import Reply from "@assets/icons/reply-2.svg?dataurl"
|
||||||
import Code2 from "@assets/icons/code-2.svg?dataurl"
|
import Code2 from "@assets/icons/code-2.svg?dataurl"
|
||||||
|
|
||||||
const {event, pubkeys, popover, replyTo} = $props()
|
const {event, pubkeys, popover, replyTo, edit} = $props()
|
||||||
|
|
||||||
const reply = () => replyTo(event)
|
const reply = () => replyTo(event)
|
||||||
|
const onEdit = () => edit?.()
|
||||||
|
|
||||||
const showInfo = () => {
|
const showInfo = () => {
|
||||||
popover.hide()
|
popover.hide()
|
||||||
@@ -24,6 +26,11 @@
|
|||||||
<Icon size={4} icon={Reply} />
|
<Icon size={4} icon={Reply} />
|
||||||
</Button>
|
</Button>
|
||||||
{/if}
|
{/if}
|
||||||
|
{#if edit}
|
||||||
|
<Button class="btn join-item btn-xs" onclick={onEdit}>
|
||||||
|
<Icon size={4} icon={Pen} />
|
||||||
|
</Button>
|
||||||
|
{/if}
|
||||||
<Button class="btn join-item btn-xs" onclick={showInfo}>
|
<Button class="btn join-item btn-xs" onclick={showInfo}>
|
||||||
<Icon size={4} icon={Code2} />
|
<Icon size={4} icon={Code2} />
|
||||||
</Button>
|
</Button>
|
||||||
|
|||||||
@@ -3,6 +3,7 @@
|
|||||||
import type {TrustedEvent} from "@welshman/util"
|
import type {TrustedEvent} from "@welshman/util"
|
||||||
import {sendWrapped} from "@welshman/app"
|
import {sendWrapped} from "@welshman/app"
|
||||||
import SmileCircle from "@assets/icons/smile-circle.svg?dataurl"
|
import SmileCircle from "@assets/icons/smile-circle.svg?dataurl"
|
||||||
|
import Pen from "@assets/icons/pen.svg?dataurl"
|
||||||
import Reply from "@assets/icons/reply-2.svg?dataurl"
|
import Reply from "@assets/icons/reply-2.svg?dataurl"
|
||||||
import Copy from "@assets/icons/copy.svg?dataurl"
|
import Copy from "@assets/icons/copy.svg?dataurl"
|
||||||
import Code2 from "@assets/icons/code-2.svg?dataurl"
|
import Code2 from "@assets/icons/code-2.svg?dataurl"
|
||||||
@@ -20,15 +21,17 @@
|
|||||||
pubkeys: string[]
|
pubkeys: string[]
|
||||||
event: TrustedEvent
|
event: TrustedEvent
|
||||||
reply: () => void
|
reply: () => void
|
||||||
|
edit?: () => void
|
||||||
}
|
}
|
||||||
|
|
||||||
const {event, pubkeys, reply}: Props = $props()
|
const {event, pubkeys, reply, edit}: Props = $props()
|
||||||
|
|
||||||
const onEmoji = ((event: TrustedEvent, pubkeys: string[], emoji: NativeEmoji) => {
|
const onEmoji = ((event: TrustedEvent, pubkeys: string[], emoji: NativeEmoji) => {
|
||||||
history.back()
|
history.back()
|
||||||
sendWrapped({
|
sendWrapped({
|
||||||
event: makeReaction({event, content: emoji.unicode, protect: false}),
|
event: makeReaction({event, content: emoji.unicode, protect: false}),
|
||||||
recipients: pubkeys,
|
recipients: pubkeys,
|
||||||
|
pow: 16,
|
||||||
})
|
})
|
||||||
}).bind(undefined, event, pubkeys)
|
}).bind(undefined, event, pubkeys)
|
||||||
|
|
||||||
@@ -39,6 +42,11 @@
|
|||||||
reply()
|
reply()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const sendEdit = () => {
|
||||||
|
history.back()
|
||||||
|
edit?.()
|
||||||
|
}
|
||||||
|
|
||||||
const copyText = () => {
|
const copyText = () => {
|
||||||
history.back()
|
history.back()
|
||||||
clip(event.content)
|
clip(event.content)
|
||||||
@@ -62,6 +70,12 @@
|
|||||||
<Icon size={4} icon={Reply} />
|
<Icon size={4} icon={Reply} />
|
||||||
Send Reply
|
Send Reply
|
||||||
</Button>
|
</Button>
|
||||||
|
{#if edit}
|
||||||
|
<Button class="btn btn-neutral w-full" onclick={sendEdit}>
|
||||||
|
<Icon size={4} icon={Pen} />
|
||||||
|
Edit Message
|
||||||
|
</Button>
|
||||||
|
{/if}
|
||||||
<Button class="btn btn-primary w-full" onclick={showEmojiPicker}>
|
<Button class="btn btn-primary w-full" onclick={showEmojiPicker}>
|
||||||
<Icon size={4} icon={SmileCircle} />
|
<Icon size={4} icon={SmileCircle} />
|
||||||
Send Reaction
|
Send Reaction
|
||||||
|
|||||||
@@ -2,7 +2,6 @@
|
|||||||
import * as nip19 from "nostr-tools/nip19"
|
import * as nip19 from "nostr-tools/nip19"
|
||||||
import {onMount} from "svelte"
|
import {onMount} from "svelte"
|
||||||
import {writable} from "svelte/store"
|
import {writable} from "svelte/store"
|
||||||
import {goto} from "$app/navigation"
|
|
||||||
import {tryCatch, uniq} from "@welshman/lib"
|
import {tryCatch, uniq} from "@welshman/lib"
|
||||||
import {fromNostrURI} from "@welshman/util"
|
import {fromNostrURI} from "@welshman/util"
|
||||||
import {loadMessagingRelayList} from "@welshman/app"
|
import {loadMessagingRelayList} from "@welshman/app"
|
||||||
@@ -19,11 +18,11 @@
|
|||||||
import ModalSubtitle from "@lib/components/ModalSubtitle.svelte"
|
import ModalSubtitle from "@lib/components/ModalSubtitle.svelte"
|
||||||
import ModalFooter from "@lib/components/ModalFooter.svelte"
|
import ModalFooter from "@lib/components/ModalFooter.svelte"
|
||||||
import ProfileMultiSelect from "@app/components/ProfileMultiSelect.svelte"
|
import ProfileMultiSelect from "@app/components/ProfileMultiSelect.svelte"
|
||||||
import {makeChatPath} from "@app/util/routes"
|
import {goToChat} from "@app/util/routes"
|
||||||
|
|
||||||
const back = () => history.back()
|
const back = () => history.back()
|
||||||
|
|
||||||
const onSubmit = () => goto(makeChatPath(pubkeys))
|
const onSubmit = () => goToChat(pubkeys)
|
||||||
|
|
||||||
const addPubkey = (pubkey: string) => {
|
const addPubkey = (pubkey: string) => {
|
||||||
pubkeys = uniq([...pubkeys, pubkey])
|
pubkeys = uniq([...pubkeys, pubkey])
|
||||||
|
|||||||
@@ -20,7 +20,7 @@
|
|||||||
|
|
||||||
const title = getTagValue("title", event.tags)
|
const title = getTagValue("title", event.tags)
|
||||||
const h = getTagValue("h", event.tags)
|
const h = getTagValue("h", event.tags)
|
||||||
const images = getTagValues("image", event.tags)
|
const images = new Set(getTagValues("image", event.tags))
|
||||||
const [_, price = 0, currency = "SAT"] = getTag("price", event.tags) || []
|
const [_, price = 0, currency = "SAT"] = getTag("price", event.tags) || []
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,17 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import Modal from "@lib/components/Modal.svelte"
|
||||||
|
import ModalBody from "@lib/components/ModalBody.svelte"
|
||||||
|
import IconPicker from "@app/components/IconPicker.svelte"
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
onSelect: (iconUrl: string) => void
|
||||||
|
}
|
||||||
|
|
||||||
|
const {onSelect}: Props = $props()
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<Modal>
|
||||||
|
<ModalBody>
|
||||||
|
<IconPicker {onSelect} />
|
||||||
|
</ModalBody>
|
||||||
|
</Modal>
|
||||||
@@ -82,7 +82,7 @@
|
|||||||
<p>Your recovery codes have been sent!</p>
|
<p>Your recovery codes have been sent!</p>
|
||||||
<p>
|
<p>
|
||||||
For security reasons, you may receive three or more emails with recovery codes in them. Please
|
For security reasons, you may receive three or more emails with recovery codes in them. Please
|
||||||
paste <strong>all</strong> recovery codes into the text box below, on separate lines.
|
paste <strong>all</strong> recovery codes into the text box below.
|
||||||
</p>
|
</p>
|
||||||
<StringMultiInput bind:value={otps} placeholder="Enter your recovery codes..." />
|
<StringMultiInput bind:value={otps} placeholder="Enter your recovery codes..." />
|
||||||
</ModalBody>
|
</ModalBody>
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
import {Capacitor} from "@capacitor/core"
|
import {Capacitor} from "@capacitor/core"
|
||||||
import {getNip07, getNip55, Nip55Signer} from "@welshman/signer"
|
import {getNip07, getNip55, Nip55Signer} from "@welshman/signer"
|
||||||
import {addSession, type Session, makeNip07Session, makeNip55Session} from "@welshman/app"
|
import {addSession, type Session, makeNip07Session, makeNip55Session} from "@welshman/app"
|
||||||
import Widget from "@assets/icons/widget-2.svg?dataurl"
|
import Widget from "@assets/icons/widget-4.svg?dataurl"
|
||||||
import Letter from "@assets/icons/letter.svg?dataurl"
|
import Letter from "@assets/icons/letter.svg?dataurl"
|
||||||
import Cpu from "@assets/icons/cpu-bolt.svg?dataurl"
|
import Cpu from "@assets/icons/cpu-bolt.svg?dataurl"
|
||||||
import Compass from "@assets/icons/compass-big.svg?dataurl"
|
import Compass from "@assets/icons/compass-big.svg?dataurl"
|
||||||
|
|||||||
@@ -77,6 +77,7 @@
|
|||||||
controller.stop()
|
controller.stop()
|
||||||
|
|
||||||
loginWithNip46(pubkey, clientSecret, signerPubkey, relays)
|
loginWithNip46(pubkey, clientSecret, signerPubkey, relays)
|
||||||
|
setChecked("*")
|
||||||
} else {
|
} else {
|
||||||
return pushToast({
|
return pushToast({
|
||||||
theme: "error",
|
theme: "error",
|
||||||
|
|||||||
@@ -86,7 +86,7 @@
|
|||||||
<p>Your login codes have been sent!</p>
|
<p>Your login codes have been sent!</p>
|
||||||
<p>
|
<p>
|
||||||
For security reasons, you may receive three or more emails with login codes in them. Please
|
For security reasons, you may receive three or more emails with login codes in them. Please
|
||||||
paste <strong>all</strong> login codes into the text box below, on separate lines.
|
paste <strong>all</strong> login codes into the text box below.
|
||||||
</p>
|
</p>
|
||||||
<StringMultiInput bind:value={otps} placeholder="Enter your login codes..." />
|
<StringMultiInput bind:value={otps} placeholder="Enter your login codes..." />
|
||||||
</ModalBody>
|
</ModalBody>
|
||||||
|
|||||||
@@ -1,12 +1,9 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import {Capacitor} from "@capacitor/core"
|
import {Capacitor} from "@capacitor/core"
|
||||||
import AltArrowLeft from "@assets/icons/alt-arrow-left.svg?dataurl"
|
import {pubkey} from "@welshman/app"
|
||||||
import UserRounded from "@assets/icons/user-rounded.svg?dataurl"
|
|
||||||
import Server from "@assets/icons/server.svg?dataurl"
|
import Server from "@assets/icons/server.svg?dataurl"
|
||||||
import Moon from "@assets/icons/moon.svg?dataurl"
|
import GalleryMinimalistic from "@assets/icons/gallery-minimalistic.svg?dataurl"
|
||||||
import Settings from "@assets/icons/settings-minimalistic.svg?dataurl"
|
import Shield from "@assets/icons/shield-minimalistic.svg?dataurl"
|
||||||
import Code2 from "@assets/icons/code-2.svg?dataurl"
|
|
||||||
import Exit from "@assets/icons/logout-3.svg?dataurl"
|
|
||||||
import Bell from "@assets/icons/bell.svg?dataurl"
|
import Bell from "@assets/icons/bell.svg?dataurl"
|
||||||
import Wallet from "@assets/icons/wallet.svg?dataurl"
|
import Wallet from "@assets/icons/wallet.svg?dataurl"
|
||||||
import Icon from "@lib/components/Icon.svelte"
|
import Icon from "@lib/components/Icon.svelte"
|
||||||
@@ -14,120 +11,69 @@
|
|||||||
import Button from "@lib/components/Button.svelte"
|
import Button from "@lib/components/Button.svelte"
|
||||||
import Modal from "@lib/components/Modal.svelte"
|
import Modal from "@lib/components/Modal.svelte"
|
||||||
import ModalBody from "@lib/components/ModalBody.svelte"
|
import ModalBody from "@lib/components/ModalBody.svelte"
|
||||||
import CardButton from "@lib/components/CardButton.svelte"
|
import Profile from "@app/components/Profile.svelte"
|
||||||
import LogOut from "@app/components/LogOut.svelte"
|
import LogOut from "@app/components/LogOut.svelte"
|
||||||
import {PLATFORM_NAME} from "@app/core/state"
|
|
||||||
import {pushModal} from "@app/util/modal"
|
import {pushModal} from "@app/util/modal"
|
||||||
import {theme} from "@app/util/theme"
|
import {theme} from "@app/util/theme"
|
||||||
|
|
||||||
const back = () => history.back()
|
|
||||||
const logout = () => pushModal(LogOut)
|
const logout = () => pushModal(LogOut)
|
||||||
const toggleTheme = () => theme.set($theme === "dark" ? "light" : "dark")
|
const toggleTheme = () => theme.set($theme === "dark" ? "light" : "dark")
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<Modal>
|
<Modal>
|
||||||
<ModalBody>
|
<ModalBody>
|
||||||
<div class="flex flex-col gap-2">
|
<div class="flex flex-col gap-8 items-center py-12 max-w-[16rem] m-auto w-full">
|
||||||
<Link replaceState href="/settings/profile">
|
{#if $pubkey}
|
||||||
<CardButton class="btn-neutral">
|
<Link replaceState href="/settings/profile">
|
||||||
{#snippet icon()}
|
<Profile inert pubkey={$pubkey} />
|
||||||
<div><Icon icon={UserRounded} size={7} /></div>
|
|
||||||
{/snippet}
|
|
||||||
{#snippet title()}
|
|
||||||
<div>Profile</div>
|
|
||||||
{/snippet}
|
|
||||||
{#snippet info()}
|
|
||||||
<div>Customize your user profile</div>
|
|
||||||
{/snippet}
|
|
||||||
</CardButton>
|
|
||||||
</Link>
|
|
||||||
<Link replaceState href="/settings/alerts">
|
|
||||||
<CardButton class="btn-neutral">
|
|
||||||
{#snippet icon()}
|
|
||||||
<div><Icon icon={Bell} size={7} /></div>
|
|
||||||
{/snippet}
|
|
||||||
{#snippet title()}
|
|
||||||
<div>Alerts</div>
|
|
||||||
{/snippet}
|
|
||||||
{#snippet info()}
|
|
||||||
<div>Set up email digests and push notifications</div>
|
|
||||||
{/snippet}
|
|
||||||
</CardButton>
|
|
||||||
</Link>
|
|
||||||
{#if Capacitor.getPlatform() !== "ios"}
|
|
||||||
<Link replaceState href="/settings/wallet">
|
|
||||||
<CardButton class="btn-neutral">
|
|
||||||
{#snippet icon()}
|
|
||||||
<div><Icon icon={Wallet} size={7} /></div>
|
|
||||||
{/snippet}
|
|
||||||
{#snippet title()}
|
|
||||||
<div>Wallet</div>
|
|
||||||
{/snippet}
|
|
||||||
{#snippet info()}
|
|
||||||
<div>Connect a bitcoin wallet for sending social tips</div>
|
|
||||||
{/snippet}
|
|
||||||
</CardButton>
|
|
||||||
</Link>
|
</Link>
|
||||||
{/if}
|
{/if}
|
||||||
<Link replaceState href="/settings/relays">
|
<div class="grid grid-cols-3 gap-3 w-full">
|
||||||
<CardButton class="btn-neutral">
|
<Link
|
||||||
{#snippet icon()}
|
replaceState
|
||||||
<div><Icon icon={Server} size={7} /></div>
|
href="/settings/alerts"
|
||||||
{/snippet}
|
class="aspect-square btn h-[unset] btn-neutral flex flex-col gap-2 text-center">
|
||||||
{#snippet title()}
|
<Icon icon={Bell} size={5} />
|
||||||
<div>Relays</div>
|
Alerts
|
||||||
{/snippet}
|
</Link>
|
||||||
{#snippet info()}
|
{#if Capacitor.getPlatform() !== "ios"}
|
||||||
<div>Control how {PLATFORM_NAME} talks to the network</div>
|
<Link
|
||||||
{/snippet}
|
replaceState
|
||||||
</CardButton>
|
href="/settings/wallet"
|
||||||
</Link>
|
class="aspect-square btn h-[unset] btn-neutral flex flex-col gap-2 text-center">
|
||||||
<Link replaceState href="/settings/content">
|
<Icon icon={Wallet} size={5} />
|
||||||
<CardButton class="btn-neutral">
|
Wallet
|
||||||
{#snippet icon()}
|
</Link>
|
||||||
<div><Icon icon={Settings} size={7} /></div>
|
{/if}
|
||||||
{/snippet}
|
<Link
|
||||||
{#snippet title()}
|
replaceState
|
||||||
<div>Settings</div>
|
href="/settings/relays"
|
||||||
{/snippet}
|
class="aspect-square btn h-[unset] btn-neutral flex flex-col gap-2 text-center">
|
||||||
{#snippet info()}
|
<Icon icon={Server} size={5} />
|
||||||
<div>Get into the details about how {PLATFORM_NAME} works</div>
|
Relays
|
||||||
{/snippet}
|
</Link>
|
||||||
</CardButton>
|
<Link
|
||||||
</Link>
|
replaceState
|
||||||
<Button onclick={toggleTheme}>
|
href="/settings/content"
|
||||||
<CardButton class="btn-neutral">
|
class="aspect-square btn h-[unset] btn-neutral flex flex-col gap-2 text-center">
|
||||||
{#snippet icon()}
|
<Icon icon={GalleryMinimalistic} size={5} />
|
||||||
<div><Icon icon={Moon} size={7} /></div>
|
Content
|
||||||
{/snippet}
|
</Link>
|
||||||
{#snippet title()}
|
<Link
|
||||||
<div>Theme</div>
|
replaceState
|
||||||
{/snippet}
|
href="/settings/privacy"
|
||||||
{#snippet info()}
|
class="aspect-square btn h-[unset] btn-neutral flex flex-col gap-2 text-center">
|
||||||
<div>Switch between light and dark mode</div>
|
<Icon icon={Shield} size={5} />
|
||||||
{/snippet}
|
Privacy
|
||||||
</CardButton>
|
</Link>
|
||||||
</Button>
|
</div>
|
||||||
<Link replaceState href="/settings/about">
|
<div class="flex gap-3 items-center opacity-75 text-sm">
|
||||||
<CardButton class="btn-neutral">
|
<Button onclick={toggleTheme}>Theme</Button>
|
||||||
{#snippet icon()}
|
/
|
||||||
<div><Icon icon={Code2} size={7} /></div>
|
<Link replaceState href="/settings/about">About</Link>
|
||||||
{/snippet}
|
/
|
||||||
{#snippet title()}
|
<Button onclick={logout}>Log Out</Button>
|
||||||
<div>About</div>
|
</div>
|
||||||
{/snippet}
|
|
||||||
{#snippet info()}
|
|
||||||
<div>Learn about {PLATFORM_NAME} and support the developer</div>
|
|
||||||
{/snippet}
|
|
||||||
</CardButton>
|
|
||||||
</Link>
|
|
||||||
<Button onclick={logout} class="btn btn-neutral">
|
|
||||||
<Icon icon={Exit} /> Log Out
|
|
||||||
</Button>
|
|
||||||
<Button class="btn btn-link w-full md:hidden" onclick={back}>
|
|
||||||
<Icon icon={AltArrowLeft} />
|
|
||||||
Go back
|
|
||||||
</Button>
|
|
||||||
</div>
|
</div>
|
||||||
</ModalBody>
|
</ModalBody>
|
||||||
</Modal>
|
</Modal>
|
||||||
|
|||||||
@@ -1,32 +0,0 @@
|
|||||||
<script lang="ts">
|
|
||||||
import Link from "@lib/components/Link.svelte"
|
|
||||||
import CardButton from "@lib/components/CardButton.svelte"
|
|
||||||
import RelayIcon from "@app/components/RelayIcon.svelte"
|
|
||||||
import RelayName from "@app/components/RelayName.svelte"
|
|
||||||
import RelayDescription from "@app/components/RelayDescription.svelte"
|
|
||||||
import {makeSpacePath} from "@app/util/routes"
|
|
||||||
import {notifications} from "@app/util/notifications"
|
|
||||||
|
|
||||||
const {url} = $props()
|
|
||||||
|
|
||||||
const path = makeSpacePath(url)
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<Link replaceState href={path}>
|
|
||||||
<CardButton class="btn-neutral shadow-md bg-alt rounded-box border-none">
|
|
||||||
{#snippet icon()}
|
|
||||||
<RelayIcon {url} size={12} class="rounded-full" />
|
|
||||||
{/snippet}
|
|
||||||
{#snippet title()}
|
|
||||||
<div class="flex gap-1">
|
|
||||||
<RelayName {url} />
|
|
||||||
{#if $notifications.has(path)}
|
|
||||||
<div class="relative top-1 h-2 w-2 rounded-full bg-primary"></div>
|
|
||||||
{/if}
|
|
||||||
</div>
|
|
||||||
{/snippet}
|
|
||||||
{#snippet info()}
|
|
||||||
<div><RelayDescription {url} /></div>
|
|
||||||
{/snippet}
|
|
||||||
</CardButton>
|
|
||||||
</Link>
|
|
||||||
@@ -2,38 +2,47 @@
|
|||||||
import {onMount, mount, unmount} from "svelte"
|
import {onMount, mount, unmount} from "svelte"
|
||||||
import Drawer from "@lib/components/Drawer.svelte"
|
import Drawer from "@lib/components/Drawer.svelte"
|
||||||
import Dialog from "@lib/components/Dialog.svelte"
|
import Dialog from "@lib/components/Dialog.svelte"
|
||||||
import {modal, clearModals} from "@app/util/modal"
|
import {modal, modalStack, popModal} from "@app/util/modal"
|
||||||
|
|
||||||
const closeModals = () => {
|
const closeModal = () => {
|
||||||
if ($modal && !$modal.options.noEscape) {
|
if ($modal && !$modal.options.noEscape) {
|
||||||
clearModals()
|
popModal()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const onKeyDown = (e: any) => {
|
const onKeyDown = (e: any) => {
|
||||||
if (e.code === "Escape" && e.target === document.body) {
|
if (e.code === "Escape" && e.target === document.body) {
|
||||||
closeModals()
|
closeModal()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let element: HTMLElement
|
let element: HTMLElement
|
||||||
let instance: any | undefined
|
const instances: Record<string, any> = {}
|
||||||
|
|
||||||
onMount(() => {
|
onMount(() => {
|
||||||
return modal.subscribe($modal => {
|
return modalStack.subscribe($modalStack => {
|
||||||
if (instance) {
|
const ids = $modalStack.map(({id}) => id)
|
||||||
unmount(instance, {outro: true})
|
|
||||||
instance = undefined
|
for (const [id, instance] of Object.entries(instances)) {
|
||||||
|
if (!ids.includes(id)) {
|
||||||
|
unmount(instance, {outro: true})
|
||||||
|
delete instances[id]
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if ($modal) {
|
for (const item of $modalStack) {
|
||||||
const {options, component, props} = $modal
|
if (instances[item.id]) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
const {options, component, props} = item
|
||||||
const wrapper = options.drawer ? Drawer : Dialog
|
const wrapper = options.drawer ? Drawer : Dialog
|
||||||
|
|
||||||
instance = mount(wrapper as any, {
|
instances[item.id] = mount(wrapper as any, {
|
||||||
target: element,
|
target: element,
|
||||||
props: {
|
props: {
|
||||||
onClose: closeModals,
|
onClose: closeModal,
|
||||||
|
noEscape: options.noEscape,
|
||||||
fullscreen: options.fullscreen,
|
fullscreen: options.fullscreen,
|
||||||
children: {component, props},
|
children: {component, props},
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import {onMount} from "svelte"
|
import {onMount} from "svelte"
|
||||||
import {notificationSettings} from "@app/core/state"
|
import {notificationSettings} from "@app/core/state"
|
||||||
import {onNotification} from "@app/util/notifications"
|
import {onNotification} from "@app/util/push"
|
||||||
|
|
||||||
let audioElement: HTMLAudioElement
|
let audioElement: HTMLAudioElement
|
||||||
|
|
||||||
|
|||||||
@@ -89,7 +89,7 @@
|
|||||||
<p>Let's start by confirming your email.</p>
|
<p>Let's start by confirming your email.</p>
|
||||||
<p>
|
<p>
|
||||||
For security reasons, you may receive three or more emails with confirmation codes in them.
|
For security reasons, you may receive three or more emails with confirmation codes in them.
|
||||||
Please paste <strong>all</strong> confirmation codes into the text box below, on separate lines.
|
Please paste <strong>all</strong> confirmation codes into the text box below.
|
||||||
</p>
|
</p>
|
||||||
<StringMultiInput bind:value={otps} placeholder="Enter your confirmation codes..." />
|
<StringMultiInput bind:value={otps} placeholder="Enter your confirmation codes..." />
|
||||||
</ModalBody>
|
</ModalBody>
|
||||||
|
|||||||
@@ -1,24 +1,20 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import type {Snippet} from "svelte"
|
import type {Snippet} from "svelte"
|
||||||
import {splitAt} from "@welshman/lib"
|
|
||||||
import {userProfile} from "@welshman/app"
|
import {userProfile} from "@welshman/app"
|
||||||
import Widget from "@assets/icons/widget.svg?dataurl"
|
|
||||||
import Compass from "@assets/icons/compass.svg?dataurl"
|
|
||||||
import Letter from "@assets/icons/letter.svg?dataurl"
|
import Letter from "@assets/icons/letter.svg?dataurl"
|
||||||
import Magnifier from "@assets/icons/magnifier.svg?dataurl"
|
import Magnifier from "@assets/icons/magnifier.svg?dataurl"
|
||||||
import HomeSmile from "@assets/icons/home-smile.svg?dataurl"
|
import Widget from "@assets/icons/widget-4.svg?dataurl"
|
||||||
import SettingsMinimalistic from "@assets/icons/settings-minimalistic.svg?dataurl"
|
|
||||||
import UserRounded from "@assets/icons/user-rounded.svg?dataurl"
|
import UserRounded from "@assets/icons/user-rounded.svg?dataurl"
|
||||||
import Settings from "@assets/icons/settings.svg?dataurl"
|
import Settings from "@assets/icons/settings.svg?dataurl"
|
||||||
import ImageIcon from "@lib/components/ImageIcon.svelte"
|
import ImageIcon from "@lib/components/ImageIcon.svelte"
|
||||||
import Divider from "@lib/components/Divider.svelte"
|
import Divider from "@lib/components/Divider.svelte"
|
||||||
import PrimaryNavItem from "@lib/components/PrimaryNavItem.svelte"
|
|
||||||
import MenuSettings from "@app/components/MenuSettings.svelte"
|
import MenuSettings from "@app/components/MenuSettings.svelte"
|
||||||
import PrimaryNavItemSpace from "@app/components/PrimaryNavItemSpace.svelte"
|
import PrimaryNavItem from "@lib/components/PrimaryNavItem.svelte"
|
||||||
import {userSpaceUrls, PLATFORM_RELAYS, PLATFORM_LOGO} from "@app/core/state"
|
import PrimaryNavSpaces from "@app/components/PrimaryNavSpaces.svelte"
|
||||||
|
import {userSpaceUrls, PLATFORM_RELAYS} from "@app/core/state"
|
||||||
import {pushModal} from "@app/util/modal"
|
import {pushModal} from "@app/util/modal"
|
||||||
import {notifications} from "@app/util/notifications"
|
import {notifications} from "@app/util/notifications"
|
||||||
import {goToLastChat} from "@app/util/routes"
|
import {goToChat} from "@app/util/routes"
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
children?: Snippet
|
children?: Snippet
|
||||||
@@ -26,46 +22,17 @@
|
|||||||
|
|
||||||
const {children}: Props = $props()
|
const {children}: Props = $props()
|
||||||
|
|
||||||
|
const chatHandler = () => goToChat()
|
||||||
|
|
||||||
const showSettingsMenu = () => pushModal(MenuSettings)
|
const showSettingsMenu = () => pushModal(MenuSettings)
|
||||||
|
|
||||||
let windowHeight = $state(0)
|
|
||||||
|
|
||||||
const itemHeight = 56
|
|
||||||
const navPadding = 8 * itemHeight
|
|
||||||
const itemLimit = $derived((windowHeight - navPadding) / itemHeight)
|
|
||||||
const [primarySpaceUrls, secondarySpaceUrls] = $derived(splitAt(itemLimit, $userSpaceUrls))
|
|
||||||
const anySpaceNotifications = $derived($userSpaceUrls.some(p => $notifications.has(p)))
|
const anySpaceNotifications = $derived($userSpaceUrls.some(p => $notifications.has(p)))
|
||||||
const otherSpaceNotifications = $derived(secondarySpaceUrls.some(p => $notifications.has(p)))
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<svelte:window bind:innerHeight={windowHeight} />
|
|
||||||
|
|
||||||
<div
|
<div
|
||||||
class="ml-sai mt-sai mb-sai relative z-nav hidden w-14 flex-shrink-0 bg-base-200 pt-4 md:block">
|
class="ml-sai mt-sai mb-sai relative z-nav hidden w-14 flex-shrink-0 bg-base-200 pt-4 md:block">
|
||||||
<div class="flex h-full flex-col" class:justify-between={PLATFORM_RELAYS.length === 0}>
|
<div class="flex h-full flex-col" class:justify-between={PLATFORM_RELAYS.length === 0}>
|
||||||
<div>
|
<PrimaryNavSpaces />
|
||||||
{#each PLATFORM_RELAYS as url (url)}
|
|
||||||
<PrimaryNavItemSpace {url} />
|
|
||||||
{:else}
|
|
||||||
<PrimaryNavItem title="Home" href="/home" class="tooltip-right">
|
|
||||||
<ImageIcon alt="Home" src={PLATFORM_LOGO} class="rounded-full" size={10} />
|
|
||||||
</PrimaryNavItem>
|
|
||||||
<Divider />
|
|
||||||
{#each primarySpaceUrls as url (url)}
|
|
||||||
<PrimaryNavItemSpace {url} />
|
|
||||||
{/each}
|
|
||||||
<PrimaryNavItem
|
|
||||||
href="/spaces"
|
|
||||||
title="All Spaces"
|
|
||||||
class="tooltip-right"
|
|
||||||
notification={otherSpaceNotifications}>
|
|
||||||
<ImageIcon alt="All Spaces" src={Widget} size={8} />
|
|
||||||
</PrimaryNavItem>
|
|
||||||
<PrimaryNavItem title="Add a Space" href="/discover" class="tooltip-right">
|
|
||||||
<ImageIcon alt="Add a Space" src={Compass} size={8} />
|
|
||||||
</PrimaryNavItem>
|
|
||||||
{/each}
|
|
||||||
</div>
|
|
||||||
{#if PLATFORM_RELAYS.length > 0}
|
{#if PLATFORM_RELAYS.length > 0}
|
||||||
<Divider />
|
<Divider />
|
||||||
{/if}
|
{/if}
|
||||||
@@ -83,7 +50,7 @@
|
|||||||
</PrimaryNavItem>
|
</PrimaryNavItem>
|
||||||
<PrimaryNavItem
|
<PrimaryNavItem
|
||||||
title="Messages"
|
title="Messages"
|
||||||
onclick={goToLastChat}
|
onclick={chatHandler}
|
||||||
class="tooltip-right"
|
class="tooltip-right"
|
||||||
notification={$notifications.has("/chat")}>
|
notification={$notifications.has("/chat")}>
|
||||||
<ImageIcon alt="Messages" src={Letter} size={8} />
|
<ImageIcon alt="Messages" src={Letter} size={8} />
|
||||||
@@ -105,18 +72,19 @@
|
|||||||
class="bottom-nav hide-on-keyboard border-top bottom-sai fixed left-0 right-0 z-nav h-14 border border-base-200 bg-base-100 md:hidden">
|
class="bottom-nav hide-on-keyboard border-top bottom-sai fixed left-0 right-0 z-nav h-14 border border-base-200 bg-base-100 md:hidden">
|
||||||
<div class="content-padding-x content-sizing flex justify-between px-2">
|
<div class="content-padding-x content-sizing flex justify-between px-2">
|
||||||
<div class="flex gap-2 sm:gap-6">
|
<div class="flex gap-2 sm:gap-6">
|
||||||
<PrimaryNavItem title="Home" href="/home">
|
<PrimaryNavItem title="Search" href="/people">
|
||||||
<ImageIcon alt="Home" src={HomeSmile} size={8} />
|
<ImageIcon alt="Search" src={Magnifier} size={8} />
|
||||||
</PrimaryNavItem>
|
</PrimaryNavItem>
|
||||||
<PrimaryNavItem
|
<PrimaryNavItem
|
||||||
title="Messages"
|
title="Messages"
|
||||||
onclick={goToLastChat}
|
href="/chat"
|
||||||
|
onclick={chatHandler}
|
||||||
notification={$notifications.has("/chat")}>
|
notification={$notifications.has("/chat")}>
|
||||||
<ImageIcon alt="Messages" src={Letter} size={8} />
|
<ImageIcon alt="Messages" src={Letter} size={8} />
|
||||||
</PrimaryNavItem>
|
</PrimaryNavItem>
|
||||||
{#if PLATFORM_RELAYS.length !== 1}
|
{#if PLATFORM_RELAYS.length !== 1}
|
||||||
<PrimaryNavItem title="Spaces" href="/spaces" notification={anySpaceNotifications}>
|
<PrimaryNavItem title="Spaces" href="/spaces" notification={anySpaceNotifications}>
|
||||||
<ImageIcon alt="Spaces" src={SettingsMinimalistic} size={8} />
|
<ImageIcon alt="Spaces" src={Widget} size={8} />
|
||||||
</PrimaryNavItem>
|
</PrimaryNavItem>
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -0,0 +1,42 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import {splitAt} from "@welshman/lib"
|
||||||
|
import Widget from "@assets/icons/widget-4.svg?dataurl"
|
||||||
|
import ImageIcon from "@lib/components/ImageIcon.svelte"
|
||||||
|
import Divider from "@lib/components/Divider.svelte"
|
||||||
|
import PrimaryNavItem from "@lib/components/PrimaryNavItem.svelte"
|
||||||
|
import PrimaryNavItemSpace from "@app/components/PrimaryNavItemSpace.svelte"
|
||||||
|
import {userSpaceUrls, PLATFORM_RELAYS, PLATFORM_LOGO} from "@app/core/state"
|
||||||
|
import {notifications} from "@app/util/notifications"
|
||||||
|
|
||||||
|
let windowHeight = $state(0)
|
||||||
|
|
||||||
|
const itemHeight = 56
|
||||||
|
const navPadding = 8 * itemHeight
|
||||||
|
const itemLimit = $derived((windowHeight - navPadding) / itemHeight)
|
||||||
|
const [primarySpaceUrls, secondarySpaceUrls] = $derived(splitAt(itemLimit, $userSpaceUrls))
|
||||||
|
const otherSpaceNotifications = $derived(secondarySpaceUrls.some(p => $notifications.has(p)))
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<svelte:window bind:innerHeight={windowHeight} />
|
||||||
|
|
||||||
|
<div>
|
||||||
|
{#each PLATFORM_RELAYS as url (url)}
|
||||||
|
<PrimaryNavItemSpace {url} />
|
||||||
|
{:else}
|
||||||
|
<PrimaryNavItem title="Home" href="/home" class="tooltip-right">
|
||||||
|
<ImageIcon alt="Home" src={PLATFORM_LOGO} class="rounded-full" size={10} />
|
||||||
|
</PrimaryNavItem>
|
||||||
|
<Divider />
|
||||||
|
{#each primarySpaceUrls as url (url)}
|
||||||
|
<PrimaryNavItemSpace {url} />
|
||||||
|
{/each}
|
||||||
|
<PrimaryNavItem
|
||||||
|
href="/spaces"
|
||||||
|
title="All Spaces"
|
||||||
|
class="tooltip-right"
|
||||||
|
prefix="no-highlight"
|
||||||
|
notification={otherSpaceNotifications}>
|
||||||
|
<ImageIcon alt="All Spaces" src={Widget} size={8} />
|
||||||
|
</PrimaryNavItem>
|
||||||
|
{/each}
|
||||||
|
</div>
|
||||||
@@ -17,28 +17,43 @@
|
|||||||
url?: string
|
url?: string
|
||||||
showPubkey?: boolean
|
showPubkey?: boolean
|
||||||
avatarSize?: number
|
avatarSize?: number
|
||||||
|
inert?: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
const {pubkey, url, showPubkey, avatarSize = 10}: Props = $props()
|
const {pubkey, url, showPubkey, inert, avatarSize = 10}: Props = $props()
|
||||||
|
|
||||||
const relays = removeUndefined([url])
|
const relays = removeUndefined([url])
|
||||||
const profileDisplay = deriveProfileDisplay(pubkey, relays)
|
const profileDisplay = deriveProfileDisplay(pubkey, relays)
|
||||||
const handle = deriveHandleForPubkey(pubkey)
|
const handle = deriveHandleForPubkey(pubkey)
|
||||||
|
|
||||||
const openProfile = () => pushModal(ProfileDetail, {pubkey, url})
|
const openProfile = () => {
|
||||||
|
pushModal(ProfileDetail, {pubkey, url})
|
||||||
|
}
|
||||||
|
|
||||||
const copyPubkey = () => clip(nip19.npubEncode(pubkey))
|
const copyPubkey = () => clip(nip19.npubEncode(pubkey))
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="flex max-w-full items-start gap-3">
|
<div class="flex max-w-full items-start gap-3">
|
||||||
<Button onclick={openProfile} class="py-1">
|
{#if inert}
|
||||||
<ProfileCircle {pubkey} size={avatarSize} />
|
<span class="py-1">
|
||||||
</Button>
|
<ProfileCircle {pubkey} size={avatarSize} />
|
||||||
|
</span>
|
||||||
|
{:else}
|
||||||
|
<Button onclick={openProfile} class="py-1">
|
||||||
|
<ProfileCircle {pubkey} size={avatarSize} />
|
||||||
|
</Button>
|
||||||
|
{/if}
|
||||||
<div class="flex min-w-0 flex-col">
|
<div class="flex min-w-0 flex-col">
|
||||||
<div class="flex items-center gap-2">
|
<div class="flex items-center gap-2">
|
||||||
<Button onclick={openProfile} class="text-bold overflow-hidden text-ellipsis">
|
{#if inert}
|
||||||
{$profileDisplay}
|
<span class="text-bold overflow-hidden text-ellipsis">
|
||||||
</Button>
|
{$profileDisplay}
|
||||||
|
</span>
|
||||||
|
{:else}
|
||||||
|
<Button onclick={openProfile} class="text-bold overflow-hidden text-ellipsis">
|
||||||
|
{$profileDisplay}
|
||||||
|
</Button>
|
||||||
|
{/if}
|
||||||
<WotScore {pubkey} />
|
<WotScore {pubkey} />
|
||||||
</div>
|
</div>
|
||||||
{#if $handle}
|
{#if $handle}
|
||||||
|
|||||||
@@ -6,7 +6,7 @@
|
|||||||
import ImageIcon from "@lib/components/ImageIcon.svelte"
|
import ImageIcon from "@lib/components/ImageIcon.svelte"
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
pubkey: string
|
pubkey?: string
|
||||||
class?: string
|
class?: string
|
||||||
size?: number
|
size?: number
|
||||||
url?: string
|
url?: string
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import {onMount} from "svelte"
|
import {onMount} from "svelte"
|
||||||
import {goto} from "$app/navigation"
|
|
||||||
import {removeUndefined} from "@welshman/lib"
|
import {removeUndefined} from "@welshman/lib"
|
||||||
import {ManagementMethod} from "@welshman/util"
|
import {ManagementMethod} from "@welshman/util"
|
||||||
import {
|
import {
|
||||||
@@ -30,9 +29,10 @@
|
|||||||
import EventInfo from "@app/components/EventInfo.svelte"
|
import EventInfo from "@app/components/EventInfo.svelte"
|
||||||
import ProfileBadges from "@app/components/ProfileBadges.svelte"
|
import ProfileBadges from "@app/components/ProfileBadges.svelte"
|
||||||
import {pubkeyLink, deriveUserIsSpaceAdmin, deriveSpaceBannedPubkeyItems} from "@app/core/state"
|
import {pubkeyLink, deriveUserIsSpaceAdmin, deriveSpaceBannedPubkeyItems} from "@app/core/state"
|
||||||
|
import {addSpaceMembers} from "@app/core/commands"
|
||||||
import {pushModal} from "@app/util/modal"
|
import {pushModal} from "@app/util/modal"
|
||||||
import {pushToast} from "@app/util/toast"
|
import {pushToast} from "@app/util/toast"
|
||||||
import {makeChatPath} from "@app/util/routes"
|
import {goToChat} from "@app/util/routes"
|
||||||
|
|
||||||
export type Props = {
|
export type Props = {
|
||||||
pubkey: string
|
pubkey: string
|
||||||
@@ -51,11 +51,9 @@
|
|||||||
|
|
||||||
const back = () => history.back()
|
const back = () => history.back()
|
||||||
|
|
||||||
const chatPath = makeChatPath([pubkey])
|
|
||||||
|
|
||||||
const showInfo = () => pushModal(EventInfo, {url, event: $profile!.event})
|
const showInfo = () => pushModal(EventInfo, {url, event: $profile!.event})
|
||||||
|
|
||||||
const openChat = () => goto(chatPath)
|
const openChat = () => goToChat([pubkey])
|
||||||
|
|
||||||
const toggleMenu = (pubkey: string) => {
|
const toggleMenu = (pubkey: string) => {
|
||||||
showMenu = !showMenu
|
showMenu = !showMenu
|
||||||
@@ -85,10 +83,7 @@
|
|||||||
})
|
})
|
||||||
|
|
||||||
const restoreMember = async () => {
|
const restoreMember = async () => {
|
||||||
const {error} = await manageRelay(url!, {
|
const error = await addSpaceMembers(url!, [pubkey])
|
||||||
method: ManagementMethod.AllowPubkey,
|
|
||||||
params: [pubkey],
|
|
||||||
})
|
|
||||||
|
|
||||||
if (error) {
|
if (error) {
|
||||||
pushToast({theme: "error", message: error})
|
pushToast({theme: "error", message: error})
|
||||||
|
|||||||
@@ -1,30 +1,70 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import {onMount} from "svelte"
|
import {onMount} from "svelte"
|
||||||
|
import {SvelteSet} from "svelte/reactivity"
|
||||||
import type {Readable} from "svelte/store"
|
import type {Readable} from "svelte/store"
|
||||||
import {tryCatch} from "@welshman/lib"
|
import {tryCatch} from "@welshman/lib"
|
||||||
import {isShareableRelayUrl, normalizeRelayUrl} from "@welshman/util"
|
import {isShareableRelayUrl, isIPAddress, normalizeRelayUrl} from "@welshman/util"
|
||||||
import {relaySearch} from "@welshman/app"
|
import type {Thunk} from "@welshman/app"
|
||||||
|
import {waitForThunkError, relaySearch} from "@welshman/app"
|
||||||
import {createScroller} from "@lib/html"
|
import {createScroller} from "@lib/html"
|
||||||
|
import {errorMessage} from "@lib/util"
|
||||||
import Magnifier from "@assets/icons/magnifier.svg?dataurl"
|
import Magnifier from "@assets/icons/magnifier.svg?dataurl"
|
||||||
import AddCircle from "@assets/icons/add-circle.svg?dataurl"
|
import AddCircle from "@assets/icons/add-circle.svg?dataurl"
|
||||||
import Icon from "@lib/components/Icon.svelte"
|
import Icon from "@lib/components/Icon.svelte"
|
||||||
import Button from "@lib/components/Button.svelte"
|
import Button from "@lib/components/Button.svelte"
|
||||||
import Modal from "@lib/components/Modal.svelte"
|
import Modal from "@lib/components/Modal.svelte"
|
||||||
import ModalBody from "@lib/components/ModalBody.svelte"
|
import ModalBody from "@lib/components/ModalBody.svelte"
|
||||||
|
import ModalFooter from "@lib/components/ModalFooter.svelte"
|
||||||
import RelayItem from "@app/components/RelayItem.svelte"
|
import RelayItem from "@app/components/RelayItem.svelte"
|
||||||
|
import {pushToast} from "@app/util/toast"
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
relays: Readable<string[]>
|
relays: Readable<string[]>
|
||||||
addRelay: (url: string) => void
|
addRelay: (url: string) => Promise<Thunk>
|
||||||
|
matchRelay?: (url: string) => boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
const {relays, addRelay}: Props = $props()
|
const {relays, addRelay, matchRelay}: Props = $props()
|
||||||
|
|
||||||
|
const back = () => history.back()
|
||||||
|
|
||||||
|
const customUrl = $derived(tryCatch(() => normalizeRelayUrl(term)))
|
||||||
|
|
||||||
|
const add = async (url: string) => {
|
||||||
|
loading.add(url)
|
||||||
|
|
||||||
|
try {
|
||||||
|
const error = await waitForThunkError(await addRelay(url))
|
||||||
|
|
||||||
|
if (error) {
|
||||||
|
pushToast({
|
||||||
|
theme: "error",
|
||||||
|
message: `Failed to add relay: ${errorMessage(error)}`,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
loading.delete(url)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
let term = $state("")
|
let term = $state("")
|
||||||
let limit = $state(20)
|
let limit = $state(20)
|
||||||
let element: Element | undefined = $state()
|
let element: Element | undefined = $state()
|
||||||
|
|
||||||
const customUrl = $derived(tryCatch(() => normalizeRelayUrl(term)))
|
const loading = $state(new SvelteSet<string>())
|
||||||
|
|
||||||
|
const searchResults = $derived(
|
||||||
|
$relaySearch
|
||||||
|
.searchValues(term)
|
||||||
|
.filter(url => {
|
||||||
|
if (matchRelay?.(url) === false) return false
|
||||||
|
if ($relays.includes(url)) return false
|
||||||
|
if (isIPAddress(url)) return false
|
||||||
|
|
||||||
|
return true
|
||||||
|
})
|
||||||
|
.slice(0, limit),
|
||||||
|
)
|
||||||
|
|
||||||
onMount(() => {
|
onMount(() => {
|
||||||
const scroller = createScroller({
|
const scroller = createScroller({
|
||||||
@@ -52,23 +92,35 @@
|
|||||||
<RelayItem url={term}>
|
<RelayItem url={term}>
|
||||||
<Button
|
<Button
|
||||||
class="btn btn-outline btn-sm flex items-center"
|
class="btn btn-outline btn-sm flex items-center"
|
||||||
onclick={() => addRelay(customUrl)}>
|
disabled={loading.has(customUrl)}
|
||||||
<Icon icon={AddCircle} />
|
onclick={() => add(customUrl)}>
|
||||||
|
{#if loading.has(customUrl)}
|
||||||
|
<span class="loading loading-spinner loading-sm"></span>
|
||||||
|
{:else}
|
||||||
|
<Icon icon={AddCircle} />
|
||||||
|
{/if}
|
||||||
Add Relay
|
Add Relay
|
||||||
</Button>
|
</Button>
|
||||||
</RelayItem>
|
</RelayItem>
|
||||||
{/if}
|
{/if}
|
||||||
{#each $relaySearch
|
{#each searchResults as url (url)}
|
||||||
.searchValues(term)
|
|
||||||
.filter(url => !$relays.includes(url))
|
|
||||||
.slice(0, limit) as url (url)}
|
|
||||||
<RelayItem {url}>
|
<RelayItem {url}>
|
||||||
<Button class="btn btn-outline btn-sm flex items-center" onclick={() => addRelay(url)}>
|
<Button
|
||||||
<Icon icon={AddCircle} />
|
class="btn btn-outline btn-sm flex items-center"
|
||||||
|
disabled={loading.has(url)}
|
||||||
|
onclick={() => add(url)}>
|
||||||
|
{#if loading.has(url)}
|
||||||
|
<span class="loading loading-spinner loading-sm"></span>
|
||||||
|
{:else}
|
||||||
|
<Icon icon={AddCircle} />
|
||||||
|
{/if}
|
||||||
Add Relay
|
Add Relay
|
||||||
</Button>
|
</Button>
|
||||||
</RelayItem>
|
</RelayItem>
|
||||||
{/each}
|
{/each}
|
||||||
</div>
|
</div>
|
||||||
</ModalBody>
|
</ModalBody>
|
||||||
|
<ModalFooter>
|
||||||
|
<Button class="btn btn-primary flex-grow" onclick={back}>Done</Button>
|
||||||
|
</ModalFooter>
|
||||||
</Modal>
|
</Modal>
|
||||||
|
|||||||
@@ -0,0 +1,91 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import type {Readable} from "svelte/store"
|
||||||
|
import {SvelteSet} from "svelte/reactivity"
|
||||||
|
import {waitForThunkError} from "@welshman/app"
|
||||||
|
import type {Thunk} from "@welshman/app"
|
||||||
|
import CloseCircle from "@assets/icons/close-circle.svg?dataurl"
|
||||||
|
import AltArrowLeft from "@assets/icons/alt-arrow-left.svg?dataurl"
|
||||||
|
import AltArrowRight from "@assets/icons/alt-arrow-right.svg?dataurl"
|
||||||
|
import DangerTriangle from "@assets/icons/danger-triangle.svg?dataurl"
|
||||||
|
import {errorMessage} from "@lib/util"
|
||||||
|
import Icon from "@lib/components/Icon.svelte"
|
||||||
|
import Button from "@lib/components/Button.svelte"
|
||||||
|
import Modal from "@lib/components/Modal.svelte"
|
||||||
|
import ModalBody from "@lib/components/ModalBody.svelte"
|
||||||
|
import ModalFooter from "@lib/components/ModalFooter.svelte"
|
||||||
|
import RelayAdd from "@app/components/RelayAdd.svelte"
|
||||||
|
import RelayItem from "@app/components/RelayItem.svelte"
|
||||||
|
import {pushModal} from "@app/util/modal"
|
||||||
|
import {pushToast} from "@app/util/toast"
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
title: string
|
||||||
|
subtitle: string
|
||||||
|
relays: Readable<string[]>
|
||||||
|
addRelay: (url: string) => Promise<Thunk>
|
||||||
|
removeRelay: (url: string) => Promise<Thunk>
|
||||||
|
matchRelay?: (url: string) => boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
const {title, subtitle, relays, addRelay, removeRelay, matchRelay}: Props = $props()
|
||||||
|
|
||||||
|
const back = () => history.back()
|
||||||
|
|
||||||
|
const add = () => pushModal(RelayAdd, {relays, addRelay, matchRelay})
|
||||||
|
|
||||||
|
const remove = async (url: string) => {
|
||||||
|
loading.add(url)
|
||||||
|
|
||||||
|
try {
|
||||||
|
const error = await waitForThunkError(await removeRelay(url))
|
||||||
|
|
||||||
|
if (error) {
|
||||||
|
pushToast({
|
||||||
|
theme: "error",
|
||||||
|
message: `Failed to remove relay: ${errorMessage(error)}`,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
loading.delete(url)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const loading = $state(new SvelteSet<string>())
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<Modal>
|
||||||
|
<ModalBody>
|
||||||
|
<h2 class="text-xl">{title}</h2>
|
||||||
|
<p class="text-sm">{subtitle}</p>
|
||||||
|
{#each $relays.toSorted() as url (url)}
|
||||||
|
<RelayItem {url}>
|
||||||
|
<Button
|
||||||
|
class="btn btn-sm btn-neutral"
|
||||||
|
disabled={loading.has(url)}
|
||||||
|
onclick={() => remove(url)}>
|
||||||
|
{#if loading.has(url)}
|
||||||
|
<span class="loading loading-spinner loading-sm"></span>
|
||||||
|
{:else}
|
||||||
|
<Icon icon={CloseCircle} />
|
||||||
|
{/if}
|
||||||
|
Remove
|
||||||
|
</Button>
|
||||||
|
</RelayItem>
|
||||||
|
{:else}
|
||||||
|
<p class="text-center py-12 flex justify-center items-center gap-2">
|
||||||
|
<Icon icon={DangerTriangle} />
|
||||||
|
No relay selections found.
|
||||||
|
</p>
|
||||||
|
{/each}
|
||||||
|
</ModalBody>
|
||||||
|
<ModalFooter>
|
||||||
|
<Button class="btn btn-link" onclick={back}>
|
||||||
|
<Icon icon={AltArrowLeft} />
|
||||||
|
Go back
|
||||||
|
</Button>
|
||||||
|
<Button class="btn btn-primary" onclick={add}>
|
||||||
|
Add Relays
|
||||||
|
<Icon icon={AltArrowRight} />
|
||||||
|
</Button>
|
||||||
|
</ModalFooter>
|
||||||
|
</Modal>
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import Stars from "@assets/icons/stars.svg?dataurl"
|
||||||
|
import Icon from "@lib/components/Icon.svelte"
|
||||||
|
import Button from "@lib/components/Button.svelte"
|
||||||
|
import type {HealthCheck} from "@app/util/health"
|
||||||
|
import {applyHealthCheck} from "@app/util/health"
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
healthCheck: HealthCheck
|
||||||
|
}
|
||||||
|
|
||||||
|
const {healthCheck}: Props = $props()
|
||||||
|
|
||||||
|
const apply = () => applyHealthCheck(healthCheck)
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="card2 card2-sm bg-alt flex justify-between">
|
||||||
|
<div class="flex flex-col gap-1">
|
||||||
|
<strong>{healthCheck.title}</strong>
|
||||||
|
<p class="text-sm">{healthCheck.description}</p>
|
||||||
|
</div>
|
||||||
|
<Button class="btn btn-neutral btn-sm" onclick={apply}>
|
||||||
|
<Icon icon={Stars} />
|
||||||
|
{healthCheck.action}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
@@ -0,0 +1,43 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import Stars from "@assets/icons/stars.svg?dataurl"
|
||||||
|
import CheckCircle from "@assets/icons/check-circle.svg?dataurl"
|
||||||
|
import DangerTriangle from "@assets/icons/danger-triangle.svg?dataurl"
|
||||||
|
import Stethoscope from "@assets/icons/stethoscope.svg?dataurl"
|
||||||
|
import Icon from "@lib/components/Icon.svelte"
|
||||||
|
import Button from "@lib/components/Button.svelte"
|
||||||
|
import RelaySettingsHealthCheck from "@app/components/RelaySettingsHealthCheck.svelte"
|
||||||
|
import {PLATFORM_NAME} from "@app/core/state"
|
||||||
|
import {pendingHealthChecks, applyHealthCheck} from "@app/util/health"
|
||||||
|
|
||||||
|
const applyAll = () => {
|
||||||
|
for (const healthCheck of $pendingHealthChecks) {
|
||||||
|
applyHealthCheck(healthCheck)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="card2 bg-alt flex flex-col gap-4 shadow-md">
|
||||||
|
<div class="flex justify-between items-center">
|
||||||
|
<strong class="flex items-center gap-3 text-lg">
|
||||||
|
<Icon icon={Stethoscope} />
|
||||||
|
Health Check
|
||||||
|
</strong>
|
||||||
|
<span class="flex items-center gap-2 text-sm">
|
||||||
|
<Icon icon={$pendingHealthChecks.length === 0 ? CheckCircle : DangerTriangle} />
|
||||||
|
{$pendingHealthChecks.length} Issue{$pendingHealthChecks.length === 1 ? "" : "s"} Detected
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<p>
|
||||||
|
{PLATFORM_NAME} actively checks your connection to the network in the background to discover relays
|
||||||
|
that are offline, that you don't have access to, or are otherwise causing trouble.
|
||||||
|
</p>
|
||||||
|
{#each $pendingHealthChecks as healthCheck}
|
||||||
|
<RelaySettingsHealthCheck {healthCheck} />
|
||||||
|
{/each}
|
||||||
|
{#if $pendingHealthChecks.length > 0}
|
||||||
|
<Button class="btn btn-primary" onclick={applyAll}>
|
||||||
|
<Icon icon={Stars} />
|
||||||
|
Apply All Recommendations
|
||||||
|
</Button>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
@@ -0,0 +1,50 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import type {Readable} from "svelte/store"
|
||||||
|
import Check from "@assets/icons/check.svg?dataurl"
|
||||||
|
import DangerTriangle from "@assets/icons/danger-triangle.svg?dataurl"
|
||||||
|
import Icon from "@lib/components/Icon.svelte"
|
||||||
|
import RelayList from "@app/components/RelayList.svelte"
|
||||||
|
import {pushModal} from "@app/util/modal"
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
icon: string
|
||||||
|
title: string
|
||||||
|
subtitle: string
|
||||||
|
relays: Readable<string[]>
|
||||||
|
addRelay: (url: string) => unknown
|
||||||
|
removeRelay: (url: string) => unknown
|
||||||
|
matchRelay?: (url: string) => boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
const {icon, title, relays, subtitle, addRelay, removeRelay, matchRelay}: Props = $props()
|
||||||
|
|
||||||
|
const onclick = () =>
|
||||||
|
pushModal(RelayList, {title, subtitle, relays, addRelay, removeRelay, matchRelay})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="btn font-normal flex h-[unset] w-full flex-nowrap py-4 text-left items-start justify-between"
|
||||||
|
{onclick}>
|
||||||
|
<div class="flex flex-grow flex-row items-start gap-4">
|
||||||
|
<div class="flex h-7 w-7 flex-shrink-0 items-center justify-center">
|
||||||
|
<Icon {icon} />
|
||||||
|
</div>
|
||||||
|
<div class="flex flex-col gap-1">
|
||||||
|
<p class="text-lg">
|
||||||
|
{title}
|
||||||
|
</p>
|
||||||
|
<p class="text-sm">
|
||||||
|
{subtitle}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="flex items-center justify-end gap-1">
|
||||||
|
{#if $relays.length <= 1}
|
||||||
|
<Icon icon={DangerTriangle} />
|
||||||
|
{:else}
|
||||||
|
<Icon icon={Check} />
|
||||||
|
{/if}
|
||||||
|
{$relays.length}
|
||||||
|
</div>
|
||||||
|
</button>
|
||||||
@@ -9,9 +9,10 @@
|
|||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
url: string
|
url: string
|
||||||
|
hideFavorites?: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
const {url}: Props = $props()
|
const {url, hideFavorites}: Props = $props()
|
||||||
const rooms = deriveUserRooms(url)
|
const rooms = deriveUserRooms(url)
|
||||||
const favorited = deriveGroupListPubkeys(url)
|
const favorited = deriveGroupListPubkeys(url)
|
||||||
</script>
|
</script>
|
||||||
@@ -43,7 +44,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<RelayDescription {url} />
|
<RelayDescription {url} />
|
||||||
</div>
|
</div>
|
||||||
{#if $favorited.size > 0}
|
{#if !hideFavorites && $favorited.size > 0}
|
||||||
<div class="row-2 card2 card2-sm bg-alt">
|
<div class="row-2 card2 card2-sm bg-alt">
|
||||||
Favorited By:
|
Favorited By:
|
||||||
<ProfileCircles pubkeys={Array.from($favorited)} />
|
<ProfileCircles pubkeys={Array.from($favorited)} />
|
||||||
|
|||||||
@@ -26,7 +26,7 @@
|
|||||||
|
|
||||||
const back = () => history.back()
|
const back = () => history.back()
|
||||||
|
|
||||||
const onDelete = () => {
|
const onResolved = () => {
|
||||||
if ($reports.size === 0) {
|
if ($reports.size === 0) {
|
||||||
back()
|
back()
|
||||||
}
|
}
|
||||||
@@ -40,7 +40,7 @@
|
|||||||
<ModalSubtitle>All reports for this event are shown below.</ModalSubtitle>
|
<ModalSubtitle>All reports for this event are shown below.</ModalSubtitle>
|
||||||
</ModalHeader>
|
</ModalHeader>
|
||||||
{#each $reports.values() as report (report.id)}
|
{#each $reports.values() as report (report.id)}
|
||||||
<ReportItem {url} event={report} {onDelete} />
|
<ReportItem {url} event={report} {onResolved} />
|
||||||
{/each}
|
{/each}
|
||||||
</ModalBody>
|
</ModalBody>
|
||||||
<ModalFooter>
|
<ModalFooter>
|
||||||
|
|||||||
@@ -15,10 +15,10 @@
|
|||||||
type Props = {
|
type Props = {
|
||||||
url: string
|
url: string
|
||||||
event: TrustedEvent
|
event: TrustedEvent
|
||||||
onDelete?: () => void
|
onResolved?: () => void
|
||||||
}
|
}
|
||||||
|
|
||||||
const {url, event, onDelete}: Props = $props()
|
const {url, event, onResolved}: Props = $props()
|
||||||
|
|
||||||
const etag = getTag("e", event.tags)
|
const etag = getTag("e", event.tags)
|
||||||
const ptag = getTag("p", event.tags)
|
const ptag = getTag("p", event.tags)
|
||||||
@@ -45,7 +45,7 @@
|
|||||||
{/if}
|
{/if}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<ReportMenu {url} {event} {onDelete} />
|
<ReportMenu {url} {event} {onResolved} />
|
||||||
</div>
|
</div>
|
||||||
{#if event.content}
|
{#if event.content}
|
||||||
<div class="border-l-2 border-primary pl-3">
|
<div class="border-l-2 border-primary pl-3">
|
||||||
|
|||||||
@@ -20,10 +20,10 @@
|
|||||||
type Props = {
|
type Props = {
|
||||||
url: string
|
url: string
|
||||||
event: TrustedEvent
|
event: TrustedEvent
|
||||||
onDelete?: () => void
|
onResolved?: () => void
|
||||||
}
|
}
|
||||||
|
|
||||||
const {url, event, onDelete}: Props = $props()
|
const {url, event, onResolved}: Props = $props()
|
||||||
|
|
||||||
const shouldProtect = canEnforceNip70(url)
|
const shouldProtect = canEnforceNip70(url)
|
||||||
const userIsAdmin = deriveUserIsSpaceAdmin(url)
|
const userIsAdmin = deriveUserIsSpaceAdmin(url)
|
||||||
@@ -40,7 +40,7 @@
|
|||||||
|
|
||||||
const deleteReport = async () => {
|
const deleteReport = async () => {
|
||||||
publishDelete({event, relays: [url], protect: await shouldProtect})
|
publishDelete({event, relays: [url], protect: await shouldProtect})
|
||||||
onDelete?.()
|
onResolved?.()
|
||||||
}
|
}
|
||||||
|
|
||||||
const dismissReport = async () => {
|
const dismissReport = async () => {
|
||||||
@@ -54,7 +54,7 @@
|
|||||||
} else {
|
} else {
|
||||||
pushToast({message: "Content has successfully been deleted!"})
|
pushToast({message: "Content has successfully been deleted!"})
|
||||||
repository.removeEvent(event.id)
|
repository.removeEvent(event.id)
|
||||||
onDelete?.()
|
onResolved?.()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -77,7 +77,7 @@
|
|||||||
repository.removeEvent(event.id)
|
repository.removeEvent(event.id)
|
||||||
repository.removeEvent(id)
|
repository.removeEvent(id)
|
||||||
history.back()
|
history.back()
|
||||||
setTimeout(() => onDelete?.())
|
setTimeout(() => onResolved?.())
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
@@ -101,7 +101,7 @@
|
|||||||
pushToast({message: "User has successfully been banned!"})
|
pushToast({message: "User has successfully been banned!"})
|
||||||
repository.removeEvent(event.id)
|
repository.removeEvent(event.id)
|
||||||
history.back()
|
history.back()
|
||||||
setTimeout(() => onDelete?.())
|
setTimeout(() => onResolved?.())
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -16,7 +16,7 @@
|
|||||||
import Lock from "@assets/icons/lock.svg?dataurl"
|
import Lock from "@assets/icons/lock.svg?dataurl"
|
||||||
import Microphone from "@assets/icons/microphone.svg?dataurl"
|
import Microphone from "@assets/icons/microphone.svg?dataurl"
|
||||||
import Bookmark from "@assets/icons/bookmark.svg?dataurl"
|
import Bookmark from "@assets/icons/bookmark.svg?dataurl"
|
||||||
import VolumeLoud from "@assets/icons/volume-loud.svg?dataurl"
|
import Bell from "@assets/icons/bell.svg?dataurl"
|
||||||
import {fly} from "@lib/transition"
|
import {fly} from "@lib/transition"
|
||||||
import Icon from "@lib/components/Icon.svelte"
|
import Icon from "@lib/components/Icon.svelte"
|
||||||
import Button from "@lib/components/Button.svelte"
|
import Button from "@lib/components/Button.svelte"
|
||||||
@@ -255,7 +255,7 @@
|
|||||||
<strong class="text-lg">Room Settings</strong>
|
<strong class="text-lg">Room Settings</strong>
|
||||||
<div class="flex items-center justify-between">
|
<div class="flex items-center justify-between">
|
||||||
<div class="flex items-center gap-2">
|
<div class="flex items-center gap-2">
|
||||||
<Icon icon={VolumeLoud} />
|
<Icon icon={Bell} />
|
||||||
<span>Notifications</span>
|
<span>Notifications</span>
|
||||||
</div>
|
</div>
|
||||||
<input
|
<input
|
||||||
|
|||||||
@@ -5,6 +5,7 @@
|
|||||||
import {waitForThunkError, createRoom, editRoom, joinRoom} from "@welshman/app"
|
import {waitForThunkError, createRoom, editRoom, joinRoom} from "@welshman/app"
|
||||||
import StickerSmileSquare from "@assets/icons/sticker-smile-square.svg?dataurl"
|
import StickerSmileSquare from "@assets/icons/sticker-smile-square.svg?dataurl"
|
||||||
import Hashtag from "@assets/icons/hashtag.svg?dataurl"
|
import Hashtag from "@assets/icons/hashtag.svg?dataurl"
|
||||||
|
import Volume from "@assets/icons/volume.svg?dataurl"
|
||||||
import UploadMinimalistic from "@assets/icons/upload-minimalistic.svg?dataurl"
|
import UploadMinimalistic from "@assets/icons/upload-minimalistic.svg?dataurl"
|
||||||
import {preventDefault} from "@lib/html"
|
import {preventDefault} from "@lib/html"
|
||||||
import FieldInline from "@lib/components/FieldInline.svelte"
|
import FieldInline from "@lib/components/FieldInline.svelte"
|
||||||
@@ -15,6 +16,7 @@
|
|||||||
import ModalBody from "@lib/components/ModalBody.svelte"
|
import ModalBody from "@lib/components/ModalBody.svelte"
|
||||||
import {pushToast} from "@app/util/toast"
|
import {pushToast} from "@app/util/toast"
|
||||||
import {uploadFile} from "@app/core/commands"
|
import {uploadFile} from "@app/core/commands"
|
||||||
|
import {deriveHasLivekit, getRoomType, RoomType} from "@app/core/state"
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
url: string
|
url: string
|
||||||
@@ -27,12 +29,25 @@
|
|||||||
const {url, header, footer, onsubmit, initialValues = makeRoomMeta()}: Props = $props()
|
const {url, header, footer, onsubmit, initialValues = makeRoomMeta()}: Props = $props()
|
||||||
|
|
||||||
const values = $state(initialValues)
|
const values = $state(initialValues)
|
||||||
|
const relayHasLivekit = deriveHasLivekit(url)
|
||||||
|
|
||||||
const submit = async () => {
|
const submit = async () => {
|
||||||
const room = $state.snapshot(values)
|
const room = $state.snapshot(values)
|
||||||
|
|
||||||
|
if (roomType === RoomType.Voice && !$relayHasLivekit) {
|
||||||
|
return pushToast({
|
||||||
|
theme: "error",
|
||||||
|
message: "This relay does not support voice rooms.",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
room.livekit = roomType === RoomType.Voice
|
||||||
|
|
||||||
if (imageFile) {
|
if (imageFile) {
|
||||||
const {error, result} = await uploadFile(imageFile, {maxWidth: 256, maxHeight: 256})
|
const {error, result} = await uploadFile(imageFile, {
|
||||||
|
maxWidth: 256,
|
||||||
|
maxHeight: 256,
|
||||||
|
})
|
||||||
|
|
||||||
if (error) {
|
if (error) {
|
||||||
return pushToast({theme: "error", message: error})
|
return pushToast({theme: "error", message: error})
|
||||||
@@ -76,6 +91,7 @@
|
|||||||
let loading = $state(false)
|
let loading = $state(false)
|
||||||
let imageFile = $state<File | undefined>()
|
let imageFile = $state<File | undefined>()
|
||||||
let imagePreview = $state(initialValues.picture)
|
let imagePreview = $state(initialValues.picture)
|
||||||
|
let roomType = $state(getRoomType(initialValues))
|
||||||
|
|
||||||
const handleImageUpload = async (event: Event) => {
|
const handleImageUpload = async (event: Event) => {
|
||||||
const file = (event.target as HTMLInputElement).files?.[0]
|
const file = (event.target as HTMLInputElement).files?.[0]
|
||||||
@@ -145,7 +161,7 @@
|
|||||||
{#if imagePreview}
|
{#if imagePreview}
|
||||||
<ImageIcon src={imagePreview} alt="" class="rounded-lg" />
|
<ImageIcon src={imagePreview} alt="" class="rounded-lg" />
|
||||||
{:else}
|
{:else}
|
||||||
<Icon icon={Hashtag} />
|
<Icon icon={roomType === RoomType.Voice ? Volume : Hashtag} />
|
||||||
{/if}
|
{/if}
|
||||||
<input bind:value={values.name} class="grow" type="text" />
|
<input bind:value={values.name} class="grow" type="text" />
|
||||||
</label>
|
</label>
|
||||||
@@ -161,6 +177,22 @@
|
|||||||
</label>
|
</label>
|
||||||
{/snippet}
|
{/snippet}
|
||||||
</FieldInline>
|
</FieldInline>
|
||||||
|
{#if $relayHasLivekit}
|
||||||
|
<FieldInline>
|
||||||
|
{#snippet label()}
|
||||||
|
<p>Room type</p>
|
||||||
|
{/snippet}
|
||||||
|
{#snippet input()}
|
||||||
|
<select
|
||||||
|
class="select select-bordered w-full"
|
||||||
|
bind:value={roomType}
|
||||||
|
aria-label="Room type">
|
||||||
|
<option value={RoomType.Text}>Text</option>
|
||||||
|
<option value={RoomType.Voice}>Voice</option>
|
||||||
|
</select>
|
||||||
|
{/snippet}
|
||||||
|
</FieldInline>
|
||||||
|
{/if}
|
||||||
<strong class="md:hidden">Permissions</strong>
|
<strong class="md:hidden">Permissions</strong>
|
||||||
<div class="flex items-center gap-2">
|
<div class="flex items-center gap-2">
|
||||||
<input type="checkbox" class="checkbox" bind:checked={values.isRestricted} />
|
<input type="checkbox" class="checkbox" bind:checked={values.isRestricted} />
|
||||||
@@ -176,7 +208,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="flex items-center gap-2">
|
<div class="flex items-center gap-2">
|
||||||
<input type="checkbox" class="checkbox" bind:checked={values.isClosed} />
|
<input type="checkbox" class="checkbox" bind:checked={values.isClosed} />
|
||||||
<span class="text-sm opacity-75">Ignore requests to join</span>
|
<span class="text-sm opacity-75">Membership requires approval</span>
|
||||||
</div>
|
</div>
|
||||||
</ModalBody>
|
</ModalBody>
|
||||||
{@render footer({loading})}
|
{@render footer({loading})}
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import Hashtag from "@assets/icons/hashtag.svg?dataurl"
|
import Hashtag from "@assets/icons/hashtag.svg?dataurl"
|
||||||
|
import Volume from "@assets/icons/volume.svg?dataurl"
|
||||||
import Icon from "@lib/components/Icon.svelte"
|
import Icon from "@lib/components/Icon.svelte"
|
||||||
import ImageIcon from "@lib/components/ImageIcon.svelte"
|
import ImageIcon from "@lib/components/ImageIcon.svelte"
|
||||||
import {deriveRoom} from "@app/core/state"
|
import {deriveRoom} from "@app/core/state"
|
||||||
@@ -8,15 +9,25 @@
|
|||||||
h: string
|
h: string
|
||||||
url: string
|
url: string
|
||||||
size?: number
|
size?: number
|
||||||
|
fallbackIcon?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
const {url, h, size = 5}: Props = $props()
|
const {url, h, size = 5, fallbackIcon = Hashtag}: Props = $props()
|
||||||
|
|
||||||
const room = deriveRoom(url, h)
|
const room = deriveRoom(url, h)
|
||||||
|
const isVoiceRoom = $derived($room.livekit)
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
{#if $room.picture}
|
{#if isVoiceRoom}
|
||||||
|
<div class="flex shrink-0 items-center gap-1.5">
|
||||||
|
<Icon size={size + 1} icon={Volume} />
|
||||||
|
{#if $room.picture}
|
||||||
|
<span class="text-base">/</span>
|
||||||
|
<ImageIcon src={$room.picture} {size} alt="" class="rounded-lg" />
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
{:else if $room.picture}
|
||||||
<ImageIcon src={$room.picture} {size} alt="" class="rounded-lg" />
|
<ImageIcon src={$room.picture} {size} alt="" class="rounded-lg" />
|
||||||
{:else}
|
{:else}
|
||||||
<Icon icon={Hashtag} {size} />
|
<Icon icon={fallbackIcon} {size} />
|
||||||
{/if}
|
{/if}
|
||||||
|
|||||||
@@ -0,0 +1,83 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import {getTagValue, ManagementMethod} from "@welshman/util"
|
||||||
|
import type {TrustedEvent, PublishedRoomMeta} from "@welshman/util"
|
||||||
|
import {repository, manageRelay} from "@welshman/app"
|
||||||
|
import Button from "@lib/components/Button.svelte"
|
||||||
|
import ProfileName from "@app/components/ProfileName.svelte"
|
||||||
|
import ProfileDetail from "@app/components/ProfileDetail.svelte"
|
||||||
|
import RoomName from "@app/components/RoomName.svelte"
|
||||||
|
import {pushModal} from "@app/util/modal"
|
||||||
|
import {pushToast} from "@app/util/toast"
|
||||||
|
import {deriveRoom} from "@app/core/state"
|
||||||
|
import {addRoomMembers} from "@app/core/commands"
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
url: string
|
||||||
|
event: TrustedEvent
|
||||||
|
onResolved?: () => void
|
||||||
|
}
|
||||||
|
|
||||||
|
const {url, event, onResolved}: Props = $props()
|
||||||
|
|
||||||
|
const h = getTagValue("h", event.tags) || ""
|
||||||
|
const room = deriveRoom(url, h)
|
||||||
|
|
||||||
|
const showProfile = () => pushModal(ProfileDetail, {pubkey: event.pubkey, url})
|
||||||
|
|
||||||
|
const dismiss = async () => {
|
||||||
|
loading = true
|
||||||
|
|
||||||
|
try {
|
||||||
|
const {error} = await manageRelay(url, {
|
||||||
|
method: ManagementMethod.BanEvent,
|
||||||
|
params: [event.id, "Join request dismissed"],
|
||||||
|
})
|
||||||
|
|
||||||
|
if (error) {
|
||||||
|
pushToast({theme: "error", message: error})
|
||||||
|
} else {
|
||||||
|
pushToast({message: "Join request has been dismissed."})
|
||||||
|
repository.removeEvent(event.id)
|
||||||
|
onResolved?.()
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
loading = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const accept = async () => {
|
||||||
|
loading = true
|
||||||
|
|
||||||
|
try {
|
||||||
|
const error = await addRoomMembers(url, $room as PublishedRoomMeta, [event.pubkey])
|
||||||
|
|
||||||
|
if (error) {
|
||||||
|
pushToast({theme: "error", message: error})
|
||||||
|
} else {
|
||||||
|
pushToast({message: "Member has been added to the room!"})
|
||||||
|
onResolved?.()
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
loading = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let loading = $state(false)
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="column gap-4 card2 card2-sm bg-alt">
|
||||||
|
<div class="flex justify-between gap-2">
|
||||||
|
<div>
|
||||||
|
<Button class="inline text-primary" onclick={showProfile}>
|
||||||
|
<ProfileName pubkey={event.pubkey} {url} />
|
||||||
|
</Button>
|
||||||
|
<span>
|
||||||
|
requested membership in #<RoomName {url} {h} />
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div class="flex gap-2">
|
||||||
|
<Button class="btn btn-neutral btn-sm" onclick={dismiss} disabled={loading}>Dismiss</Button>
|
||||||
|
<Button class="btn btn-primary btn-sm" onclick={accept} disabled={loading}>Accept</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
@@ -2,9 +2,8 @@
|
|||||||
import {onMount} from "svelte"
|
import {onMount} from "svelte"
|
||||||
import {setKey, popKey} from "@lib/implicit"
|
import {setKey, popKey} from "@lib/implicit"
|
||||||
import {sleep} from "@welshman/lib"
|
import {sleep} from "@welshman/lib"
|
||||||
import {ManagementMethod} from "@welshman/util"
|
import {displayProfileByPubkey} from "@welshman/app"
|
||||||
import {manageRelay} from "@welshman/app"
|
import type {PublishedRoomMeta} from "@welshman/util"
|
||||||
import {addRoomMember, displayProfileByPubkey, waitForThunkError} from "@welshman/app"
|
|
||||||
import AltArrowLeft from "@assets/icons/alt-arrow-left.svg?dataurl"
|
import AltArrowLeft from "@assets/icons/alt-arrow-left.svg?dataurl"
|
||||||
import Spinner from "@lib/components/Spinner.svelte"
|
import Spinner from "@lib/components/Spinner.svelte"
|
||||||
import Button from "@lib/components/Button.svelte"
|
import Button from "@lib/components/Button.svelte"
|
||||||
@@ -22,6 +21,7 @@
|
|||||||
import {pushToast} from "@app/util/toast"
|
import {pushToast} from "@app/util/toast"
|
||||||
import {pushModal} from "@app/util/modal"
|
import {pushModal} from "@app/util/modal"
|
||||||
import {deriveRoom, deriveSpaceMembers} from "@app/core/state"
|
import {deriveRoom, deriveSpaceMembers} from "@app/core/state"
|
||||||
|
import {addRoomMembers} from "@app/core/commands"
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
url: string
|
url: string
|
||||||
@@ -42,35 +42,14 @@
|
|||||||
// Show loading for auto submit callback
|
// Show loading for auto submit callback
|
||||||
await sleep(500)
|
await sleep(500)
|
||||||
|
|
||||||
const results = await Promise.all(
|
const error = await addRoomMembers(url, $room as PublishedRoomMeta, pubkeys)
|
||||||
pubkeys
|
|
||||||
.filter(pubkey => !$spaceMembers.includes(pubkey))
|
|
||||||
.map(pubkey =>
|
|
||||||
manageRelay(url, {
|
|
||||||
method: ManagementMethod.AllowPubkey,
|
|
||||||
params: [pubkey],
|
|
||||||
}),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
|
|
||||||
for (const {error} of results) {
|
if (error) {
|
||||||
if (error) {
|
pushToast({theme: "error", message: error})
|
||||||
return pushToast({theme: "error", message: error})
|
} else {
|
||||||
}
|
pushToast({message: "Members have successfully been added!"})
|
||||||
|
back()
|
||||||
}
|
}
|
||||||
|
|
||||||
const errors = await Promise.all(
|
|
||||||
pubkeys.map(pubkey => waitForThunkError(addRoomMember(url, $room, pubkey))),
|
|
||||||
)
|
|
||||||
|
|
||||||
for (const error of errors) {
|
|
||||||
if (error) {
|
|
||||||
return pushToast({theme: "error", message: errors[0]})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pushToast({message: "Members have successfully been added!"})
|
|
||||||
back()
|
|
||||||
} finally {
|
} finally {
|
||||||
loading = false
|
loading = false
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,6 +12,6 @@
|
|||||||
const room = deriveRoom(url, h)
|
const room = deriveRoom(url, h)
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<span class="ellipsize {props.class}">
|
<span class="ellipsize min-w-0 {props.class}">
|
||||||
{$room?.name || h}
|
{$room?.name || h}
|
||||||
</span>
|
</span>
|
||||||
|
|||||||
@@ -0,0 +1,58 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import AltArrowLeft from "@assets/icons/alt-arrow-left.svg?dataurl"
|
||||||
|
import Button from "@lib/components/Button.svelte"
|
||||||
|
import Icon from "@lib/components/Icon.svelte"
|
||||||
|
import Modal from "@lib/components/Modal.svelte"
|
||||||
|
import ModalBody from "@lib/components/ModalBody.svelte"
|
||||||
|
import ModalHeader from "@lib/components/ModalHeader.svelte"
|
||||||
|
import ModalTitle from "@lib/components/ModalTitle.svelte"
|
||||||
|
import ModalSubtitle from "@lib/components/ModalSubtitle.svelte"
|
||||||
|
import ModalFooter from "@lib/components/ModalFooter.svelte"
|
||||||
|
import ReportItem from "@app/components/ReportItem.svelte"
|
||||||
|
import RoomJoinItem from "@app/components/RoomJoinItem.svelte"
|
||||||
|
import RelayName from "@app/components/RelayName.svelte"
|
||||||
|
import {REPORT} from "@welshman/util"
|
||||||
|
import {deriveSpaceActionItems} from "@app/core/state"
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
url: string
|
||||||
|
}
|
||||||
|
|
||||||
|
const {url}: Props = $props()
|
||||||
|
|
||||||
|
const actionItems = deriveSpaceActionItems(url)
|
||||||
|
|
||||||
|
const back = () => history.back()
|
||||||
|
|
||||||
|
const onResolved = () => {
|
||||||
|
if ($actionItems.length === 0) {
|
||||||
|
back()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<Modal>
|
||||||
|
<ModalBody>
|
||||||
|
<ModalHeader>
|
||||||
|
<ModalTitle>Action Items</ModalTitle>
|
||||||
|
<ModalSubtitle>on <RelayName {url} class="text-primary" /></ModalSubtitle>
|
||||||
|
</ModalHeader>
|
||||||
|
<div class="flex flex-col gap-2">
|
||||||
|
{#each $actionItems as event (event.id)}
|
||||||
|
{#if event.kind === REPORT}
|
||||||
|
<ReportItem {url} {event} {onResolved} />
|
||||||
|
{:else}
|
||||||
|
<RoomJoinItem {url} {event} {onResolved} />
|
||||||
|
{/if}
|
||||||
|
{:else}
|
||||||
|
<p class="py-12 text-center">No action items found.</p>
|
||||||
|
{/each}
|
||||||
|
</div>
|
||||||
|
</ModalBody>
|
||||||
|
<ModalFooter>
|
||||||
|
<Button class="btn btn-link" onclick={back}>
|
||||||
|
<Icon icon={AltArrowLeft} />
|
||||||
|
Go back
|
||||||
|
</Button>
|
||||||
|
</ModalFooter>
|
||||||
|
</Modal>
|
||||||
@@ -1,7 +1,6 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import Login from "@assets/icons/login-3.svg?dataurl"
|
import Login from "@assets/icons/login-3.svg?dataurl"
|
||||||
import AddCircle from "@assets/icons/add-circle.svg?dataurl"
|
import AddCircle from "@assets/icons/add-circle.svg?dataurl"
|
||||||
import Compass from "@assets/icons/compass.svg?dataurl"
|
|
||||||
import Icon from "@lib/components/Icon.svelte"
|
import Icon from "@lib/components/Icon.svelte"
|
||||||
import Link from "@lib/components/Link.svelte"
|
import Link from "@lib/components/Link.svelte"
|
||||||
import Button from "@lib/components/Button.svelte"
|
import Button from "@lib/components/Button.svelte"
|
||||||
@@ -14,12 +13,6 @@
|
|||||||
import SpaceInviteAccept from "@app/components/SpaceInviteAccept.svelte"
|
import SpaceInviteAccept from "@app/components/SpaceInviteAccept.svelte"
|
||||||
import {pushModal} from "@app/util/modal"
|
import {pushModal} from "@app/util/modal"
|
||||||
|
|
||||||
type Props = {
|
|
||||||
hideDiscover?: boolean
|
|
||||||
}
|
|
||||||
|
|
||||||
const {hideDiscover}: Props = $props()
|
|
||||||
|
|
||||||
const startJoin = () => pushModal(SpaceInviteAccept)
|
const startJoin = () => pushModal(SpaceInviteAccept)
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
@@ -30,23 +23,8 @@
|
|||||||
<ModalSubtitle
|
<ModalSubtitle
|
||||||
>Spaces are places where communities come together to work, play, and hang out.</ModalSubtitle>
|
>Spaces are places where communities come together to work, play, and hang out.</ModalSubtitle>
|
||||||
</ModalHeader>
|
</ModalHeader>
|
||||||
{#if !hideDiscover}
|
|
||||||
<Link href="/discover">
|
|
||||||
<CardButton class="btn-primary">
|
|
||||||
{#snippet icon()}
|
|
||||||
<div><Icon icon={Compass} size={7} /></div>
|
|
||||||
{/snippet}
|
|
||||||
{#snippet title()}
|
|
||||||
<div>Explore Spaces</div>
|
|
||||||
{/snippet}
|
|
||||||
{#snippet info()}
|
|
||||||
<div>Join create, or browse spaces</div>
|
|
||||||
{/snippet}
|
|
||||||
</CardButton>
|
|
||||||
</Link>
|
|
||||||
{/if}
|
|
||||||
<Button onclick={startJoin}>
|
<Button onclick={startJoin}>
|
||||||
<CardButton class={hideDiscover ? "btn-primary" : "btn-neutral"}>
|
<CardButton class="btn-primary">
|
||||||
{#snippet icon()}
|
{#snippet icon()}
|
||||||
<div><Icon icon={Login} size={7} /></div>
|
<div><Icon icon={Login} size={7} /></div>
|
||||||
{/snippet}
|
{/snippet}
|
||||||
|
|||||||
@@ -0,0 +1,44 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import type {Snippet} from "svelte"
|
||||||
|
import {page} from "$app/stores"
|
||||||
|
import {goto} from "$app/navigation"
|
||||||
|
import {displayRelayUrl} from "@welshman/util"
|
||||||
|
import ArrowLeft from "@assets/icons/arrow-left.svg?dataurl"
|
||||||
|
import Icon from "@lib/components/Icon.svelte"
|
||||||
|
import Button from "@lib/components/Button.svelte"
|
||||||
|
import PageBar from "@lib/components/PageBar.svelte"
|
||||||
|
import {decodeRelay} from "@app/core/state"
|
||||||
|
import {makeSpacePath} from "@app/util/routes"
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
back?: () => unknown
|
||||||
|
title?: Snippet
|
||||||
|
action?: Snippet
|
||||||
|
[key: string]: any
|
||||||
|
}
|
||||||
|
|
||||||
|
const {back = () => goto(makeSpacePath(url)), title, action, ...props}: Props = $props()
|
||||||
|
|
||||||
|
const url = decodeRelay($page.params.relay!)
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<PageBar {...props}>
|
||||||
|
<div class="flex">
|
||||||
|
<Button onclick={back} class="place-self-start pr-3 md:hidden">
|
||||||
|
<Icon icon={ArrowLeft} size={7} />
|
||||||
|
</Button>
|
||||||
|
<div class="ellipsize whitespace-nowrap flex flex-grow items-center justify-between gap-4">
|
||||||
|
<div class="flex flex-col">
|
||||||
|
<div class="flex gap-2 items-center">
|
||||||
|
{@render title?.()}
|
||||||
|
</div>
|
||||||
|
<div class="text-xs text-primary md:hidden">
|
||||||
|
{displayRelayUrl(url)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="flex gap-2 items-start">
|
||||||
|
{@render action?.()}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</PageBar>
|
||||||
@@ -3,7 +3,7 @@
|
|||||||
import {displayRelayUrl, ManagementMethod} from "@welshman/util"
|
import {displayRelayUrl, ManagementMethod} from "@welshman/util"
|
||||||
import {manageRelay, forceLoadRelay} from "@welshman/app"
|
import {manageRelay, forceLoadRelay} from "@welshman/app"
|
||||||
import StickerSmileSquare from "@assets/icons/sticker-smile-square.svg?dataurl"
|
import StickerSmileSquare from "@assets/icons/sticker-smile-square.svg?dataurl"
|
||||||
import SettingsMinimalistic from "@assets/icons/settings-minimalistic.svg?dataurl"
|
import Widget from "@assets/icons/widget-4.svg?dataurl"
|
||||||
import AltArrowLeft from "@assets/icons/alt-arrow-left.svg?dataurl"
|
import AltArrowLeft from "@assets/icons/alt-arrow-left.svg?dataurl"
|
||||||
import UploadMinimalistic from "@assets/icons/upload-minimalistic.svg?dataurl"
|
import UploadMinimalistic from "@assets/icons/upload-minimalistic.svg?dataurl"
|
||||||
import {preventDefault} from "@lib/html"
|
import {preventDefault} from "@lib/html"
|
||||||
@@ -164,7 +164,7 @@
|
|||||||
{#if imagePreview}
|
{#if imagePreview}
|
||||||
<ImageIcon src={imagePreview} alt="" />
|
<ImageIcon src={imagePreview} alt="" />
|
||||||
{:else}
|
{:else}
|
||||||
<Icon icon={SettingsMinimalistic} />
|
<Icon icon={Widget} />
|
||||||
{/if}
|
{/if}
|
||||||
<input bind:value={values.name} class="grow" type="text" />
|
<input bind:value={values.name} class="grow" type="text" />
|
||||||
</label>
|
</label>
|
||||||
|
|||||||
@@ -60,7 +60,7 @@
|
|||||||
} else {
|
} else {
|
||||||
const permissions = await Push.request()
|
const permissions = await Push.request()
|
||||||
|
|
||||||
if (permissions === "granted") {
|
if (permissions.startsWith("granted")) {
|
||||||
await setSpaceNotifications(url, true)
|
await setSpaceNotifications(url, true)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -48,7 +48,7 @@
|
|||||||
} else {
|
} else {
|
||||||
const permissions = await Push.request()
|
const permissions = await Push.request()
|
||||||
|
|
||||||
if (permissions === "granted") {
|
if (permissions.startsWith("granted")) {
|
||||||
await setSpaceNotifications(url, true)
|
await setSpaceNotifications(url, true)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -159,9 +159,11 @@
|
|||||||
<Icon icon={AltArrowLeft} />
|
<Icon icon={AltArrowLeft} />
|
||||||
Go back
|
Go back
|
||||||
</Button>
|
</Button>
|
||||||
<Button class="btn btn-primary" onclick={addMember}>
|
{#if $userIsAdmin}
|
||||||
<Icon icon={AddCircle} />
|
<Button class="btn btn-primary" onclick={addMember}>
|
||||||
Add members
|
<Icon icon={AddCircle} />
|
||||||
</Button>
|
Add members
|
||||||
|
</Button>
|
||||||
|
{/if}
|
||||||
</ModalFooter>
|
</ModalFooter>
|
||||||
</Modal>
|
</Modal>
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import {displayRelayUrl, ManagementMethod} from "@welshman/util"
|
import {displayRelayUrl} from "@welshman/util"
|
||||||
import {manageRelay} from "@welshman/app"
|
|
||||||
import AltArrowLeft from "@assets/icons/alt-arrow-left.svg?dataurl"
|
import AltArrowLeft from "@assets/icons/alt-arrow-left.svg?dataurl"
|
||||||
import Spinner from "@lib/components/Spinner.svelte"
|
import Spinner from "@lib/components/Spinner.svelte"
|
||||||
import Button from "@lib/components/Button.svelte"
|
import Button from "@lib/components/Button.svelte"
|
||||||
@@ -13,6 +12,7 @@
|
|||||||
import ModalSubtitle from "@lib/components/ModalSubtitle.svelte"
|
import ModalSubtitle from "@lib/components/ModalSubtitle.svelte"
|
||||||
import ModalFooter from "@lib/components/ModalFooter.svelte"
|
import ModalFooter from "@lib/components/ModalFooter.svelte"
|
||||||
import ProfileMultiSelect from "@app/components/ProfileMultiSelect.svelte"
|
import ProfileMultiSelect from "@app/components/ProfileMultiSelect.svelte"
|
||||||
|
import {addSpaceMembers} from "@app/core/commands"
|
||||||
import {pushToast} from "@app/util/toast"
|
import {pushToast} from "@app/util/toast"
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
@@ -27,23 +27,14 @@
|
|||||||
loading = true
|
loading = true
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const results = await Promise.all(
|
const error = await addSpaceMembers(url, pubkeys)
|
||||||
pubkeys.map(pubkey =>
|
|
||||||
manageRelay(url, {
|
|
||||||
method: ManagementMethod.AllowPubkey,
|
|
||||||
params: [pubkey],
|
|
||||||
}),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
|
|
||||||
for (const {error} of results) {
|
if (error) {
|
||||||
if (error) {
|
pushToast({theme: "error", message: error})
|
||||||
return pushToast({theme: "error", message: error})
|
} else {
|
||||||
}
|
pushToast({message: "Members have successfully been added!"})
|
||||||
|
back()
|
||||||
}
|
}
|
||||||
|
|
||||||
pushToast({message: "Members have successfully been added!"})
|
|
||||||
back()
|
|
||||||
} finally {
|
} finally {
|
||||||
loading = false
|
loading = false
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -17,6 +17,7 @@
|
|||||||
import ModalFooter from "@lib/components/ModalFooter.svelte"
|
import ModalFooter from "@lib/components/ModalFooter.svelte"
|
||||||
import Profile from "@app/components/Profile.svelte"
|
import Profile from "@app/components/Profile.svelte"
|
||||||
import {deriveSpaceBannedPubkeyItems, deriveSupportedMethods} from "@app/core/state"
|
import {deriveSpaceBannedPubkeyItems, deriveSupportedMethods} from "@app/core/state"
|
||||||
|
import {addSpaceMembers} from "@app/core/commands"
|
||||||
import {pushToast} from "@app/util/toast"
|
import {pushToast} from "@app/util/toast"
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
@@ -55,10 +56,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
const restoreMember = async (pubkey: string) => {
|
const restoreMember = async (pubkey: string) => {
|
||||||
const {error} = await manageRelay(url, {
|
const error = await addSpaceMembers(url, [pubkey])
|
||||||
method: ManagementMethod.AllowPubkey,
|
|
||||||
params: [pubkey],
|
|
||||||
})
|
|
||||||
|
|
||||||
if (error) {
|
if (error) {
|
||||||
pushToast({theme: "error", message: error})
|
pushToast({theme: "error", message: error})
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import {onMount} from "svelte"
|
|
||||||
import {derived} from "svelte/store"
|
import {derived} from "svelte/store"
|
||||||
import {displayRelayUrl, EVENT_TIME, ZAP_GOAL, THREAD, CLASSIFIED, REPORT} from "@welshman/util"
|
import {displayRelayUrl, EVENT_TIME, ZAP_GOAL, THREAD, CLASSIFIED} from "@welshman/util"
|
||||||
import {deriveRelay, createSearch, pubkey} from "@welshman/app"
|
import {deriveRelay, createSearch, pubkey} from "@welshman/app"
|
||||||
import {fly} from "@lib/transition"
|
import {fly} from "@lib/transition"
|
||||||
import Magnifier from "@assets/icons/magnifier.svg?dataurl"
|
import Magnifier from "@assets/icons/magnifier.svg?dataurl"
|
||||||
@@ -20,8 +19,8 @@
|
|||||||
import CaseMinimalistic from "@assets/icons/case-minimalistic.svg?dataurl"
|
import CaseMinimalistic from "@assets/icons/case-minimalistic.svg?dataurl"
|
||||||
import AddCircle from "@assets/icons/add-circle.svg?dataurl"
|
import AddCircle from "@assets/icons/add-circle.svg?dataurl"
|
||||||
import ChatRound from "@assets/icons/chat-round.svg?dataurl"
|
import ChatRound from "@assets/icons/chat-round.svg?dataurl"
|
||||||
import VolumeLoud from "@assets/icons/volume-loud.svg?dataurl"
|
import Bell from "@assets/icons/bell.svg?dataurl"
|
||||||
import VolumeCross from "@assets/icons/volume-cross.svg?dataurl"
|
import BellOff from "@assets/icons/bell-off.svg?dataurl"
|
||||||
import Icon from "@lib/components/Icon.svelte"
|
import Icon from "@lib/components/Icon.svelte"
|
||||||
import Link from "@lib/components/Link.svelte"
|
import Link from "@lib/components/Link.svelte"
|
||||||
import Button from "@lib/components/Button.svelte"
|
import Button from "@lib/components/Button.svelte"
|
||||||
@@ -35,9 +34,10 @@
|
|||||||
import SpaceJoin from "@app/components/SpaceJoin.svelte"
|
import SpaceJoin from "@app/components/SpaceJoin.svelte"
|
||||||
import RelayName from "@app/components/RelayName.svelte"
|
import RelayName from "@app/components/RelayName.svelte"
|
||||||
import SpaceMembers from "@app/components/SpaceMembers.svelte"
|
import SpaceMembers from "@app/components/SpaceMembers.svelte"
|
||||||
import SpaceReports from "@app/components/SpaceReports.svelte"
|
import SpaceActionItems from "@app/components/SpaceActionItems.svelte"
|
||||||
import RoomCreate from "@app/components/RoomCreate.svelte"
|
import RoomCreate from "@app/components/RoomCreate.svelte"
|
||||||
import SpaceMenuRoomItem from "@app/components/SpaceMenuRoomItem.svelte"
|
import SpaceMenuRoomItem from "@app/components/SpaceMenuRoomItem.svelte"
|
||||||
|
import VoiceWidget from "@app/components/VoiceWidget.svelte"
|
||||||
import SocketStatusIndicator from "@app/components/SocketStatusIndicator.svelte"
|
import SocketStatusIndicator from "@app/components/SocketStatusIndicator.svelte"
|
||||||
import {
|
import {
|
||||||
ENABLE_ZAPS,
|
ENABLE_ZAPS,
|
||||||
@@ -45,18 +45,21 @@
|
|||||||
deriveSpaceMembers,
|
deriveSpaceMembers,
|
||||||
deriveUserRooms,
|
deriveUserRooms,
|
||||||
deriveOtherRooms,
|
deriveOtherRooms,
|
||||||
|
deriveOtherVoiceRooms,
|
||||||
userSpaceUrls,
|
userSpaceUrls,
|
||||||
hasNip29,
|
hasNip29,
|
||||||
deriveUserCanCreateRoom,
|
deriveUserCanCreateRoom,
|
||||||
deriveUserIsSpaceAdmin,
|
deriveUserIsSpaceAdmin,
|
||||||
deriveEventsForUrl,
|
deriveEventsForUrl,
|
||||||
|
deriveSpaceActionItems,
|
||||||
notificationSettings,
|
notificationSettings,
|
||||||
deriveShouldNotify,
|
deriveShouldNotify,
|
||||||
displayRoom,
|
displayRoom,
|
||||||
} from "@app/core/state"
|
} from "@app/core/state"
|
||||||
import {setSpaceNotifications} from "@app/core/commands"
|
import {setSpaceNotifications} from "@app/core/commands"
|
||||||
import {pushModal} from "@app/util/modal"
|
import {pushModal} from "@app/util/modal"
|
||||||
import {makeSpacePath, makeChatPath} from "@app/util/routes"
|
import {makeSpacePath, goToChat} from "@app/util/routes"
|
||||||
|
import {notifications} from "@app/util/notifications"
|
||||||
|
|
||||||
const {url} = $props()
|
const {url} = $props()
|
||||||
|
|
||||||
@@ -68,9 +71,10 @@
|
|||||||
const calendarPath = makeSpacePath(url, "calendar")
|
const calendarPath = makeSpacePath(url, "calendar")
|
||||||
const userRooms = deriveUserRooms(url)
|
const userRooms = deriveUserRooms(url)
|
||||||
const otherRooms = deriveOtherRooms(url)
|
const otherRooms = deriveOtherRooms(url)
|
||||||
|
const otherVoiceRooms = deriveOtherVoiceRooms(url)
|
||||||
const members = deriveSpaceMembers(url)
|
const members = deriveSpaceMembers(url)
|
||||||
const userIsAdmin = deriveUserIsSpaceAdmin(url)
|
const userIsAdmin = deriveUserIsSpaceAdmin(url)
|
||||||
const reports = deriveEventsForUrl(url, [{kinds: [REPORT]}])
|
const actionItems = deriveSpaceActionItems(url)
|
||||||
|
|
||||||
const spaceKinds = derived(
|
const spaceKinds = derived(
|
||||||
deriveEventsForUrl(url, [{kinds: CONTENT_KINDS}]),
|
deriveEventsForUrl(url, [{kinds: CONTENT_KINDS}]),
|
||||||
@@ -95,21 +99,23 @@
|
|||||||
showMenu = !showMenu
|
showMenu = !showMenu
|
||||||
}
|
}
|
||||||
|
|
||||||
const showDetail = () => pushModal(SpaceDetail, {url}, {replaceState})
|
const showDetail = () => pushModal(SpaceDetail, {url})
|
||||||
|
|
||||||
const showMembers = () => pushModal(SpaceMembers, {url}, {replaceState})
|
const showMembers = () => pushModal(SpaceMembers, {url})
|
||||||
|
|
||||||
const showReports = () => pushModal(SpaceReports, {url}, {replaceState})
|
const showActionItems = () => pushModal(SpaceActionItems, {url})
|
||||||
|
|
||||||
const canCreateRoom = deriveUserCanCreateRoom(url)
|
const canCreateRoom = deriveUserCanCreateRoom(url)
|
||||||
|
|
||||||
const createInvite = () => pushModal(SpaceInvite, {url}, {replaceState})
|
const createInvite = () => pushModal(SpaceInvite, {url})
|
||||||
|
|
||||||
const leaveSpace = () => pushModal(SpaceExit, {url}, {replaceState})
|
const leaveSpace = () => pushModal(SpaceExit, {url})
|
||||||
|
|
||||||
const joinSpace = () => pushModal(SpaceJoin, {url}, {replaceState})
|
const joinSpace = () => pushModal(SpaceJoin, {url})
|
||||||
|
|
||||||
const addRoom = () => pushModal(RoomCreate, {url}, {replaceState})
|
const addRoom = () => pushModal(RoomCreate, {url})
|
||||||
|
|
||||||
|
const contactOwner = () => goToChat([$relay!.pubkey!])
|
||||||
|
|
||||||
const shouldNotify = deriveShouldNotify(url)
|
const shouldNotify = deriveShouldNotify(url)
|
||||||
|
|
||||||
@@ -125,25 +131,24 @@
|
|||||||
|
|
||||||
let term = $state("")
|
let term = $state("")
|
||||||
let showMenu = $state(false)
|
let showMenu = $state(false)
|
||||||
let replaceState = $state(false)
|
|
||||||
let element: Element | undefined = $state()
|
let element: Element | undefined = $state()
|
||||||
|
|
||||||
onMount(() => {
|
|
||||||
replaceState = Boolean(element?.closest(".drawer"))
|
|
||||||
})
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div bind:this={element} class="flex h-full flex-col justify-between">
|
<div bind:this={element} class="flex min-h-0 flex-1 flex-col">
|
||||||
<SecondaryNavSection class="pb-0">
|
<SecondaryNavSection class="min-h-0 flex-1 flex flex-col overflow-hidden pb-0">
|
||||||
<div>
|
<div class="flex-shrink-0">
|
||||||
<Button
|
<Button
|
||||||
class="flex w-full flex-col rounded-xl p-3 transition-all hover:bg-base-100"
|
class="relative flex w-full flex-col rounded-xl p-3 transition-all hover:bg-base-100"
|
||||||
onclick={openMenu}>
|
onclick={openMenu}>
|
||||||
<div class="flex items-center justify-between">
|
<div class="flex items-center justify-between">
|
||||||
<strong class="ellipsize flex items-center gap-1">
|
<strong class="flex items-center gap-1 relative">
|
||||||
<RelayName {url} />
|
<RelayName {url} class="ellipsize" />
|
||||||
|
<div
|
||||||
|
class="absolute -right-3 top-0 h-2 w-2 rounded-full bg-primary transition-all opacity-0"
|
||||||
|
class:opacity-100={$userIsAdmin && $actionItems.length > 0}>
|
||||||
|
</div>
|
||||||
{#if $notificationSettings.push && !$shouldNotify}
|
{#if $notificationSettings.push && !$shouldNotify}
|
||||||
<Icon icon={VolumeCross} size={3} class="opacity-50" />
|
<Icon icon={BellOff} size={3} class="opacity-50" />
|
||||||
{/if}
|
{/if}
|
||||||
</strong>
|
</strong>
|
||||||
<Icon icon={AltArrowDown} />
|
<Icon icon={AltArrowDown} />
|
||||||
@@ -175,29 +180,32 @@
|
|||||||
</li>
|
</li>
|
||||||
{#if $userIsAdmin}
|
{#if $userIsAdmin}
|
||||||
<li>
|
<li>
|
||||||
<Button onclick={showReports}>
|
<Button onclick={showActionItems}>
|
||||||
<Icon icon={Danger} />
|
<Icon icon={Danger} />
|
||||||
View Reports ({$reports.length})
|
Action Items ({$actionItems.length})
|
||||||
|
{#if $actionItems.length > 0}
|
||||||
|
<div class="h-2 w-2 rounded-full bg-primary"></div>
|
||||||
|
{/if}
|
||||||
</Button>
|
</Button>
|
||||||
</li>
|
</li>
|
||||||
{/if}
|
{/if}
|
||||||
{#if $relay?.pubkey && $relay.pubkey !== $pubkey}
|
{#if $relay?.pubkey && $relay.pubkey !== $pubkey}
|
||||||
<li>
|
<li>
|
||||||
<Link href={makeChatPath([$relay.pubkey])}>
|
<Button onclick={contactOwner}>
|
||||||
<Icon icon={Letter} />
|
<Icon icon={Letter} />
|
||||||
Contact Owner
|
Contact Owner
|
||||||
</Link>
|
</Button>
|
||||||
</li>
|
</li>
|
||||||
{/if}
|
{/if}
|
||||||
<li>
|
<li>
|
||||||
{#if $notificationSettings.push}
|
{#if $notificationSettings.push}
|
||||||
<Button onclick={toggleSpaceNotifications}>
|
<Button onclick={toggleSpaceNotifications}>
|
||||||
<Icon icon={$shouldNotify ? VolumeLoud : VolumeCross} />
|
<Icon icon={$shouldNotify ? Bell : BellOff} />
|
||||||
{$shouldNotify ? "Turn off" : "Turn on"} notifications
|
{$shouldNotify ? "Turn off" : "Turn on"} notifications
|
||||||
</Button>
|
</Button>
|
||||||
{:else}
|
{:else}
|
||||||
<Link href="/settings/alerts">
|
<Link href="/settings/alerts">
|
||||||
<Icon icon={VolumeLoud} />
|
<Icon icon={Bell} />
|
||||||
Enable notifications
|
Enable notifications
|
||||||
</Link>
|
</Link>
|
||||||
{/if}
|
{/if}
|
||||||
@@ -219,47 +227,46 @@
|
|||||||
</Popover>
|
</Popover>
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div class="flex min-h-0 flex-1 flex-col gap-1 overflow-auto overflow-x-hidden">
|
||||||
class="flex max-h-[calc(100vh-150px)] min-h-0 flex-col gap-1 overflow-auto overflow-x-hidden">
|
|
||||||
{#if hasNip29($relay)}
|
{#if hasNip29($relay)}
|
||||||
<SecondaryNavItem {replaceState} href={makeSpacePath(url, "recent")}>
|
<SecondaryNavItem href={makeSpacePath(url, "recent")}>
|
||||||
<Icon icon={History} /> Recent Activity
|
<Icon icon={History} /> Recent Activity
|
||||||
</SecondaryNavItem>
|
</SecondaryNavItem>
|
||||||
{:else}
|
{:else}
|
||||||
<SecondaryNavItem {replaceState} href={chatPath}>
|
<SecondaryNavItem href={chatPath} notification={$notifications.has(chatPath)}>
|
||||||
<Icon icon={ChatRound} /> Chat
|
<Icon icon={ChatRound} /> Chat
|
||||||
</SecondaryNavItem>
|
</SecondaryNavItem>
|
||||||
{/if}
|
{/if}
|
||||||
{#if ENABLE_ZAPS && $spaceKinds.has(ZAP_GOAL)}
|
{#if ENABLE_ZAPS && $spaceKinds.has(ZAP_GOAL)}
|
||||||
<SecondaryNavItem {replaceState} href={goalsPath}>
|
<SecondaryNavItem href={goalsPath}>
|
||||||
<Icon icon={StarFallMinimalistic} /> Goals
|
<Icon icon={StarFallMinimalistic} /> Goals
|
||||||
</SecondaryNavItem>
|
</SecondaryNavItem>
|
||||||
{/if}
|
{/if}
|
||||||
{#if $spaceKinds.has(THREAD)}
|
{#if $spaceKinds.has(THREAD)}
|
||||||
<SecondaryNavItem {replaceState} href={threadsPath}>
|
<SecondaryNavItem href={threadsPath}>
|
||||||
<Icon icon={NotesMinimalistic} /> Threads
|
<Icon icon={NotesMinimalistic} /> Threads
|
||||||
</SecondaryNavItem>
|
</SecondaryNavItem>
|
||||||
{/if}
|
{/if}
|
||||||
{#if $spaceKinds.has(CLASSIFIED)}
|
{#if $spaceKinds.has(CLASSIFIED)}
|
||||||
<SecondaryNavItem {replaceState} href={classifiedsPath}>
|
<SecondaryNavItem href={classifiedsPath}>
|
||||||
<Icon icon={CaseMinimalistic} /> Classifieds
|
<Icon icon={CaseMinimalistic} /> Classifieds
|
||||||
</SecondaryNavItem>
|
</SecondaryNavItem>
|
||||||
{/if}
|
{/if}
|
||||||
{#if $spaceKinds.has(EVENT_TIME)}
|
{#if $spaceKinds.has(EVENT_TIME)}
|
||||||
<SecondaryNavItem {replaceState} href={calendarPath}>
|
<SecondaryNavItem href={calendarPath}>
|
||||||
<Icon icon={CalendarMinimalistic} /> Calendar
|
<Icon icon={CalendarMinimalistic} /> Calendar
|
||||||
</SecondaryNavItem>
|
</SecondaryNavItem>
|
||||||
{/if}
|
{/if}
|
||||||
{#if hasNip29($relay)}
|
{#if hasNip29($relay)}
|
||||||
{#if $userRooms.length > 0}
|
{#if $userRooms.length > 0}
|
||||||
<div class="h-2"></div>
|
<div class="h-2 flex-shrink-0"></div>
|
||||||
<SecondaryNavHeader>Your Rooms</SecondaryNavHeader>
|
<SecondaryNavHeader>Your Rooms</SecondaryNavHeader>
|
||||||
{/if}
|
{/if}
|
||||||
{#each $userRooms as h, i (h)}
|
{#each $userRooms as h (h)}
|
||||||
<SpaceMenuRoomItem notify {replaceState} {url} {h} />
|
<SpaceMenuRoomItem {url} {h} />
|
||||||
{/each}
|
{/each}
|
||||||
{#if $otherRooms.length > 0}
|
{#if $otherRooms.length > 0}
|
||||||
<div class="h-2"></div>
|
<div class="h-2 flex-shrink-0"></div>
|
||||||
<SecondaryNavHeader>
|
<SecondaryNavHeader>
|
||||||
{#if $userRooms.length > 0}
|
{#if $userRooms.length > 0}
|
||||||
Other Rooms
|
Other Rooms
|
||||||
@@ -274,19 +281,29 @@
|
|||||||
<input bind:value={term} onblur={clearTerm} class="grow" />
|
<input bind:value={term} onblur={clearTerm} class="grow" />
|
||||||
</label>
|
</label>
|
||||||
{/if}
|
{/if}
|
||||||
{#each $roomSearch.searchValues(term) as h, i (h)}
|
{#each $roomSearch.searchValues(term) as h (h)}
|
||||||
<SpaceMenuRoomItem {replaceState} {url} {h} />
|
<SpaceMenuRoomItem {url} {h} />
|
||||||
{/each}
|
{/each}
|
||||||
|
{#if $otherVoiceRooms.length > 0}
|
||||||
|
<div class="h-2 flex-shrink-0"></div>
|
||||||
|
<SecondaryNavHeader>Voice Rooms</SecondaryNavHeader>
|
||||||
|
{#each $otherVoiceRooms as h (h)}
|
||||||
|
<SpaceMenuRoomItem {url} {h} />
|
||||||
|
{/each}
|
||||||
|
{/if}
|
||||||
{#if $canCreateRoom}
|
{#if $canCreateRoom}
|
||||||
<SecondaryNavItem {replaceState} onclick={addRoom}>
|
<SecondaryNavItem onclick={addRoom}>
|
||||||
<Icon icon={AddCircle} />
|
<Icon icon={AddCircle} />
|
||||||
Create room
|
Create room
|
||||||
</SecondaryNavItem>
|
</SecondaryNavItem>
|
||||||
{/if}
|
{/if}
|
||||||
{/if}
|
{/if}
|
||||||
|
<div class="h-5 flex-shrink-0"></div>
|
||||||
</div>
|
</div>
|
||||||
</SecondaryNavSection>
|
</SecondaryNavSection>
|
||||||
<div class="flex flex-col gap-2 pb-2 p-4 pt-0">
|
<div
|
||||||
|
class="flex flex-shrink-0 flex-col gap-2 p-2 pt-0 -mt-4 pb-[calc(var(--saib)+0.25rem)] md:pb-2 z-nav">
|
||||||
|
<VoiceWidget />
|
||||||
<Button class="btn btn-neutral btn-sm h-10" onclick={showDetail}>
|
<Button class="btn btn-neutral btn-sm h-10" onclick={showDetail}>
|
||||||
<SocketStatusIndicator {url} />
|
<SocketStatusIndicator {url} />
|
||||||
</Button>
|
</Button>
|
||||||
|
|||||||
@@ -1,27 +0,0 @@
|
|||||||
<script lang="ts">
|
|
||||||
import MenuDots from "@assets/icons/menu-dots.svg?dataurl"
|
|
||||||
import Icon from "@lib/components/Icon.svelte"
|
|
||||||
import Button from "@lib/components/Button.svelte"
|
|
||||||
import SpaceMenu from "@app/components/SpaceMenu.svelte"
|
|
||||||
import {notifications} from "@app/util/notifications"
|
|
||||||
import {makeSpacePath} from "@app/util/routes"
|
|
||||||
import {pushDrawer} from "@app/util/modal"
|
|
||||||
import {deriveSocketStatus} from "@app/core/state"
|
|
||||||
|
|
||||||
const {url} = $props()
|
|
||||||
|
|
||||||
const path = makeSpacePath(url) + ":mobile"
|
|
||||||
|
|
||||||
const status = deriveSocketStatus(url)
|
|
||||||
|
|
||||||
const openMenu = () => pushDrawer(SpaceMenu, {url})
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<Button onclick={openMenu} class="btn btn-neutral btn-sm relative md:hidden btn-square">
|
|
||||||
<Icon icon={MenuDots} />
|
|
||||||
{#if $status.theme !== "success"}
|
|
||||||
<div class="absolute right-0 top-0 -mr-1 -mt-1 h-2 w-2 rounded-full bg-{$status.theme}"></div>
|
|
||||||
{:else if $notifications.has(path)}
|
|
||||||
<div class="absolute right-0 top-0 -mr-1 -mt-1 h-2 w-2 rounded-full bg-primary"></div>
|
|
||||||
{/if}
|
|
||||||
</Button>
|
|
||||||
@@ -1,34 +1,38 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import VolumeCross from "@assets/icons/volume-cross.svg?dataurl"
|
import Bell from "@assets/icons/bell.svg?dataurl"
|
||||||
import VolumeLoud from "@assets/icons/volume-loud.svg?dataurl"
|
import BellOff from "@assets/icons/bell-off.svg?dataurl"
|
||||||
import Icon from "@lib/components/Icon.svelte"
|
import Icon from "@lib/components/Icon.svelte"
|
||||||
import SecondaryNavItem from "@lib/components/SecondaryNavItem.svelte"
|
import SecondaryNavItem from "@lib/components/SecondaryNavItem.svelte"
|
||||||
import RoomNameWithImage from "@app/components/RoomNameWithImage.svelte"
|
import RoomNameWithImage from "@app/components/RoomNameWithImage.svelte"
|
||||||
|
import VoiceRoomItem from "@app/components/VoiceRoomItem.svelte"
|
||||||
|
import {deriveRoom, deriveShouldNotify, getRoomType, RoomType} from "@app/core/state"
|
||||||
import {notifications} from "@app/util/notifications"
|
import {notifications} from "@app/util/notifications"
|
||||||
import {makeRoomPath} from "@app/util/routes"
|
import {makeRoomPath} from "@app/util/routes"
|
||||||
import {deriveShouldNotify} from "@app/core/state"
|
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
url: any
|
url: any
|
||||||
h: any
|
h: any
|
||||||
notify?: boolean
|
|
||||||
replaceState?: boolean
|
replaceState?: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
const {url, h, notify = false, replaceState = false}: Props = $props()
|
const {url, h, replaceState = false}: Props = $props()
|
||||||
|
|
||||||
|
const room = deriveRoom(url, h)
|
||||||
|
const roomType = $derived(getRoomType($room))
|
||||||
const path = makeRoomPath(url, h)
|
const path = makeRoomPath(url, h)
|
||||||
const shouldNotifyForSpace = deriveShouldNotify(url)
|
const shouldNotifyForSpace = deriveShouldNotify(url)
|
||||||
const shouldNotifyForRoom = deriveShouldNotify(url, h)
|
const shouldNotifyForRoom = deriveShouldNotify(url, h)
|
||||||
const showDifferenceIcon = $derived($shouldNotifyForRoom !== $shouldNotifyForSpace)
|
const showDifferenceIcon = $derived($shouldNotifyForRoom !== $shouldNotifyForSpace)
|
||||||
|
const notification = $derived($shouldNotifyForRoom ? $notifications.has(path) : false)
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<SecondaryNavItem
|
{#if roomType === RoomType.Voice}
|
||||||
href={path}
|
<VoiceRoomItem {url} {h} {replaceState} {notification} />
|
||||||
{replaceState}
|
{:else}
|
||||||
notification={notify ? $notifications.has(path) : false}>
|
<SecondaryNavItem href={path} {replaceState} {notification}>
|
||||||
<RoomNameWithImage {url} {h} />
|
<RoomNameWithImage {url} {h} />
|
||||||
{#if showDifferenceIcon}
|
{#if showDifferenceIcon}
|
||||||
<Icon icon={$shouldNotifyForRoom ? VolumeLoud : VolumeCross} size={4} class="opacity-50" />
|
<Icon icon={$shouldNotifyForRoom ? Bell : BellOff} size={4} class="opacity-50" />
|
||||||
{/if}
|
{/if}
|
||||||
</SecondaryNavItem>
|
</SecondaryNavItem>
|
||||||
|
{/if}
|
||||||
|
|||||||
@@ -23,7 +23,7 @@
|
|||||||
|
|
||||||
const back = () => history.back()
|
const back = () => history.back()
|
||||||
|
|
||||||
const onDelete = () => {
|
const onResolved = () => {
|
||||||
if ($reports.length === 0) {
|
if ($reports.length === 0) {
|
||||||
back()
|
back()
|
||||||
}
|
}
|
||||||
@@ -38,7 +38,7 @@
|
|||||||
</ModalHeader>
|
</ModalHeader>
|
||||||
<div class="flex flex-col gap-2">
|
<div class="flex flex-col gap-2">
|
||||||
{#each $reports as event (event.id)}
|
{#each $reports as event (event.id)}
|
||||||
<ReportItem {url} {event} {onDelete} />
|
<ReportItem {url} {event} {onResolved} />
|
||||||
{:else}
|
{:else}
|
||||||
<p class="py-12 text-center">No reports found.</p>
|
<p class="py-12 text-center">No reports found.</p>
|
||||||
{/each}
|
{/each}
|
||||||
|
|||||||
@@ -0,0 +1,94 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import cx from "classnames"
|
||||||
|
import {goto} from "$app/navigation"
|
||||||
|
import {loadProfile, displayProfileByPubkey} from "@welshman/app"
|
||||||
|
import SecondaryNavItem from "@lib/components/SecondaryNavItem.svelte"
|
||||||
|
import ProfileCircle from "@app/components/ProfileCircle.svelte"
|
||||||
|
import RoomImage from "@app/components/RoomImage.svelte"
|
||||||
|
import RoomName from "@app/components/RoomName.svelte"
|
||||||
|
import {makeRoomPath} from "@app/util/routes"
|
||||||
|
import {pushModal} from "@app/util/modal"
|
||||||
|
import VoiceRoomJoinDialog from "@app/components/VoiceRoomJoinDialog.svelte"
|
||||||
|
import {makeRoomId} from "@app/core/state"
|
||||||
|
import {
|
||||||
|
VoiceState,
|
||||||
|
deriveVoiceParticipants,
|
||||||
|
cancelJoinVoiceRoom,
|
||||||
|
currentVoiceRoom,
|
||||||
|
voiceState,
|
||||||
|
isParticipantSpeaking,
|
||||||
|
participantKey,
|
||||||
|
type VoiceParticipant,
|
||||||
|
} from "@app/voice"
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
url: string
|
||||||
|
h: string
|
||||||
|
replaceState?: boolean
|
||||||
|
notification?: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
const {url, h, replaceState = false, notification = false}: Props = $props()
|
||||||
|
|
||||||
|
const participants = deriveVoiceParticipants(url, h)
|
||||||
|
const isActive = $derived(
|
||||||
|
$voiceState === VoiceState.Connected && $currentVoiceRoom?.id === makeRoomId(url, h),
|
||||||
|
)
|
||||||
|
const isJoining = $derived(
|
||||||
|
$voiceState === VoiceState.Joining && $currentVoiceRoom?.id === makeRoomId(url, h),
|
||||||
|
)
|
||||||
|
|
||||||
|
const handleClick = async (e: MouseEvent) => {
|
||||||
|
if (isActive) return
|
||||||
|
|
||||||
|
if (isJoining) {
|
||||||
|
e.preventDefault()
|
||||||
|
cancelJoinVoiceRoom()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
e.preventDefault()
|
||||||
|
await goto(makeRoomPath(url, h), {replaceState})
|
||||||
|
pushModal(VoiceRoomJoinDialog, {url, h})
|
||||||
|
}
|
||||||
|
|
||||||
|
$effect(() => {
|
||||||
|
for (const p of $participants) {
|
||||||
|
if (p.pubkey) loadProfile(p.pubkey)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<SecondaryNavItem
|
||||||
|
href={makeRoomPath(url, h)}
|
||||||
|
{replaceState}
|
||||||
|
{notification}
|
||||||
|
onclick={handleClick}
|
||||||
|
class={cx("!items-start", isActive && "!bg-base-100 !text-base-content")}>
|
||||||
|
<div class="flex w-full min-w-0 flex-col gap-2">
|
||||||
|
<div class="flex gap-2 items-center">
|
||||||
|
{#if isJoining}
|
||||||
|
<span class="loading loading-spinner loading-sm"></span>
|
||||||
|
{:else}
|
||||||
|
<RoomImage {url} {h} size={4} />
|
||||||
|
{/if}
|
||||||
|
<RoomName {url} {h} />
|
||||||
|
</div>
|
||||||
|
{#if $participants.length > 0}
|
||||||
|
{#each $participants as p (participantKey(p as VoiceParticipant))}
|
||||||
|
<div class="flex items-center gap-2 ml-6">
|
||||||
|
<div
|
||||||
|
class={cx(
|
||||||
|
"inline-flex shrink-0 items-center justify-center rounded-full transition-shadow",
|
||||||
|
isActive && $isParticipantSpeaking(p) && "ring-2 ring-success",
|
||||||
|
)}>
|
||||||
|
<ProfileCircle pubkey={p.pubkey} size={5} class="h-5 w-5" />
|
||||||
|
</div>
|
||||||
|
<span class="ellipsize text-xs opacity-70">
|
||||||
|
{p.pubkey ? displayProfileByPubkey(p.pubkey) : "Unknown"}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
{/each}
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
</SecondaryNavItem>
|
||||||
@@ -0,0 +1,115 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import {displayRelayUrl} from "@welshman/util"
|
||||||
|
import AltArrowLeft from "@assets/icons/alt-arrow-left.svg?dataurl"
|
||||||
|
import AltArrowRight from "@assets/icons/alt-arrow-right.svg?dataurl"
|
||||||
|
import Volume from "@assets/icons/volume.svg?dataurl"
|
||||||
|
import Button from "@lib/components/Button.svelte"
|
||||||
|
import FieldInline from "@lib/components/FieldInline.svelte"
|
||||||
|
import Icon from "@lib/components/Icon.svelte"
|
||||||
|
import Modal from "@lib/components/Modal.svelte"
|
||||||
|
import ModalBody from "@lib/components/ModalBody.svelte"
|
||||||
|
import ModalFooter from "@lib/components/ModalFooter.svelte"
|
||||||
|
import ModalHeader from "@lib/components/ModalHeader.svelte"
|
||||||
|
import ModalSubtitle from "@lib/components/ModalSubtitle.svelte"
|
||||||
|
import ModalTitle from "@lib/components/ModalTitle.svelte"
|
||||||
|
import {displayRoom} from "@app/core/state"
|
||||||
|
import {joinVoiceRoom} from "@app/voice"
|
||||||
|
import {popModal} from "@app/util/modal"
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
url: string
|
||||||
|
h: string
|
||||||
|
}
|
||||||
|
|
||||||
|
const {url, h}: Props = $props()
|
||||||
|
|
||||||
|
const spaceLabel = $derived(displayRelayUrl(url))
|
||||||
|
|
||||||
|
let audioInputs = $state<MediaDeviceInfo[]>([])
|
||||||
|
let selectedDeviceId = $state("")
|
||||||
|
let startWithoutMic = $state(false)
|
||||||
|
|
||||||
|
const loadDevices = async () => {
|
||||||
|
if (!navigator.mediaDevices?.enumerateDevices) return
|
||||||
|
try {
|
||||||
|
const devices = await navigator.mediaDevices.enumerateDevices()
|
||||||
|
audioInputs = devices.filter(d => d.kind === "audioinput")
|
||||||
|
} catch {
|
||||||
|
audioInputs = []
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$effect(() => {
|
||||||
|
void loadDevices()
|
||||||
|
})
|
||||||
|
|
||||||
|
const goBack = () => history.back()
|
||||||
|
|
||||||
|
const joinVoice = async () => {
|
||||||
|
popModal()
|
||||||
|
await joinVoiceRoom(
|
||||||
|
url,
|
||||||
|
h,
|
||||||
|
startWithoutMic,
|
||||||
|
startWithoutMic ? undefined : selectedDeviceId || undefined,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<Modal>
|
||||||
|
<ModalBody>
|
||||||
|
<ModalHeader>
|
||||||
|
<ModalTitle>Join voice room?</ModalTitle>
|
||||||
|
<ModalSubtitle>
|
||||||
|
<span class="inline-flex flex-wrap items-center justify-center gap-x-1.5 gap-y-1">
|
||||||
|
<Icon icon={Volume} size={4} class="shrink-0" />
|
||||||
|
<span class="ellipsize min-w-0">{displayRoom(url, h)}</span>
|
||||||
|
<span>·</span>
|
||||||
|
<span>{spaceLabel}</span>
|
||||||
|
</span>
|
||||||
|
</ModalSubtitle>
|
||||||
|
</ModalHeader>
|
||||||
|
<p class="text-sm opacity-80">Select a microphone to join the call:</p>
|
||||||
|
<div class="flex flex-col gap-4 pt-2">
|
||||||
|
<div class="flex items-center gap-2">
|
||||||
|
<input
|
||||||
|
id="voice-start-without-mic"
|
||||||
|
type="checkbox"
|
||||||
|
class="checkbox"
|
||||||
|
bind:checked={startWithoutMic} />
|
||||||
|
<label for="voice-start-without-mic" class="text-sm cursor-pointer">
|
||||||
|
Join without microphone (you can unmute later)
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
<FieldInline>
|
||||||
|
{#snippet label()}
|
||||||
|
<p>Microphone</p>
|
||||||
|
{/snippet}
|
||||||
|
{#snippet input()}
|
||||||
|
<select
|
||||||
|
class="select select-bordered w-full"
|
||||||
|
bind:value={selectedDeviceId}
|
||||||
|
disabled={startWithoutMic}
|
||||||
|
aria-label="Microphone">
|
||||||
|
<option value="">Default microphone</option>
|
||||||
|
{#each audioInputs as d (d.deviceId)}
|
||||||
|
<option value={d.deviceId}>
|
||||||
|
{d.label || `Microphone ${d.deviceId.slice(0, 8)}…`}
|
||||||
|
</option>
|
||||||
|
{/each}
|
||||||
|
</select>
|
||||||
|
{/snippet}
|
||||||
|
</FieldInline>
|
||||||
|
</div>
|
||||||
|
</ModalBody>
|
||||||
|
<ModalFooter>
|
||||||
|
<Button class="btn btn-link" onclick={goBack}>
|
||||||
|
<Icon icon={AltArrowLeft} />
|
||||||
|
Don't join
|
||||||
|
</Button>
|
||||||
|
<Button class="btn btn-primary" onclick={joinVoice}>
|
||||||
|
Join voice
|
||||||
|
<Icon icon={AltArrowRight} />
|
||||||
|
</Button>
|
||||||
|
</ModalFooter>
|
||||||
|
</Modal>
|
||||||
@@ -0,0 +1,119 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import {readable} from "svelte/store"
|
||||||
|
import {fly} from "svelte/transition"
|
||||||
|
import {goto} from "$app/navigation"
|
||||||
|
import {page} from "$app/stores"
|
||||||
|
import {displayRelayUrl} from "@welshman/util"
|
||||||
|
import Microphone from "@assets/icons/microphone.svg?dataurl"
|
||||||
|
import MicrophoneOff from "@assets/icons/microphone-off.svg?dataurl"
|
||||||
|
import PhoneRounded from "@assets/icons/phone-rounded.svg?dataurl"
|
||||||
|
import PhoneCallingRounded from "@assets/icons/phone-calling-rounded.svg?dataurl"
|
||||||
|
import CloseCircle from "@assets/icons/close-circle.svg?dataurl"
|
||||||
|
import Icon from "@lib/components/Icon.svelte"
|
||||||
|
import Button from "@lib/components/Button.svelte"
|
||||||
|
import VoiceRoomJoinDialog from "@app/components/VoiceRoomJoinDialog.svelte"
|
||||||
|
import {
|
||||||
|
decodeRelay,
|
||||||
|
deriveRoom,
|
||||||
|
displayRoom,
|
||||||
|
getRoomType,
|
||||||
|
RoomType,
|
||||||
|
type Room,
|
||||||
|
} from "@app/core/state"
|
||||||
|
import {pushModal} from "@app/util/modal"
|
||||||
|
import {makeRoomPath} from "@app/util/routes"
|
||||||
|
import {
|
||||||
|
VoiceState,
|
||||||
|
currentVoiceSession,
|
||||||
|
currentVoiceRoom,
|
||||||
|
voiceState,
|
||||||
|
leaveVoiceRoom,
|
||||||
|
toggleMute,
|
||||||
|
cancelJoinVoiceRoom,
|
||||||
|
} from "@app/voice"
|
||||||
|
|
||||||
|
const {relay, h} = $derived($page.params)
|
||||||
|
const url = $derived(relay ? decodeRelay(relay) : undefined)
|
||||||
|
const displayedRoomStore = $derived(
|
||||||
|
url && h && typeof h === "string" ? deriveRoom(url, h) : readable(undefined),
|
||||||
|
)
|
||||||
|
const routeDisplayedRoom = $derived($displayedRoomStore)
|
||||||
|
|
||||||
|
const targetRoom = $derived.by((): Room | undefined => {
|
||||||
|
if ($voiceState === VoiceState.Joining || $voiceState === VoiceState.Connected) {
|
||||||
|
return $currentVoiceRoom
|
||||||
|
}
|
||||||
|
if ($voiceState === VoiceState.Disconnected) {
|
||||||
|
if (routeDisplayedRoom) {
|
||||||
|
if (getRoomType(routeDisplayedRoom) === RoomType.Voice) {
|
||||||
|
return routeDisplayedRoom
|
||||||
|
}
|
||||||
|
return undefined
|
||||||
|
}
|
||||||
|
return $currentVoiceRoom
|
||||||
|
}
|
||||||
|
return $currentVoiceRoom
|
||||||
|
})
|
||||||
|
|
||||||
|
const roomName = $derived(targetRoom ? displayRoom(targetRoom.url, targetRoom.h) : "")
|
||||||
|
const spaceName = $derived(targetRoom ? displayRelayUrl(targetRoom.url) : "")
|
||||||
|
|
||||||
|
const openJoinDialog = async () => {
|
||||||
|
if (!targetRoom) return
|
||||||
|
await goto(makeRoomPath(targetRoom.url, targetRoom.h))
|
||||||
|
pushModal(VoiceRoomJoinDialog, {url: targetRoom.url, h: targetRoom.h})
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
{#if targetRoom}
|
||||||
|
<div
|
||||||
|
in:fly={{y: 60, duration: 350}}
|
||||||
|
out:fly={{y: 60, duration: 250}}
|
||||||
|
class="flex flex-col gap-2 rounded-box bg-base-100 p-3">
|
||||||
|
<div class="flex flex-col gap-0.5">
|
||||||
|
{#if $voiceState === VoiceState.Joining}
|
||||||
|
<span class="text-sm font-semibold text-warning">Joining...</span>
|
||||||
|
{:else if $voiceState === VoiceState.Connected}
|
||||||
|
<span class="text-sm font-semibold text-success">Voice Connected</span>
|
||||||
|
{:else}
|
||||||
|
<span class="text-sm font-semibold text-neutral-content">Disconnected</span>
|
||||||
|
{/if}
|
||||||
|
<span class="ellipsize text-xs opacity-70">
|
||||||
|
{roomName} / {spaceName}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div class="flex items-center gap-1">
|
||||||
|
{#if $voiceState === VoiceState.Joining}
|
||||||
|
<span class="loading loading-spinner loading-sm"></span>
|
||||||
|
<Button
|
||||||
|
data-tip="Cancel"
|
||||||
|
class="center tooltip tooltip-top btn btn-sm btn-square btn-ghost"
|
||||||
|
onclick={cancelJoinVoiceRoom}>
|
||||||
|
<Icon icon={CloseCircle} size={4} />
|
||||||
|
</Button>
|
||||||
|
{:else if $voiceState === VoiceState.Connected && $currentVoiceSession}
|
||||||
|
<Button
|
||||||
|
data-tip={$currentVoiceSession.muted ? "Unmute" : "Mute"}
|
||||||
|
class="center tooltip tooltip-top btn btn-sm btn-square {$currentVoiceSession.muted
|
||||||
|
? 'btn-error'
|
||||||
|
: 'btn-ghost'}"
|
||||||
|
onclick={toggleMute}>
|
||||||
|
<Icon icon={$currentVoiceSession.muted ? MicrophoneOff : Microphone} size={4} />
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
data-tip="Leave room"
|
||||||
|
class="center tooltip tooltip-top btn btn-sm btn-square btn-error"
|
||||||
|
onclick={leaveVoiceRoom}>
|
||||||
|
<Icon icon={PhoneRounded} size={4} />
|
||||||
|
</Button>
|
||||||
|
{:else}
|
||||||
|
<Button
|
||||||
|
data-tip="Join Voice"
|
||||||
|
class="center tooltip tooltip-top btn btn-sm btn-square btn-success"
|
||||||
|
onclick={openJoinDialog}>
|
||||||
|
<Icon icon={PhoneCallingRounded} size={4} />
|
||||||
|
</Button>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
@@ -17,7 +17,7 @@ import {
|
|||||||
} from "@welshman/lib"
|
} from "@welshman/lib"
|
||||||
import {Nip01Signer} from "@welshman/signer"
|
import {Nip01Signer} from "@welshman/signer"
|
||||||
import type {UploadTask} from "@welshman/editor"
|
import type {UploadTask} from "@welshman/editor"
|
||||||
import type {TrustedEvent, EventContent, Profile} from "@welshman/util"
|
import type {TrustedEvent, EventContent, Profile, PublishedRoomMeta} from "@welshman/util"
|
||||||
import {
|
import {
|
||||||
DELETE,
|
DELETE,
|
||||||
REPORT,
|
REPORT,
|
||||||
@@ -52,6 +52,7 @@ import {
|
|||||||
editProfile,
|
editProfile,
|
||||||
createProfile,
|
createProfile,
|
||||||
uniqTags,
|
uniqTags,
|
||||||
|
ManagementMethod,
|
||||||
} from "@welshman/util"
|
} from "@welshman/util"
|
||||||
import {Pool, AuthStatus, SocketStatus} from "@welshman/net"
|
import {Pool, AuthStatus, SocketStatus} from "@welshman/net"
|
||||||
import {Router} from "@welshman/router"
|
import {Router} from "@welshman/router"
|
||||||
@@ -72,6 +73,9 @@ import {
|
|||||||
getPubkeyRelays,
|
getPubkeyRelays,
|
||||||
userBlossomServerList,
|
userBlossomServerList,
|
||||||
getThunkError,
|
getThunkError,
|
||||||
|
addRoomMember,
|
||||||
|
manageRelay,
|
||||||
|
getRelay,
|
||||||
} from "@welshman/app"
|
} from "@welshman/app"
|
||||||
import {compressFile} from "@lib/html"
|
import {compressFile} from "@lib/html"
|
||||||
import type {SettingsValues, SpaceNotificationSettings} from "@app/core/state"
|
import type {SettingsValues, SpaceNotificationSettings} from "@app/core/state"
|
||||||
@@ -89,6 +93,7 @@ import {
|
|||||||
stripPrefix,
|
stripPrefix,
|
||||||
relaysMostlyRestricted,
|
relaysMostlyRestricted,
|
||||||
deriveSocket,
|
deriveSocket,
|
||||||
|
deriveSpaceMembers,
|
||||||
} from "@app/core/state"
|
} from "@app/core/state"
|
||||||
|
|
||||||
// Utils
|
// Utils
|
||||||
@@ -220,8 +225,7 @@ export const attemptRelayAccess = async (url: string, claim = "") => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const thunk = publishJoinRequest({url, claim})
|
const error = await waitForThunkError(publishJoinRequest({url, claim}))
|
||||||
const error = await waitForThunkError(thunk)
|
|
||||||
|
|
||||||
if (shouldIgnoreError(error)) return
|
if (shouldIgnoreError(error)) return
|
||||||
if (!claim && error.includes("invite code size")) return
|
if (!claim && error.includes("invite code size")) return
|
||||||
@@ -408,12 +412,9 @@ export const toggleRoomNotifications = async (url: string, h: string) => {
|
|||||||
let updated: typeof alerts
|
let updated: typeof alerts
|
||||||
|
|
||||||
if (!existing) {
|
if (!existing) {
|
||||||
// No space settings yet, create one with this room as an exception (default is notify: true)
|
|
||||||
updated = [...alerts, {url, notify: true, exceptions: [h]}]
|
updated = [...alerts, {url, notify: true, exceptions: [h]}]
|
||||||
} else {
|
} else {
|
||||||
// Toggle exception status
|
const exceptions = existing.exceptions.includes(h)
|
||||||
const hasException = existing.exceptions.includes(h)
|
|
||||||
const exceptions = hasException
|
|
||||||
? remove(h, existing.exceptions)
|
? remove(h, existing.exceptions)
|
||||||
: append(h, existing.exceptions)
|
: append(h, existing.exceptions)
|
||||||
|
|
||||||
@@ -550,6 +551,12 @@ export const createInvoice = async ({
|
|||||||
export const normalizeBlossomUrl = (url: string) => normalizeUrl(url.replace(/^ws/, "http"))
|
export const normalizeBlossomUrl = (url: string) => normalizeUrl(url.replace(/^ws/, "http"))
|
||||||
|
|
||||||
export const fetchHasBlossomSupport = async (url: string) => {
|
export const fetchHasBlossomSupport = async (url: string) => {
|
||||||
|
const relay = getRelay(url)
|
||||||
|
|
||||||
|
if (relay?.supported_nips?.map(String).includes("BUD-02")) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
const server = normalizeBlossomUrl(url)
|
const server = normalizeBlossomUrl(url)
|
||||||
const $signer = signer.get() || Nip01Signer.ephemeral()
|
const $signer = signer.get() || Nip01Signer.ephemeral()
|
||||||
const headers: Record<string, string> = {
|
const headers: Record<string, string> = {
|
||||||
@@ -639,13 +646,19 @@ export const uploadFile = async (file: File, options: UploadFileOptions = {}) =>
|
|||||||
const res = await uploadBlob(server, file, {authEvent})
|
const res = await uploadBlob(server, file, {authEvent})
|
||||||
const text = await res.text()
|
const text = await res.text()
|
||||||
|
|
||||||
let {uploaded, url, ...task} = parseJson(text) || {}
|
let task
|
||||||
|
try {
|
||||||
|
task = parseJson(text)
|
||||||
|
} catch (e) {
|
||||||
|
return {error: text}
|
||||||
|
}
|
||||||
|
|
||||||
if (!uploaded) {
|
if (!task?.uploaded) {
|
||||||
return {error: text || `Failed to upload file (HTTP ${res.status})`}
|
return {error: text || `Failed to upload file (HTTP ${res.status})`}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Always append correct file extension if we encrypted the file, or if it's missing
|
// Always append correct file extension if we encrypted the file, or if it's missing
|
||||||
|
let url = task.url
|
||||||
if (options.encrypt) {
|
if (options.encrypt) {
|
||||||
url = url.replace(/\.\w+$/, "") + ext
|
url = url.replace(/\.\w+$/, "") + ext
|
||||||
} else if (new URL(url).pathname.split(".").length === 1) {
|
} else if (new URL(url).pathname.split(".").length === 1) {
|
||||||
@@ -699,3 +712,50 @@ export const updateProfile = ({
|
|||||||
|
|
||||||
return publishThunk({event, relays})
|
return publishThunk({event, relays})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Admin actions
|
||||||
|
|
||||||
|
export const addSpaceMembers = async (
|
||||||
|
url: string,
|
||||||
|
pubkeys: string[],
|
||||||
|
): Promise<string | undefined> => {
|
||||||
|
const spaceMembers = get(deriveSpaceMembers(url))
|
||||||
|
const results = await Promise.all(
|
||||||
|
pubkeys
|
||||||
|
.filter(pubkey => !spaceMembers.includes(pubkey))
|
||||||
|
.map(pubkey =>
|
||||||
|
manageRelay(url, {
|
||||||
|
method: ManagementMethod.AllowPubkey,
|
||||||
|
params: [pubkey],
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
for (const {error} of results) {
|
||||||
|
if (error) {
|
||||||
|
return error
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const addRoomMembers = async (
|
||||||
|
url: string,
|
||||||
|
room: PublishedRoomMeta,
|
||||||
|
pubkeys: string[],
|
||||||
|
): Promise<string | undefined> => {
|
||||||
|
const error = await addSpaceMembers(url, pubkeys)
|
||||||
|
|
||||||
|
if (error) {
|
||||||
|
return error
|
||||||
|
}
|
||||||
|
|
||||||
|
const errors = await Promise.all(
|
||||||
|
pubkeys.map(pubkey => waitForThunkError(addRoomMember(url, room, pubkey))),
|
||||||
|
)
|
||||||
|
|
||||||
|
for (const error of errors) {
|
||||||
|
if (error) {
|
||||||
|
return error
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
+16
-12
@@ -4,7 +4,7 @@ import {
|
|||||||
uniq,
|
uniq,
|
||||||
int,
|
int,
|
||||||
YEAR,
|
YEAR,
|
||||||
DAY,
|
WEEK,
|
||||||
insertAt,
|
insertAt,
|
||||||
sortBy,
|
sortBy,
|
||||||
now,
|
now,
|
||||||
@@ -22,6 +22,7 @@ import {
|
|||||||
getAddress,
|
getAddress,
|
||||||
isShareableRelayUrl,
|
isShareableRelayUrl,
|
||||||
getRelaysFromList,
|
getRelaysFromList,
|
||||||
|
sortEventsDesc,
|
||||||
} from "@welshman/util"
|
} from "@welshman/util"
|
||||||
import type {TrustedEvent, Filter, List} from "@welshman/util"
|
import type {TrustedEvent, Filter, List} from "@welshman/util"
|
||||||
import {load, request} from "@welshman/net"
|
import {load, request} from "@welshman/net"
|
||||||
@@ -47,11 +48,11 @@ export const makeFeed = ({
|
|||||||
onForwardExhausted?: () => void
|
onForwardExhausted?: () => void
|
||||||
at?: number
|
at?: number
|
||||||
}) => {
|
}) => {
|
||||||
const interval = int(DAY)
|
|
||||||
const controller = new AbortController()
|
const controller = new AbortController()
|
||||||
const events = writable<TrustedEvent[]>([])
|
const events = writable<TrustedEvent[]>([])
|
||||||
|
|
||||||
let buffer: TrustedEvent[] = []
|
let interval = int(WEEK)
|
||||||
|
let buffer = sortEventsDesc(getEventsForUrl(url, filters))
|
||||||
let backwardWindow = [at - interval, at]
|
let backwardWindow = [at - interval, at]
|
||||||
let forwardWindow = [at, at + interval]
|
let forwardWindow = [at, at + interval]
|
||||||
|
|
||||||
@@ -111,13 +112,20 @@ export const makeFeed = ({
|
|||||||
}),
|
}),
|
||||||
]
|
]
|
||||||
|
|
||||||
const loadTimeframe = (since: number, until: number) => {
|
const loadTimeframe = async (since: number, until: number) => {
|
||||||
request({
|
const events = await request({
|
||||||
relays: [url],
|
relays: [url],
|
||||||
autoClose: true,
|
autoClose: true,
|
||||||
signal: controller.signal,
|
signal: controller.signal,
|
||||||
filters: filters.map(filter => ({...filter, since, until})),
|
filters: filters.map(filter => ({...filter, since, until})),
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// If we found nothing, accelerate
|
||||||
|
if (events.length === 0) {
|
||||||
|
interval = Math.round(interval * 1.1)
|
||||||
|
} else {
|
||||||
|
interval = int(WEEK)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const backwardScroller = createScroller({
|
const backwardScroller = createScroller({
|
||||||
@@ -129,7 +137,7 @@ export const makeFeed = ({
|
|||||||
|
|
||||||
backwardWindow = [since - interval, since]
|
backwardWindow = [since - interval, since]
|
||||||
|
|
||||||
for (const event of buffer.splice(0)) {
|
for (const event of buffer.splice(0, 30)) {
|
||||||
insertEvent(event)
|
insertEvent(event)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -152,7 +160,7 @@ export const makeFeed = ({
|
|||||||
|
|
||||||
forwardWindow = [until, until + interval]
|
forwardWindow = [until, until + interval]
|
||||||
|
|
||||||
for (const event of buffer.splice(0)) {
|
for (const event of buffer.splice(0, 30)) {
|
||||||
insertEvent(event)
|
insertEvent(event)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -165,10 +173,6 @@ export const makeFeed = ({
|
|||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
for (const event of getEventsForUrl(url, filters)) {
|
|
||||||
insertEvent(event)
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
events,
|
events,
|
||||||
cleanup: () => {
|
cleanup: () => {
|
||||||
@@ -191,7 +195,7 @@ export const makeCalendarFeed = ({
|
|||||||
element: HTMLElement
|
element: HTMLElement
|
||||||
onExhausted?: () => void
|
onExhausted?: () => void
|
||||||
}) => {
|
}) => {
|
||||||
const interval = int(5, DAY)
|
const interval = int(5, WEEK)
|
||||||
const controller = new AbortController()
|
const controller = new AbortController()
|
||||||
|
|
||||||
let exhaustedScrollers = 0
|
let exhaustedScrollers = 0
|
||||||
|
|||||||
+156
-26
@@ -14,6 +14,7 @@ import {
|
|||||||
uniqBy,
|
uniqBy,
|
||||||
sortBy,
|
sortBy,
|
||||||
append,
|
append,
|
||||||
|
reject,
|
||||||
sort,
|
sort,
|
||||||
uniq,
|
uniq,
|
||||||
indexBy,
|
indexBy,
|
||||||
@@ -30,6 +31,7 @@ import {
|
|||||||
groupBy,
|
groupBy,
|
||||||
remove,
|
remove,
|
||||||
simpleCache,
|
simpleCache,
|
||||||
|
removeUndefined,
|
||||||
} from "@welshman/lib"
|
} from "@welshman/lib"
|
||||||
import type {Override} from "@welshman/lib"
|
import type {Override} from "@welshman/lib"
|
||||||
import type {RepositoryUpdate} from "@welshman/net"
|
import type {RepositoryUpdate} from "@welshman/net"
|
||||||
@@ -98,6 +100,7 @@ import {
|
|||||||
REPOST,
|
REPOST,
|
||||||
GENERIC_REPOST,
|
GENERIC_REPOST,
|
||||||
asDecryptedEvent,
|
asDecryptedEvent,
|
||||||
|
getTagValue,
|
||||||
getGroupTags,
|
getGroupTags,
|
||||||
getListTags,
|
getListTags,
|
||||||
getPubkeyTagValues,
|
getPubkeyTagValues,
|
||||||
@@ -110,6 +113,7 @@ import {
|
|||||||
readRoomMeta,
|
readRoomMeta,
|
||||||
makeRoomMeta,
|
makeRoomMeta,
|
||||||
ManagementMethod,
|
ManagementMethod,
|
||||||
|
sortEventsAsc,
|
||||||
sortEventsDesc,
|
sortEventsDesc,
|
||||||
getAddress,
|
getAddress,
|
||||||
Address,
|
Address,
|
||||||
@@ -124,6 +128,7 @@ import type {
|
|||||||
RelayProfile,
|
RelayProfile,
|
||||||
PublishedList,
|
PublishedList,
|
||||||
PublishedRoomMeta,
|
PublishedRoomMeta,
|
||||||
|
RoomMeta,
|
||||||
List,
|
List,
|
||||||
Filter,
|
Filter,
|
||||||
} from "@welshman/util"
|
} from "@welshman/util"
|
||||||
@@ -145,6 +150,7 @@ import {
|
|||||||
displayProfileByPubkey,
|
displayProfileByPubkey,
|
||||||
getProfile,
|
getProfile,
|
||||||
} from "@welshman/app"
|
} from "@welshman/app"
|
||||||
|
import {checkRelayHasLivekit} from "$lib/livekit"
|
||||||
import {readFeed} from "@lib/feeds"
|
import {readFeed} from "@lib/feeds"
|
||||||
|
|
||||||
export const fromCsv = (s: string) => (s || "").split(",").filter(identity)
|
export const fromCsv = (s: string) => (s || "").split(",").filter(identity)
|
||||||
@@ -284,7 +290,7 @@ export const deriveRelaySignedEvents = (url: string, filters: Filter[] = [{}]) =
|
|||||||
derived(
|
derived(
|
||||||
[deriveRelay(url), deriveEventsForUrl(url, filters)],
|
[deriveRelay(url), deriveEventsForUrl(url, filters)],
|
||||||
([relay, events]) => events,
|
([relay, events]) => events,
|
||||||
// khatru doesn't support relay.self, uncomment when it's ready
|
// TODO: khatru doesn't support relay.self, uncomment when it's ready
|
||||||
// filter(spec({pubkey: relay.self}), events)
|
// filter(spec({pubkey: relay.self}), events)
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -425,10 +431,11 @@ export type PushSubscription = {
|
|||||||
|
|
||||||
export type PushState = {
|
export type PushState = {
|
||||||
token?: string
|
token?: string
|
||||||
|
useFallback?: boolean
|
||||||
subscription?: PushSubscription
|
subscription?: PushSubscription
|
||||||
}
|
}
|
||||||
|
|
||||||
export const notificationState = withGetter(writable<PushState>({}))
|
export const pushState = withGetter(writable<PushState>({}))
|
||||||
|
|
||||||
// Chats
|
// Chats
|
||||||
|
|
||||||
@@ -457,7 +464,7 @@ export const splitChatId = (id: string) => getChatPubkeys(id.split(","))
|
|||||||
|
|
||||||
export const chatsById = call(() => {
|
export const chatsById = call(() => {
|
||||||
const chatsById = new Map<string, Chat>()
|
const chatsById = new Map<string, Chat>()
|
||||||
const chatsByPubkey = new Map<string, Chat[]>()
|
const chatsByPubkey = new Map<string, string[]>()
|
||||||
|
|
||||||
const addSearchText = (chat: Override<Chat, {search_text?: string}>) => {
|
const addSearchText = (chat: Override<Chat, {search_text?: string}>) => {
|
||||||
chat.search_text =
|
chat.search_text =
|
||||||
@@ -469,6 +476,12 @@ export const chatsById = call(() => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return readable(chatsById, set => {
|
return readable(chatsById, set => {
|
||||||
|
const indexChatByPubkeys = (chat: Chat) => {
|
||||||
|
for (const pubkey of chat.pubkeys) {
|
||||||
|
chatsByPubkey.set(pubkey, uniq(append(chat.id, chatsByPubkey.get(pubkey) || [])))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const addEvents = (events: TrustedEvent[]) => {
|
const addEvents = (events: TrustedEvent[]) => {
|
||||||
let dirty = false
|
let dirty = false
|
||||||
for (const event of events) {
|
for (const event of events) {
|
||||||
@@ -484,21 +497,19 @@ export const chatsById = call(() => {
|
|||||||
const updatedChat = addSearchText({id, pubkeys, messages, last_activity})
|
const updatedChat = addSearchText({id, pubkeys, messages, last_activity})
|
||||||
|
|
||||||
chatsById.set(id, updatedChat)
|
chatsById.set(id, updatedChat)
|
||||||
|
indexChatByPubkeys(updatedChat)
|
||||||
for (const pubkey of pubkeys) {
|
|
||||||
const pubkeyChats = chatsByPubkey.get(pubkey) || []
|
|
||||||
const uniqueChats = uniqBy(chat => chat.id, append(updatedChat, pubkeyChats))
|
|
||||||
|
|
||||||
chatsByPubkey.set(pubkey, uniqueChats)
|
|
||||||
}
|
|
||||||
|
|
||||||
dirty = true
|
dirty = true
|
||||||
}
|
}
|
||||||
|
|
||||||
if (event.kind === PROFILE) {
|
if (event.kind === PROFILE) {
|
||||||
for (const chat of chatsByPubkey.get(event.pubkey) || []) {
|
for (const chatId of chatsByPubkey.get(event.pubkey) || []) {
|
||||||
addSearchText(chat)
|
const chat = chatsById.get(chatId)
|
||||||
dirty = true
|
|
||||||
|
if (chat) {
|
||||||
|
addSearchText(chat)
|
||||||
|
dirty = true
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -508,10 +519,39 @@ export const chatsById = call(() => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
addEvents(repository.query([{kinds: [...DM_KINDS, PROFILE]}]))
|
const removeEvents = (removed: Set<string>) => {
|
||||||
|
let dirty = false
|
||||||
|
|
||||||
|
for (const id of removed) {
|
||||||
|
const event = repository.getEvent(id)
|
||||||
|
|
||||||
|
if (event && DM_KINDS.includes(event.kind)) {
|
||||||
|
for (const chatId of chatsByPubkey.get(event.pubkey) || []) {
|
||||||
|
const chat = chatsById.get(chatId)
|
||||||
|
|
||||||
|
if (chat) {
|
||||||
|
chat.messages = reject(spec({id: event.id}), chat.messages)
|
||||||
|
dirty = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (dirty) {
|
||||||
|
set(chatsById)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
addEvents(repository.query([{kinds: [...DM_KINDS, DELETE, PROFILE]}]))
|
||||||
|
|
||||||
const unsubscribers = [
|
const unsubscribers = [
|
||||||
on(repository, "update", ({added}: RepositoryUpdate) => addEvents(added)),
|
on(repository, "update", ({added, removed}: RepositoryUpdate) => {
|
||||||
|
// Do this async so that profiles are populated
|
||||||
|
setTimeout(() => {
|
||||||
|
addEvents(added)
|
||||||
|
removeEvents(removed)
|
||||||
|
}, 50)
|
||||||
|
}),
|
||||||
]
|
]
|
||||||
|
|
||||||
return () => unsubscribers.forEach(call)
|
return () => unsubscribers.forEach(call)
|
||||||
@@ -536,17 +576,25 @@ export const chatSearch = derived(throttled(800, chatsById), $chatsByPubkey => {
|
|||||||
|
|
||||||
// Rooms
|
// Rooms
|
||||||
|
|
||||||
|
export enum RoomType {
|
||||||
|
Text = "text",
|
||||||
|
Voice = "voice",
|
||||||
|
}
|
||||||
|
|
||||||
export type Room = PublishedRoomMeta & {
|
export type Room = PublishedRoomMeta & {
|
||||||
id: string
|
id: string
|
||||||
url: string
|
url: string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const getRoomType = (room: RoomMeta): RoomType =>
|
||||||
|
room.livekit ? RoomType.Voice : RoomType.Text
|
||||||
|
|
||||||
export const makeRoomId = (url: string, h: string) => `${url}'${h}`
|
export const makeRoomId = (url: string, h: string) => `${url}'${h}`
|
||||||
|
|
||||||
export const splitRoomId = (id: string) => id.split("'")
|
export const splitRoomId = (id: string) => id.split("'")
|
||||||
|
|
||||||
export const hasNip29 = (relay?: RelayProfile) =>
|
export const hasNip29 = (relay?: RelayProfile) =>
|
||||||
relay?.supported_nips?.map?.(String)?.includes?.("29")
|
Boolean(relay?.supported_nips?.map?.(String)?.includes?.("29"))
|
||||||
|
|
||||||
export const roomMetaEventsByIdByUrl = deriveEventsByIdByUrl({
|
export const roomMetaEventsByIdByUrl = deriveEventsByIdByUrl({
|
||||||
tracker,
|
tracker,
|
||||||
@@ -624,7 +672,7 @@ export const deriveRoom = call(() => {
|
|||||||
return (url: string, h: string) =>
|
return (url: string, h: string) =>
|
||||||
derived(
|
derived(
|
||||||
_deriveRoom(makeRoomId(url, h)),
|
_deriveRoom(makeRoomId(url, h)),
|
||||||
room => room || {url, id: makeRoomId(url, h), ...makeRoomMeta({h})},
|
room => (room || {url, id: makeRoomId(url, h), ...makeRoomMeta({h})}) as Room,
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -632,6 +680,30 @@ export const displayRoom = (url: string, h: string) => getRoom(makeRoomId(url, h
|
|||||||
|
|
||||||
export const roomComparator = (url: string) => (h: string) => displayRoom(url, h).toLowerCase()
|
export const roomComparator = (url: string) => (h: string) => displayRoom(url, h).toLowerCase()
|
||||||
|
|
||||||
|
export const deriveVoiceRooms = (url: string) =>
|
||||||
|
derived(roomsById, $roomsById => {
|
||||||
|
const set = new Set<string>()
|
||||||
|
for (const room of $roomsById.values()) {
|
||||||
|
if (room.url === url && room.livekit) {
|
||||||
|
set.add(room.h)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return set
|
||||||
|
})
|
||||||
|
|
||||||
|
export const deriveOtherVoiceRooms = (url: string) =>
|
||||||
|
derived([deriveVoiceRooms(url), deriveUserRooms(url)], ([$roomsWithLivekit, $userRooms]) => {
|
||||||
|
const rooms: string[] = []
|
||||||
|
|
||||||
|
for (const h of $roomsWithLivekit) {
|
||||||
|
if (!$userRooms.includes(h)) {
|
||||||
|
rooms.push(h)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return sortBy(roomComparator(url), uniq(rooms))
|
||||||
|
})
|
||||||
|
|
||||||
// User space/room lists
|
// User space/room lists
|
||||||
|
|
||||||
export const groupListsByPubkey = deriveItemsByKey({
|
export const groupListsByPubkey = deriveItemsByKey({
|
||||||
@@ -721,17 +793,20 @@ export const deriveUserRooms = (url: string) =>
|
|||||||
})
|
})
|
||||||
|
|
||||||
export const deriveOtherRooms = (url: string) =>
|
export const deriveOtherRooms = (url: string) =>
|
||||||
derived([deriveUserRooms(url), roomsByUrl], ([$userRooms, $roomsByUrl]) => {
|
derived(
|
||||||
const rooms: string[] = []
|
[deriveUserRooms(url), deriveVoiceRooms(url), roomsByUrl],
|
||||||
|
([$userRooms, voiceRooms, $roomsByUrl]) => {
|
||||||
|
const rooms: string[] = []
|
||||||
|
|
||||||
for (const {h} of $roomsByUrl.get(url) || []) {
|
for (const {h} of $roomsByUrl.get(url) || []) {
|
||||||
if (!$userRooms.includes(h)) {
|
if (!$userRooms.includes(h) && !voiceRooms.has(h)) {
|
||||||
rooms.push(h)
|
rooms.push(h)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
return sortBy(roomComparator(url), uniq(rooms))
|
return sortBy(roomComparator(url), uniq(rooms))
|
||||||
})
|
},
|
||||||
|
)
|
||||||
|
|
||||||
// Space/room memberships
|
// Space/room memberships
|
||||||
|
|
||||||
@@ -800,7 +875,7 @@ export const deriveRoomMembers = (url: string, h: string) => {
|
|||||||
|
|
||||||
const members = new Set<string>()
|
const members = new Set<string>()
|
||||||
|
|
||||||
for (const event of sortBy(e => -e.created_at, $events)) {
|
for (const event of sortEventsAsc($events)) {
|
||||||
const pubkeys = getPubkeyTagValues(event.tags)
|
const pubkeys = getPubkeyTagValues(event.tags)
|
||||||
|
|
||||||
if (event.kind === ROOM_ADD_MEMBER) {
|
if (event.kind === ROOM_ADD_MEMBER) {
|
||||||
@@ -834,6 +909,50 @@ export const deriveRoomAdmins = (url: string, h: string) => {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Action items (admin review queue)
|
||||||
|
// const pendingJoins: TrustedEvent[] = []
|
||||||
|
|
||||||
|
export const deriveSpaceActionItems = (url: string) =>
|
||||||
|
derived(
|
||||||
|
deriveEventsForUrl(url, [
|
||||||
|
{
|
||||||
|
kinds: [REPORT, ROOM_JOIN, ROOM_LEAVE, ROOM_MEMBERS],
|
||||||
|
},
|
||||||
|
]),
|
||||||
|
$events => {
|
||||||
|
const getRoomId = (e: TrustedEvent) =>
|
||||||
|
getTagValue(e.kind === ROOM_MEMBERS ? "d" : "h", e.tags)
|
||||||
|
const reports = $events.filter(e => e.kind === REPORT)
|
||||||
|
const pendingJoins: TrustedEvent[] = []
|
||||||
|
|
||||||
|
// Room-level join requests — most recent per pubkey+h
|
||||||
|
for (const [h, roomEvents] of groupBy(getRoomId, $events)) {
|
||||||
|
if (!h) continue
|
||||||
|
|
||||||
|
const roomJoins = roomEvents.filter(spec({kind: ROOM_JOIN}))
|
||||||
|
const roomLeaves = roomEvents.filter(spec({kind: ROOM_LEAVE}))
|
||||||
|
const roomMembersEvent = roomEvents.find(spec({kind: ROOM_MEMBERS}))
|
||||||
|
const roomMembers = getTagValues("p", roomMembersEvent?.tags ?? [])
|
||||||
|
|
||||||
|
pendingJoins.push(
|
||||||
|
...removeUndefined(
|
||||||
|
Array.from(groupBy(e => e.pubkey, roomJoins).values())
|
||||||
|
.map(sortEventsDesc)
|
||||||
|
.map(first),
|
||||||
|
).filter(({pubkey, created_at}) => {
|
||||||
|
if (roomMembers.includes(pubkey)) return false
|
||||||
|
if (gt(roomMembersEvent?.created_at, created_at)) return false
|
||||||
|
if (roomLeaves.some(e => e.pubkey === pubkey && e.created_at > created_at)) return false
|
||||||
|
|
||||||
|
return true
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return sortEventsDesc([...reports, ...pendingJoins])
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
// User membership status
|
// User membership status
|
||||||
|
|
||||||
export enum MembershipStatus {
|
export enum MembershipStatus {
|
||||||
@@ -1134,6 +1253,12 @@ export const deriveSupportedMethods = simpleCache(([url]: [string]) => {
|
|||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
export const deriveHasLivekit = simpleCache(([url]: [string]) =>
|
||||||
|
readable<boolean | undefined>(undefined, set => {
|
||||||
|
checkRelayHasLivekit(url).then(has => set(has))
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
|
||||||
export const deriveTimeout = (timeout: number) => {
|
export const deriveTimeout = (timeout: number) => {
|
||||||
const store = writable<boolean>(false)
|
const store = writable<boolean>(false)
|
||||||
|
|
||||||
@@ -1193,3 +1318,8 @@ export const shouldNotify = (url: string, h?: string) => getShouldNotify(getSett
|
|||||||
|
|
||||||
export const deriveShouldNotify = (url: string, h?: string) =>
|
export const deriveShouldNotify = (url: string, h?: string) =>
|
||||||
derived(userSettingsValues, $settings => getShouldNotify($settings, url, h))
|
derived(userSettingsValues, $settings => getShouldNotify($settings, url, h))
|
||||||
|
|
||||||
|
// Whatever who cares
|
||||||
|
|
||||||
|
export const hasNip50 = (relay?: RelayProfile) =>
|
||||||
|
Boolean(relay?.supported_nips?.map?.(String)?.includes?.("50"))
|
||||||
|
|||||||
+52
-17
@@ -1,6 +1,6 @@
|
|||||||
import {call} from "@welshman/lib"
|
import {call} from "@welshman/lib"
|
||||||
|
import {SecureStorage} from "@aparajita/capacitor-secure-storage"
|
||||||
import {Preferences} from "@capacitor/preferences"
|
import {Preferences} from "@capacitor/preferences"
|
||||||
import {Filesystem, Directory} from "@capacitor/filesystem"
|
|
||||||
import {IDB} from "@lib/indexeddb"
|
import {IDB} from "@lib/indexeddb"
|
||||||
|
|
||||||
export const kv = call(() => {
|
export const kv = call(() => {
|
||||||
@@ -31,22 +31,57 @@ export const kv = call(() => {
|
|||||||
return {get, set, clear}
|
return {get, set, clear}
|
||||||
})
|
})
|
||||||
|
|
||||||
export const db = new IDB({name: "flotilla-9gl", version: 1})
|
export const ss = call(() => {
|
||||||
|
let p = Promise.resolve()
|
||||||
|
|
||||||
// Migration - we used to use capacitor's filesystem for storage, clear it out since we're
|
const get = async <T>(key: string): Promise<T | undefined> => {
|
||||||
// going back to indexeddb
|
let value = await SecureStorage.getItem(key)
|
||||||
call(async () => {
|
|
||||||
const res = await Filesystem.readdir({
|
|
||||||
path: "",
|
|
||||||
directory: Directory.Data,
|
|
||||||
})
|
|
||||||
|
|
||||||
await Promise.all(
|
if (!value) {
|
||||||
res.files.map(file =>
|
const legacy = await Preferences.get({key})
|
||||||
Filesystem.deleteFile({
|
|
||||||
path: file.name,
|
if (legacy.value) {
|
||||||
directory: Directory.Data,
|
value = legacy.value
|
||||||
}),
|
await SecureStorage.setItem(key, legacy.value)
|
||||||
),
|
await Preferences.remove({key})
|
||||||
)
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!value) return undefined
|
||||||
|
|
||||||
|
try {
|
||||||
|
return JSON.parse(value)
|
||||||
|
} catch (e) {
|
||||||
|
return undefined
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const set = async <T>(key: string, value: T): Promise<void> => {
|
||||||
|
p = p.then(() => SecureStorage.setItem(key, JSON.stringify(value)))
|
||||||
|
|
||||||
|
await p
|
||||||
|
}
|
||||||
|
|
||||||
|
const clear = async () => {
|
||||||
|
p = p.then(() => SecureStorage.clear())
|
||||||
|
|
||||||
|
await p
|
||||||
|
}
|
||||||
|
|
||||||
|
return {get, set, clear}
|
||||||
|
})
|
||||||
|
|
||||||
|
export const db = new IDB({
|
||||||
|
name: "flotilla-9gl",
|
||||||
|
version: 1,
|
||||||
|
stores: [
|
||||||
|
{name: "events", keyPath: "id"},
|
||||||
|
{name: "tracker", keyPath: "id"},
|
||||||
|
{name: "relays", keyPath: "url"},
|
||||||
|
{name: "relayStats", keyPath: "url"},
|
||||||
|
{name: "handles", keyPath: "nip05"},
|
||||||
|
{name: "zappers", keyPath: "lnurl"},
|
||||||
|
{name: "plaintext", keyPath: "key"},
|
||||||
|
{name: "wrapManager", keyPath: "id"},
|
||||||
|
],
|
||||||
})
|
})
|
||||||
|
|||||||
+71
-70
@@ -1,7 +1,7 @@
|
|||||||
import {page} from "$app/stores"
|
import {page} from "$app/stores"
|
||||||
import type {Unsubscriber} from "svelte/store"
|
import type {Unsubscriber} from "svelte/store"
|
||||||
import {derived, get} from "svelte/store"
|
import {derived, get} from "svelte/store"
|
||||||
import {last, call, assoc, chunk, sleep, identity, WEEK, ago} from "@welshman/lib"
|
import {last, call, ifLet, assoc, chunk, sleep, identity, WEEK, ago} from "@welshman/lib"
|
||||||
import {
|
import {
|
||||||
getListTags,
|
getListTags,
|
||||||
getRelayTagValues,
|
getRelayTagValues,
|
||||||
@@ -18,8 +18,9 @@ import {
|
|||||||
RELAY_REMOVE_MEMBER,
|
RELAY_REMOVE_MEMBER,
|
||||||
isSignedEvent,
|
isSignedEvent,
|
||||||
unionFilters,
|
unionFilters,
|
||||||
|
getTagValue,
|
||||||
} from "@welshman/util"
|
} from "@welshman/util"
|
||||||
import type {Filter} from "@welshman/util"
|
import type {Filter, TrustedEvent} from "@welshman/util"
|
||||||
import {request, requestOne, Difference, DifferenceEvent} from "@welshman/net"
|
import {request, requestOne, Difference, DifferenceEvent} from "@welshman/net"
|
||||||
import {
|
import {
|
||||||
pubkey,
|
pubkey,
|
||||||
@@ -55,6 +56,7 @@ import {
|
|||||||
loadFeedsForPubkey,
|
loadFeedsForPubkey,
|
||||||
} from "@app/core/state"
|
} from "@app/core/state"
|
||||||
import {hasBlossomSupport} from "@app/core/commands"
|
import {hasBlossomSupport} from "@app/core/commands"
|
||||||
|
import {LIVEKIT_PARTICIPANTS} from "@app/voice"
|
||||||
|
|
||||||
// Utils
|
// Utils
|
||||||
|
|
||||||
@@ -62,12 +64,24 @@ type SyncOpts = {
|
|||||||
url: string
|
url: string
|
||||||
signal: AbortSignal
|
signal: AbortSignal
|
||||||
filters: Filter[]
|
filters: Filter[]
|
||||||
|
onEvent?: (event: TrustedEvent) => void
|
||||||
}
|
}
|
||||||
|
|
||||||
const pullOneWithFallback = async (url: string, filter: Filter, signal: AbortSignal) => {
|
const pullOneWithFallback = async (
|
||||||
|
url: string,
|
||||||
|
filter: Filter,
|
||||||
|
signal: AbortSignal,
|
||||||
|
onEvent?: (event: TrustedEvent) => void,
|
||||||
|
) => {
|
||||||
const cachedEvents = repository.query([filter]).filter(isSignedEvent)
|
const cachedEvents = repository.query([filter]).filter(isSignedEvent)
|
||||||
const since = last(cachedEvents.slice(10))?.created_at || 0
|
const since = last(cachedEvents.slice(10))?.created_at || 0
|
||||||
|
|
||||||
|
if (onEvent) {
|
||||||
|
for (const event of cachedEvents) {
|
||||||
|
onEvent(event)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const shouldFallback =
|
const shouldFallback =
|
||||||
!hasNegentropy(url) ||
|
!hasNegentropy(url) ||
|
||||||
(await new Promise(resolve => {
|
(await new Promise(resolve => {
|
||||||
@@ -79,7 +93,7 @@ const pullOneWithFallback = async (url: string, filter: Filter, signal: AbortSig
|
|||||||
|
|
||||||
diff.on(DifferenceEvent.Close, () => {
|
diff.on(DifferenceEvent.Close, () => {
|
||||||
for (const ids of chunk(100, Array.from(diff.need))) {
|
for (const ids of chunk(100, Array.from(diff.need))) {
|
||||||
requestOne({relay: url, signal, autoClose: true, filters: [{ids}]})
|
requestOne({relay: url, signal, autoClose: true, filters: [{ids}], onEvent})
|
||||||
}
|
}
|
||||||
|
|
||||||
resolve(false)
|
resolve(false)
|
||||||
@@ -87,29 +101,29 @@ const pullOneWithFallback = async (url: string, filter: Filter, signal: AbortSig
|
|||||||
}))
|
}))
|
||||||
|
|
||||||
if (shouldFallback && !signal.aborted) {
|
if (shouldFallback && !signal.aborted) {
|
||||||
request({relays: [url], signal, autoClose: true, filters: [{...filter, since}]})
|
request({relays: [url], signal, autoClose: true, filters: [{since, ...filter}], onEvent})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export const pullWithFallback = async ({url, signal, filters}: SyncOpts) => {
|
export const pullWithFallback = async ({url, signal, filters, onEvent}: SyncOpts) => {
|
||||||
await loadRelay(url)
|
await loadRelay(url)
|
||||||
|
|
||||||
if (signal.aborted) return
|
if (signal.aborted) return
|
||||||
|
|
||||||
for (const filter of filters) {
|
for (const filter of filters) {
|
||||||
pullOneWithFallback(url, filter, signal)
|
pullOneWithFallback(url, filter, signal, onEvent)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const listen = ({url, signal, filters}: SyncOpts) => {
|
const listen = ({url, signal, filters, onEvent}: SyncOpts) => {
|
||||||
const relays = [url]
|
const relays = [url]
|
||||||
|
|
||||||
request({relays, signal, filters: unionFilters(filters.map(assoc("limit", 0)))})
|
request({relays, signal, filters: unionFilters(filters.map(assoc("limit", 0))), onEvent})
|
||||||
}
|
}
|
||||||
|
|
||||||
const pullAndListen = ({url, filters, signal}: SyncOpts) => {
|
const pullAndListen = (options: SyncOpts) => {
|
||||||
pullWithFallback({url, signal, filters})
|
pullWithFallback(options)
|
||||||
listen({url, signal, filters})
|
listen(options)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Relays
|
// Relays
|
||||||
@@ -254,68 +268,55 @@ const syncUserData = () => {
|
|||||||
// Spaces
|
// Spaces
|
||||||
|
|
||||||
const syncSpace = (url: string, rooms: string[]) => {
|
const syncSpace = (url: string, rooms: string[]) => {
|
||||||
|
const since = ago(WEEK)
|
||||||
|
const seen = new Set<string>()
|
||||||
const controller = new AbortController()
|
const controller = new AbortController()
|
||||||
|
|
||||||
// Relay-level kinds don't need #h tags
|
const pullRoomContent = (room: string) => {
|
||||||
pullAndListen({
|
if (!seen.has(room)) {
|
||||||
url,
|
seen.add(room)
|
||||||
signal: controller.signal,
|
pullAndListen({
|
||||||
filters: [{kinds: [RELAY_MEMBERS, RELAY_ADD_MEMBER, RELAY_REMOVE_MEMBER]}],
|
url,
|
||||||
})
|
signal: controller.signal,
|
||||||
|
filters: [
|
||||||
// Room metadata uses #d tags, not #h, so no filtering needed
|
{kinds: MESSAGE_KINDS, since, "#h": [room]},
|
||||||
pullAndListen({
|
makeCommentFilter(CONTENT_KINDS, {since, "#h": [room]}),
|
||||||
url,
|
],
|
||||||
signal: controller.signal,
|
})
|
||||||
filters: [{kinds: [ROOM_META, ROOM_ADMINS, ROOM_MEMBERS]}],
|
}
|
||||||
})
|
|
||||||
|
|
||||||
// Room-scoped kinds: add #h tags when we know which rooms the user is in.
|
|
||||||
// This avoids sending broad filters that picky relays reject.
|
|
||||||
const roomKinds = [ROOM_DELETE, ROOM_ADD_MEMBER, ROOM_REMOVE_MEMBER]
|
|
||||||
const since = ago(WEEK)
|
|
||||||
|
|
||||||
if (rooms.length > 0) {
|
|
||||||
pullAndListen({
|
|
||||||
url,
|
|
||||||
signal: controller.signal,
|
|
||||||
filters: [{kinds: roomKinds, "#h": rooms}],
|
|
||||||
})
|
|
||||||
|
|
||||||
pullAndListen({
|
|
||||||
url,
|
|
||||||
signal: controller.signal,
|
|
||||||
filters: [
|
|
||||||
{kinds: MESSAGE_KINDS, "#h": rooms, since},
|
|
||||||
makeCommentFilter(CONTENT_KINDS, {"#h": rooms, since}),
|
|
||||||
],
|
|
||||||
})
|
|
||||||
|
|
||||||
listen({
|
|
||||||
url,
|
|
||||||
signal: controller.signal,
|
|
||||||
filters: [{kinds: REACTION_KINDS, "#h": rooms}],
|
|
||||||
})
|
|
||||||
} else {
|
|
||||||
pullAndListen({
|
|
||||||
url,
|
|
||||||
signal: controller.signal,
|
|
||||||
filters: [{kinds: roomKinds}],
|
|
||||||
})
|
|
||||||
|
|
||||||
pullAndListen({
|
|
||||||
url,
|
|
||||||
signal: controller.signal,
|
|
||||||
filters: [{kinds: MESSAGE_KINDS, since}, makeCommentFilter(CONTENT_KINDS, {since})],
|
|
||||||
})
|
|
||||||
|
|
||||||
listen({
|
|
||||||
url,
|
|
||||||
signal: controller.signal,
|
|
||||||
filters: [{kinds: REACTION_KINDS}],
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
for (const room of rooms) {
|
||||||
|
pullRoomContent(room)
|
||||||
|
}
|
||||||
|
|
||||||
|
const relayKinds = [RELAY_MEMBERS, RELAY_ADD_MEMBER, RELAY_REMOVE_MEMBER]
|
||||||
|
const roomMetaKinds = [ROOM_META, ROOM_ADMINS, ROOM_MEMBERS, LIVEKIT_PARTICIPANTS]
|
||||||
|
const roomMemberKinds = [ROOM_DELETE, ROOM_ADD_MEMBER, ROOM_REMOVE_MEMBER]
|
||||||
|
|
||||||
|
pullAndListen({
|
||||||
|
url,
|
||||||
|
signal: controller.signal,
|
||||||
|
filters: [
|
||||||
|
{kinds: relayKinds},
|
||||||
|
{kinds: roomMetaKinds},
|
||||||
|
{kinds: roomMemberKinds},
|
||||||
|
{kinds: MESSAGE_KINDS, since},
|
||||||
|
makeCommentFilter(CONTENT_KINDS, {since}),
|
||||||
|
],
|
||||||
|
onEvent: event => {
|
||||||
|
if (event.kind === ROOM_META) {
|
||||||
|
ifLet(getTagValue("d", event.tags), pullRoomContent)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
listen({
|
||||||
|
url,
|
||||||
|
signal: controller.signal,
|
||||||
|
filters: [{kinds: REACTION_KINDS}],
|
||||||
|
})
|
||||||
|
|
||||||
return () => controller.abort()
|
return () => controller.abort()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,107 @@
|
|||||||
|
import {derived, get} from "svelte/store"
|
||||||
|
import {not, ifLet, sample} from "@welshman/lib"
|
||||||
|
import {getRelaysFromList, RelayMode} from "@welshman/util"
|
||||||
|
import {
|
||||||
|
getRelay,
|
||||||
|
setWriteRelays,
|
||||||
|
setReadRelays,
|
||||||
|
setSearchRelays,
|
||||||
|
setMessagingRelays,
|
||||||
|
userRelayList,
|
||||||
|
userSearchRelayList,
|
||||||
|
userMessagingRelayList,
|
||||||
|
} from "@welshman/app"
|
||||||
|
import {hasNip50, DEFAULT_RELAYS, DEFAULT_MESSAGING_RELAYS} from "@app/core/state"
|
||||||
|
|
||||||
|
export type HealthCheckContext = {
|
||||||
|
readRelays: string[]
|
||||||
|
writeRelays: string[]
|
||||||
|
messagingRelays: string[]
|
||||||
|
searchRelays: string[]
|
||||||
|
}
|
||||||
|
|
||||||
|
export type HealthCheck = {
|
||||||
|
title: string
|
||||||
|
description: string
|
||||||
|
action: string
|
||||||
|
isPending: (context: HealthCheckContext) => boolean
|
||||||
|
apply: (context: HealthCheckContext) => unknown
|
||||||
|
}
|
||||||
|
|
||||||
|
export const healthCheckContext = derived(
|
||||||
|
[userRelayList, userSearchRelayList, userMessagingRelayList],
|
||||||
|
([$userRelayList, $userSearchRelayList, $userMessagingRelayList]) => {
|
||||||
|
return {
|
||||||
|
readRelays: getRelaysFromList($userRelayList, RelayMode.Read),
|
||||||
|
writeRelays: getRelaysFromList($userRelayList, RelayMode.Write),
|
||||||
|
searchRelays: getRelaysFromList($userSearchRelayList),
|
||||||
|
messagingRelays: getRelaysFromList($userMessagingRelayList),
|
||||||
|
}
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
const healthChecks: HealthCheck[] = [
|
||||||
|
{
|
||||||
|
title: "Missing Inbox Relays",
|
||||||
|
description: "Other people aren't currently able to reliably tag you in public notes.",
|
||||||
|
action: "Update",
|
||||||
|
isPending: context => context.readRelays.length <= 1,
|
||||||
|
apply: () => setReadRelays(DEFAULT_RELAYS),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "Missing Outbox Relays",
|
||||||
|
description: "Other people aren't currently able to reliably find your public notes.",
|
||||||
|
action: "Update",
|
||||||
|
isPending: context => context.writeRelays.length <= 1,
|
||||||
|
apply: () => setWriteRelays(DEFAULT_RELAYS),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "Missing DM Relays",
|
||||||
|
description: "You aren't currently able to reliably send or receive direct messages.",
|
||||||
|
action: "Update",
|
||||||
|
isPending: context => context.messagingRelays.length <= 1,
|
||||||
|
apply: () => setMessagingRelays(DEFAULT_MESSAGING_RELAYS),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "Too Many Inbox Relays",
|
||||||
|
description:
|
||||||
|
"You have more inbox relays than is really necessary, which can affect resource usage.",
|
||||||
|
action: "Prune Selections",
|
||||||
|
isPending: context => context.readRelays.length > 8,
|
||||||
|
apply: context => setReadRelays(sample(5, context.readRelays)),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "Too Many Outbox Relays",
|
||||||
|
description:
|
||||||
|
"You have more outbox relays than is really necessary, which can affect resource usage.",
|
||||||
|
action: "Prune Selections",
|
||||||
|
isPending: context => context.writeRelays.length > 8,
|
||||||
|
apply: context => setWriteRelays(sample(5, context.writeRelays)),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "Too Many DM Relays",
|
||||||
|
description:
|
||||||
|
"You have more DM relays than is really necessary, which can affect resource usage.",
|
||||||
|
action: "Prune Selections",
|
||||||
|
isPending: context => context.messagingRelays.length > 8,
|
||||||
|
apply: context => setMessagingRelays(sample(5, context.messagingRelays)),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "Invalid Search Relays",
|
||||||
|
description: "Some of your search relays don't support search.",
|
||||||
|
action: "Remove Invalid",
|
||||||
|
isPending: context => context.searchRelays.some(url => not(ifLet(getRelay(url), hasNip50))),
|
||||||
|
apply: context =>
|
||||||
|
setSearchRelays(context.searchRelays.filter(url => ifLet(getRelay(url), hasNip50))),
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
export const isHealthCheckPending = (healthCheck: HealthCheck) =>
|
||||||
|
healthCheck.isPending(get(healthCheckContext))
|
||||||
|
|
||||||
|
export const applyHealthCheck = (healthCheck: HealthCheck) =>
|
||||||
|
healthCheck.apply(get(healthCheckContext))
|
||||||
|
|
||||||
|
export const pendingHealthChecks = derived(healthCheckContext, ctx =>
|
||||||
|
healthChecks.filter(hc => hc.isPending(ctx)),
|
||||||
|
)
|
||||||
@@ -8,6 +8,7 @@ export const setupHistory = () =>
|
|||||||
if ($page.params.relay) {
|
if ($page.params.relay) {
|
||||||
lastPageBySpaceUrl.set($page.params.relay, $page.url.pathname)
|
lastPageBySpaceUrl.set($page.params.relay, $page.url.pathname)
|
||||||
}
|
}
|
||||||
|
|
||||||
if ($page.params.chat) {
|
if ($page.params.chat) {
|
||||||
lastChatUrl = $page.url.pathname
|
lastChatUrl = $page.url.pathname
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import {kv, db} from "@app/core/storage"
|
import {db, kv, ss} from "@app/core/storage"
|
||||||
import {Push} from "@app/util/notifications"
|
import {Push} from "@app/util/notifications"
|
||||||
import {deactivateCurrentPomadeSession} from "@app/util/pomade"
|
import {deactivateCurrentPomadeSession} from "@app/util/pomade"
|
||||||
|
|
||||||
@@ -6,6 +6,7 @@ export const logout = async () => {
|
|||||||
await deactivateCurrentPomadeSession()
|
await deactivateCurrentPomadeSession()
|
||||||
await Push.disable()
|
await Push.disable()
|
||||||
await kv.clear()
|
await kv.clear()
|
||||||
|
await ss.clear()
|
||||||
await db.clear()
|
await db.clear()
|
||||||
|
|
||||||
localStorage.clear()
|
localStorage.clear()
|
||||||
|
|||||||
+33
-3
@@ -1,5 +1,5 @@
|
|||||||
import type {Component} from "svelte"
|
import type {Component} from "svelte"
|
||||||
import {writable} from "svelte/store"
|
import {get, writable} from "svelte/store"
|
||||||
import {randomId, always, assoc, Emitter} from "@welshman/lib"
|
import {randomId, always, assoc, Emitter} from "@welshman/lib"
|
||||||
import {deriveDeduplicated} from "@welshman/store"
|
import {deriveDeduplicated} from "@welshman/store"
|
||||||
import {goto} from "$app/navigation"
|
import {goto} from "$app/navigation"
|
||||||
@@ -7,6 +7,7 @@ import {page} from "$app/stores"
|
|||||||
|
|
||||||
export type ModalOptions = {
|
export type ModalOptions = {
|
||||||
drawer?: boolean
|
drawer?: boolean
|
||||||
|
nested?: boolean
|
||||||
noEscape?: boolean
|
noEscape?: boolean
|
||||||
fullscreen?: boolean
|
fullscreen?: boolean
|
||||||
replaceState?: boolean
|
replaceState?: boolean
|
||||||
@@ -24,8 +25,18 @@ export const emitter = new Emitter()
|
|||||||
|
|
||||||
export const modals = writable<Record<string, Modal>>({})
|
export const modals = writable<Record<string, Modal>>({})
|
||||||
|
|
||||||
|
const getIdsFromHash = (hash: string) => hash.slice(1).split(",").filter(Boolean)
|
||||||
|
|
||||||
|
export const modalStack = deriveDeduplicated([page, modals], ([$page, $modals]) => {
|
||||||
|
return getIdsFromHash($page.url.hash)
|
||||||
|
.map(id => $modals[id])
|
||||||
|
.filter(Boolean)
|
||||||
|
})
|
||||||
|
|
||||||
export const modal = deriveDeduplicated([page, modals], ([$page, $modals]) => {
|
export const modal = deriveDeduplicated([page, modals], ([$page, $modals]) => {
|
||||||
return $modals[$page.url.hash.slice(1)]
|
const ids = getIdsFromHash($page.url.hash)
|
||||||
|
|
||||||
|
return $modals[ids.at(-1) || ""]
|
||||||
})
|
})
|
||||||
|
|
||||||
export const pushModal = (
|
export const pushModal = (
|
||||||
@@ -35,10 +46,12 @@ export const pushModal = (
|
|||||||
) => {
|
) => {
|
||||||
const id = randomId()
|
const id = randomId()
|
||||||
const path = options.path || ""
|
const path = options.path || ""
|
||||||
|
const existingIds = getIdsFromHash(get(page).url.hash)
|
||||||
|
const ids = options.nested ? [...existingIds, id] : [id]
|
||||||
|
|
||||||
modals.update(assoc(id, {id, component, props, options}))
|
modals.update(assoc(id, {id, component, props, options}))
|
||||||
|
|
||||||
goto(path + "#" + id, {replaceState: options.replaceState})
|
goto(path + "#" + ids.join(","), {replaceState: options.replaceState})
|
||||||
|
|
||||||
return id
|
return id
|
||||||
}
|
}
|
||||||
@@ -49,7 +62,24 @@ export const pushDrawer = (
|
|||||||
options: ModalOptions = {},
|
options: ModalOptions = {},
|
||||||
) => pushModal(component, props, {...options, drawer: true})
|
) => pushModal(component, props, {...options, drawer: true})
|
||||||
|
|
||||||
|
export const popModal = () => {
|
||||||
|
const url = get(page).url
|
||||||
|
const ids = getIdsFromHash(url.hash)
|
||||||
|
|
||||||
|
if (ids.length === 0) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const next = ids.slice(0, -1).join(",")
|
||||||
|
const hash = next ? `#${next}` : ""
|
||||||
|
|
||||||
|
goto(url.pathname + url.search + hash, {replaceState: true})
|
||||||
|
}
|
||||||
|
|
||||||
export const clearModals = () => {
|
export const clearModals = () => {
|
||||||
|
const url = get(page).url
|
||||||
|
|
||||||
|
goto(url.pathname + url.search, {replaceState: true})
|
||||||
modals.update(always({}))
|
modals.update(always({}))
|
||||||
emitter.emit("close")
|
emitter.emit("close")
|
||||||
}
|
}
|
||||||
|
|||||||
+29
-521
@@ -1,87 +1,24 @@
|
|||||||
import type {Unsubscriber, Readable, Subscriber} from "svelte/store"
|
import {derived} from "svelte/store"
|
||||||
import {derived, get} from "svelte/store"
|
|
||||||
import {Capacitor} from "@capacitor/core"
|
|
||||||
import {Badge} from "@capawesome/capacitor-badge"
|
import {Badge} from "@capawesome/capacitor-badge"
|
||||||
import {PushNotifications} from "@capacitor/push-notifications"
|
|
||||||
import type {ActionPerformed, RegistrationError, Token} from "@capacitor/push-notifications"
|
|
||||||
import {synced, throttled, withGetter} from "@welshman/store"
|
import {synced, throttled, withGetter} from "@welshman/store"
|
||||||
import {load, LOCAL_RELAY_URL} from "@welshman/net"
|
import {pubkey, tracker, repository, relaysByUrl} from "@welshman/app"
|
||||||
import {
|
import {assoc, prop, first, identity, groupBy, now} from "@welshman/lib"
|
||||||
pubkey,
|
import type {TrustedEvent} from "@welshman/util"
|
||||||
tracker,
|
|
||||||
repository,
|
|
||||||
publishThunk,
|
|
||||||
loadRelay,
|
|
||||||
relaysByUrl,
|
|
||||||
waitForThunkError,
|
|
||||||
userMessagingRelayList,
|
|
||||||
} from "@welshman/app"
|
|
||||||
import {
|
|
||||||
on,
|
|
||||||
call,
|
|
||||||
find,
|
|
||||||
assoc,
|
|
||||||
poll,
|
|
||||||
prop,
|
|
||||||
hash,
|
|
||||||
spec,
|
|
||||||
first,
|
|
||||||
identity,
|
|
||||||
now,
|
|
||||||
maybe,
|
|
||||||
throttle,
|
|
||||||
} from "@welshman/lib"
|
|
||||||
import type {TrustedEvent, Filter} from "@welshman/util"
|
|
||||||
import {deriveEventsByIdByUrl} from "@welshman/store"
|
import {deriveEventsByIdByUrl} from "@welshman/store"
|
||||||
|
import {sortEventsDesc, getTagValue} from "@welshman/util"
|
||||||
|
import {makeSpacePath, makeRoomPath, makeSpaceChatPath, makeChatPath} from "@app/util/routes"
|
||||||
import {
|
import {
|
||||||
DELETE,
|
|
||||||
getTagValue,
|
|
||||||
getPubkeyTagValues,
|
|
||||||
getRelaysFromList,
|
|
||||||
matchFilter,
|
|
||||||
matchFilters,
|
|
||||||
getIdFilters,
|
|
||||||
sortEventsDesc,
|
|
||||||
makeEvent,
|
|
||||||
Address,
|
|
||||||
} from "@welshman/util"
|
|
||||||
import {buildUrl} from "@lib/util"
|
|
||||||
import {
|
|
||||||
makeSpacePath,
|
|
||||||
makeRoomPath,
|
|
||||||
makeSpaceChatPath,
|
|
||||||
makeChatPath,
|
|
||||||
getEventPath,
|
|
||||||
goToEvent,
|
|
||||||
} from "@app/util/routes"
|
|
||||||
import {
|
|
||||||
DM_KINDS,
|
|
||||||
CONTENT_KINDS,
|
|
||||||
MESSAGE_KINDS,
|
MESSAGE_KINDS,
|
||||||
PUSH_BRIDGE,
|
|
||||||
PUSH_SERVER,
|
|
||||||
notificationSettings,
|
notificationSettings,
|
||||||
notificationState,
|
|
||||||
chatsById,
|
chatsById,
|
||||||
userSettingsValues,
|
|
||||||
userGroupList,
|
userGroupList,
|
||||||
getSpaceUrlsFromGroupList,
|
getSpaceUrlsFromGroupList,
|
||||||
getSpaceRoomsFromGroupList,
|
|
||||||
makeCommentFilter,
|
makeCommentFilter,
|
||||||
userSpaceUrls,
|
|
||||||
shouldNotify,
|
|
||||||
hasNip29,
|
hasNip29,
|
||||||
device,
|
|
||||||
} from "@app/core/state"
|
} from "@app/core/state"
|
||||||
import {kv} from "@app/core/storage"
|
import {kv} from "@app/core/storage"
|
||||||
import {goto} from "$app/navigation"
|
|
||||||
import {page} from "$app/stores"
|
import {page} from "$app/stores"
|
||||||
|
export {Push} from "@app/util/push"
|
||||||
// Temporarily copied from welshman
|
|
||||||
|
|
||||||
type Stores = Readable<any> | [Readable<any>, ...Array<Readable<any>>] | Array<Readable<any>>
|
|
||||||
|
|
||||||
const merged = <S extends Stores>(stores: S) => derived(stores, identity)
|
|
||||||
|
|
||||||
// Checked state
|
// Checked state
|
||||||
|
|
||||||
@@ -108,13 +45,9 @@ export const syncChecked = () => {
|
|||||||
.map((_, i, segments) => segments.slice(0, i + 1).join("/"))
|
.map((_, i, segments) => segments.slice(0, i + 1).join("/"))
|
||||||
.slice(1)
|
.slice(1)
|
||||||
|
|
||||||
// Set checked when we enter and when we leave a given page
|
|
||||||
return page.subscribe($page => {
|
return page.subscribe($page => {
|
||||||
|
// Set checked when we leave a given page
|
||||||
checked.update($checked => {
|
checked.update($checked => {
|
||||||
for (const path of getPaths($page.url.pathname)) {
|
|
||||||
$checked[path] = now()
|
|
||||||
}
|
|
||||||
|
|
||||||
for (const path of getPaths(prev)) {
|
for (const path of getPaths(prev)) {
|
||||||
$checked[path] = now()
|
$checked[path] = now()
|
||||||
}
|
}
|
||||||
@@ -122,6 +55,17 @@ export const syncChecked = () => {
|
|||||||
return $checked
|
return $checked
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// Set checked when we visit a given page - but delay it a tad
|
||||||
|
setTimeout(() => {
|
||||||
|
checked.update($checked => {
|
||||||
|
for (const path of getPaths($page.url.pathname)) {
|
||||||
|
$checked[path] = now()
|
||||||
|
}
|
||||||
|
|
||||||
|
return $checked
|
||||||
|
})
|
||||||
|
}, 300)
|
||||||
|
|
||||||
prev = $page.url.pathname
|
prev = $page.url.pathname
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -130,7 +74,7 @@ export const syncChecked = () => {
|
|||||||
|
|
||||||
export const allNotifications = derived(
|
export const allNotifications = derived(
|
||||||
throttled(
|
throttled(
|
||||||
2000,
|
1000,
|
||||||
derived(
|
derived(
|
||||||
[
|
[
|
||||||
pubkey,
|
pubkey,
|
||||||
@@ -180,30 +124,23 @@ export const allNotifications = derived(
|
|||||||
|
|
||||||
for (const url of getSpaceUrlsFromGroupList($userGroupList)) {
|
for (const url of getSpaceUrlsFromGroupList($userGroupList)) {
|
||||||
const spacePath = makeSpacePath(url)
|
const spacePath = makeSpacePath(url)
|
||||||
const spacePathMobile = spacePath + ":mobile"
|
const events = sortEventsDesc((eventsByIdByUrl.get(url) || new Map()).values())
|
||||||
const eventsById = eventsByIdByUrl.get(url) || new Map()
|
|
||||||
const latestEvent = first(sortEventsDesc(eventsById.values()))
|
|
||||||
|
|
||||||
if (hasNotification(spacePath, latestEvent)) {
|
|
||||||
paths.add(spacePath)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (hasNip29($relaysByUrl.get(url))) {
|
if (hasNip29($relaysByUrl.get(url))) {
|
||||||
for (const h of getSpaceRoomsFromGroupList(url, $userGroupList)) {
|
for (const [h, [latestEvent]] of groupBy(e => getTagValue("h", e.tags), events)) {
|
||||||
const roomPath = makeRoomPath(url, h)
|
if (h) {
|
||||||
const latestEvent = find(e => e.tags.some(spec(["h", h])), eventsById.values())
|
const roomPath = makeRoomPath(url, h)
|
||||||
|
|
||||||
if (hasNotification(roomPath, latestEvent)) {
|
if (hasNotification(roomPath, latestEvent)) {
|
||||||
paths.add(spacePathMobile)
|
paths.add(spacePath)
|
||||||
paths.add(spacePath)
|
paths.add(roomPath)
|
||||||
paths.add(roomPath)
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
const messagesPath = makeSpaceChatPath(url)
|
const messagesPath = makeSpaceChatPath(url)
|
||||||
|
|
||||||
if (hasNotification(messagesPath, first(eventsById.values()))) {
|
if (hasNotification(messagesPath, first(events))) {
|
||||||
paths.add(spacePathMobile)
|
|
||||||
paths.add(spacePath)
|
paths.add(spacePath)
|
||||||
paths.add(messagesPath)
|
paths.add(messagesPath)
|
||||||
}
|
}
|
||||||
@@ -218,51 +155,6 @@ export const notifications = derived([page, allNotifications], ([$page, $allNoti
|
|||||||
return new Set([...$allNotifications].filter(p => !$page.url.pathname.startsWith(p)))
|
return new Set([...$allNotifications].filter(p => !$page.url.pathname.startsWith(p)))
|
||||||
})
|
})
|
||||||
|
|
||||||
export const onNotification = call(() => {
|
|
||||||
const allFilters = [{kinds: [...MESSAGE_KINDS, ...DM_KINDS]}, makeCommentFilter(MESSAGE_KINDS)]
|
|
||||||
const filters = allFilters.map(assoc("since", now()))
|
|
||||||
const subscribers: Subscriber<TrustedEvent>[] = []
|
|
||||||
|
|
||||||
let unsubscribe: Unsubscriber | undefined
|
|
||||||
|
|
||||||
return (f: (event: TrustedEvent) => void) => {
|
|
||||||
subscribers.push(f)
|
|
||||||
|
|
||||||
if (!unsubscribe) {
|
|
||||||
unsubscribe = on(repository, "update", ({added}) => {
|
|
||||||
const $pubkey = pubkey.get()
|
|
||||||
|
|
||||||
for (const event of added) {
|
|
||||||
if (event.pubkey == $pubkey) {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
const h = getTagValue("h", event.tags)
|
|
||||||
|
|
||||||
if (Array.from(tracker.getRelays(event.id)).every(url => !shouldNotify(url, h))) {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
if (matchFilters(filters, event)) {
|
|
||||||
for (const f of subscribers) {
|
|
||||||
f(event)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
return () => {
|
|
||||||
subscribers.splice(subscribers.indexOf(f), 1)
|
|
||||||
|
|
||||||
if (subscribers.length === 0) {
|
|
||||||
unsubscribe?.()
|
|
||||||
unsubscribe = undefined
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
// Badges
|
// Badges
|
||||||
|
|
||||||
export const syncBadges = () =>
|
export const syncBadges = () =>
|
||||||
@@ -287,387 +179,3 @@ export const clearBadges = async () => {
|
|||||||
// pass - firefox doesn't support this
|
// pass - firefox doesn't support this
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Push notifications
|
|
||||||
|
|
||||||
interface IPushAdapter {
|
|
||||||
request: (prompt?: boolean) => Promise<string>
|
|
||||||
disable: () => Promise<void>
|
|
||||||
enable: () => Promise<void>
|
|
||||||
}
|
|
||||||
|
|
||||||
class CapacitorNotifications implements IPushAdapter {
|
|
||||||
_controller = maybe<AbortController>()
|
|
||||||
|
|
||||||
async request(prompt = true) {
|
|
||||||
let status = await PushNotifications.checkPermissions()
|
|
||||||
|
|
||||||
if (prompt && ["prompt", "prompt-with-rationale"].includes(status.receive)) {
|
|
||||||
status = await PushNotifications.requestPermissions()
|
|
||||||
}
|
|
||||||
|
|
||||||
if (status.receive !== "granted") {
|
|
||||||
return status.receive
|
|
||||||
}
|
|
||||||
|
|
||||||
let {token} = notificationState.get()
|
|
||||||
|
|
||||||
if (!token) {
|
|
||||||
const listeners = [
|
|
||||||
PushNotifications.addListener("registration", ({value}: Token) => {
|
|
||||||
token = value
|
|
||||||
}),
|
|
||||||
PushNotifications.addListener("registrationError", (error: RegistrationError) => {
|
|
||||||
console.error(error)
|
|
||||||
}),
|
|
||||||
]
|
|
||||||
|
|
||||||
await Promise.all([
|
|
||||||
PushNotifications.register(),
|
|
||||||
poll({
|
|
||||||
condition: () => Boolean(token),
|
|
||||||
signal: AbortSignal.timeout(5000),
|
|
||||||
}),
|
|
||||||
])
|
|
||||||
|
|
||||||
listeners.forEach(p => p.then(listener => listener.remove()))
|
|
||||||
notificationState.update(assoc("token", token))
|
|
||||||
}
|
|
||||||
|
|
||||||
return token ? "granted" : "denied"
|
|
||||||
}
|
|
||||||
|
|
||||||
async _syncServer(signal: AbortSignal) {
|
|
||||||
const {token, subscription} = notificationState.get()
|
|
||||||
|
|
||||||
if (!token) {
|
|
||||||
throw new Error("Attempted to sync push server without a token")
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!subscription) {
|
|
||||||
try {
|
|
||||||
const channel = Capacitor.getPlatform() === "ios" ? "apns" : "fcm"
|
|
||||||
const url = buildUrl(PUSH_SERVER, "subscription", channel)
|
|
||||||
const res = await fetch(url, {
|
|
||||||
signal,
|
|
||||||
method: "POST",
|
|
||||||
body: JSON.stringify({token}),
|
|
||||||
headers: {
|
|
||||||
Accept: "application/json",
|
|
||||||
"Content-Type": "application/json",
|
|
||||||
},
|
|
||||||
})
|
|
||||||
|
|
||||||
if (!res.ok) {
|
|
||||||
console.warn(`Failed to register with push server (status ${res.status})`)
|
|
||||||
} else {
|
|
||||||
const json = await res.json()
|
|
||||||
|
|
||||||
if (json?.callback && json?.key) {
|
|
||||||
notificationState.update(assoc("subscription", json))
|
|
||||||
} else {
|
|
||||||
console.warn("Failed to register with push server (bad response)")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
console.warn("Failed to register with push server:", e)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
_getSubscriptionIdentifier = (relay: string, key: string) =>
|
|
||||||
String(hash(relay + key + device.get()))
|
|
||||||
|
|
||||||
_getPushUrl = async (url: string) => {
|
|
||||||
for (const candidate of [url, PUSH_BRIDGE]) {
|
|
||||||
const relay = await loadRelay(candidate)
|
|
||||||
|
|
||||||
if (relay?.supported_nips?.map(String)?.includes("9a")) {
|
|
||||||
return candidate
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
_syncRelay = async (relay: string, key: string, filters: Filter[], ignore: Filter[] = []) => {
|
|
||||||
const {subscription} = notificationState.get()
|
|
||||||
|
|
||||||
if (!subscription) {
|
|
||||||
console.warn(`Failed to subscribe ${relay} to notifications: no subscription`)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
const url = await this._getPushUrl(relay)
|
|
||||||
|
|
||||||
if (!url) {
|
|
||||||
console.warn(`Failed to subscribe ${relay} to notifications: unsupported`)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
const identifier = this._getSubscriptionIdentifier(relay, key)
|
|
||||||
|
|
||||||
const thunk = publishThunk({
|
|
||||||
relays: [url],
|
|
||||||
event: makeEvent(30390, {
|
|
||||||
tags: [
|
|
||||||
["d", identifier],
|
|
||||||
["relay", relay],
|
|
||||||
["callback", subscription.callback],
|
|
||||||
...ignore.map(filter => ["ignore", JSON.stringify(filter)]),
|
|
||||||
...filters.map(filter => ["filter", JSON.stringify(filter)]),
|
|
||||||
],
|
|
||||||
}),
|
|
||||||
})
|
|
||||||
|
|
||||||
const error = await waitForThunkError(thunk)
|
|
||||||
|
|
||||||
if (error) {
|
|
||||||
console.warn(`Failed to subscribe ${relay} to ${key} notifications:`, error)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
_unsyncRelay = async (relay: string, key: string) => {
|
|
||||||
const url = await this._getPushUrl(relay)
|
|
||||||
|
|
||||||
if (!url) {
|
|
||||||
console.warn(`Failed to unsubscribe ${relay} from notifications: unsupported`)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
const relays = [url]
|
|
||||||
const identifier = this._getSubscriptionIdentifier(relay, key)
|
|
||||||
const address = new Address(30390, pubkey.get()!, identifier).toString()
|
|
||||||
const event = makeEvent(DELETE, {tags: [["a", address]]})
|
|
||||||
const error = await waitForThunkError(publishThunk({relays, event}))
|
|
||||||
|
|
||||||
if (error) {
|
|
||||||
console.warn(`Failed to unsubscribe ${relay} from notifications:`, error)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async _syncSpaceSubscription(signal: AbortSignal) {
|
|
||||||
signal.addEventListener(
|
|
||||||
"abort",
|
|
||||||
merged([userSpaceUrls, notificationSettings, userSettingsValues]).subscribe(
|
|
||||||
throttle(3000, ([$userSpaceUrls, {spaces, mentions}, {alerts}]) => {
|
|
||||||
const baseFilters = [{kinds: MESSAGE_KINDS}, makeCommentFilter(CONTENT_KINDS)]
|
|
||||||
|
|
||||||
for (const url of $userSpaceUrls) {
|
|
||||||
const {notify = true, exceptions = []} = alerts.find(spec({url})) || {}
|
|
||||||
const filters: Filter[] = []
|
|
||||||
const ignore: Filter[] = []
|
|
||||||
|
|
||||||
// Build filters based on spaces setting
|
|
||||||
if (spaces) {
|
|
||||||
if (notify) {
|
|
||||||
// notify=true: exceptions are opt-out (exclude those rooms)
|
|
||||||
if (exceptions.length > 0) {
|
|
||||||
ignore.push({"#h": exceptions})
|
|
||||||
}
|
|
||||||
// Include all other content
|
|
||||||
filters.push(...baseFilters)
|
|
||||||
} else {
|
|
||||||
// notify=false: exceptions are opt-in (only include those rooms)
|
|
||||||
if (exceptions.length > 0) {
|
|
||||||
filters.push(...baseFilters.map(f => ({...f, "#h": exceptions})))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Build filters for mentions - always notify for p-tagged content
|
|
||||||
if (mentions) {
|
|
||||||
filters.push(...baseFilters.map(f => ({...f, "#p": [pubkey.get()!]})))
|
|
||||||
}
|
|
||||||
|
|
||||||
// Sync or unsync based on whether we have filters
|
|
||||||
if (filters.length > 0) {
|
|
||||||
this._syncRelay(url, "spaces", filters, ignore)
|
|
||||||
} else {
|
|
||||||
this._unsyncRelay(url, "spaces")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
async _syncMessageSubscription(signal: AbortSignal) {
|
|
||||||
signal.addEventListener(
|
|
||||||
"abort",
|
|
||||||
merged([userMessagingRelayList, notificationSettings]).subscribe(
|
|
||||||
throttle(3000, ([$userMessagingRelayList, {messages}]) => {
|
|
||||||
for (const url of getRelaysFromList($userMessagingRelayList)) {
|
|
||||||
if (messages) {
|
|
||||||
this._syncRelay(url, "messages", [{kinds: DM_KINDS, "#p": [pubkey.get()!]}])
|
|
||||||
} else {
|
|
||||||
this._unsyncRelay(url, "messages")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
async enable() {
|
|
||||||
if (!this._controller) {
|
|
||||||
this._controller = new AbortController()
|
|
||||||
|
|
||||||
PushNotifications.addListener(
|
|
||||||
"pushNotificationActionPerformed",
|
|
||||||
async (action: ActionPerformed) => {
|
|
||||||
const {relay, id} = action.notification.data
|
|
||||||
|
|
||||||
const [event] = await load({
|
|
||||||
relays: [relay, LOCAL_RELAY_URL],
|
|
||||||
filters: getIdFilters([id]),
|
|
||||||
})
|
|
||||||
|
|
||||||
if (event) {
|
|
||||||
goto(await getEventPath(event, [relay]))
|
|
||||||
} else {
|
|
||||||
goto(makeSpacePath(relay))
|
|
||||||
}
|
|
||||||
},
|
|
||||||
)
|
|
||||||
|
|
||||||
this._controller.signal.addEventListener("abort", () => {
|
|
||||||
PushNotifications.removeAllListeners()
|
|
||||||
})
|
|
||||||
|
|
||||||
try {
|
|
||||||
await this._syncServer(this._controller.signal)
|
|
||||||
await this._syncSpaceSubscription(this._controller.signal)
|
|
||||||
await this._syncMessageSubscription(this._controller.signal)
|
|
||||||
} catch (e) {
|
|
||||||
console.error(e)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async disable() {
|
|
||||||
this._controller?.abort()
|
|
||||||
this._controller = undefined
|
|
||||||
|
|
||||||
const {subscription} = notificationState.get()
|
|
||||||
|
|
||||||
if (subscription) {
|
|
||||||
const res = await fetch(buildUrl(PUSH_SERVER, "subscription", subscription.key), {
|
|
||||||
method: "delete",
|
|
||||||
})
|
|
||||||
|
|
||||||
if (!res.ok) {
|
|
||||||
console.warn("Failed to delete push subscription")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
notificationState.set({})
|
|
||||||
|
|
||||||
await Promise.all(get(userSpaceUrls).map(url => this._unsyncRelay(url, "spaces")))
|
|
||||||
|
|
||||||
await Promise.all(
|
|
||||||
getRelaysFromList(get(userMessagingRelayList)).map(url => this._unsyncRelay(url, "messages")),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class WebNotifications implements IPushAdapter {
|
|
||||||
_unsubscriber = maybe<Unsubscriber>()
|
|
||||||
|
|
||||||
async request(prompt = true) {
|
|
||||||
if (prompt && Notification?.permission === "default") {
|
|
||||||
await Notification.requestPermission()
|
|
||||||
}
|
|
||||||
|
|
||||||
return Notification?.permission || "denied"
|
|
||||||
}
|
|
||||||
|
|
||||||
_notify(event: TrustedEvent, title: string, body: string) {
|
|
||||||
console.log("notify:", event)
|
|
||||||
|
|
||||||
const notification = new Notification(title, {
|
|
||||||
body,
|
|
||||||
tag: event.id,
|
|
||||||
icon: "/icon.png",
|
|
||||||
badge: "/icon.png",
|
|
||||||
})
|
|
||||||
|
|
||||||
notification.onclick = () => {
|
|
||||||
window.focus()
|
|
||||||
goToEvent(event)
|
|
||||||
notification.close()
|
|
||||||
}
|
|
||||||
|
|
||||||
const onVisibilityChange = () => {
|
|
||||||
if (document.visibilityState === "visible") {
|
|
||||||
notification.close()
|
|
||||||
document.removeEventListener("visibilitychange", onVisibilityChange)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
document.addEventListener("visibilitychange", onVisibilityChange)
|
|
||||||
}
|
|
||||||
|
|
||||||
async enable() {
|
|
||||||
if (!this._unsubscriber) {
|
|
||||||
this._unsubscriber = onNotification(event => {
|
|
||||||
const {push, messages, mentions, spaces} = notificationSettings.get()
|
|
||||||
|
|
||||||
if (push && document.hidden && Notification?.permission === "granted") {
|
|
||||||
if (messages && matchFilter({kinds: DM_KINDS}, event)) {
|
|
||||||
this._notify(event, "New direct message", "Someone sent you a direct message.")
|
|
||||||
} else if (
|
|
||||||
mentions &&
|
|
||||||
event.pubkey !== pubkey.get() &&
|
|
||||||
getPubkeyTagValues(event.tags).includes(pubkey.get()!)
|
|
||||||
) {
|
|
||||||
this._notify(event, "Someone mentioned you", "Someone tagged you in a message.")
|
|
||||||
} else if (spaces) {
|
|
||||||
this._notify(event, "New activity", "Someone posted a new message.")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async disable() {
|
|
||||||
this._unsubscriber?.()
|
|
||||||
this._unsubscriber = undefined
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export class Push {
|
|
||||||
static _adapter: IPushAdapter | undefined
|
|
||||||
|
|
||||||
static _getAdapter() {
|
|
||||||
if (!Push._adapter) {
|
|
||||||
if (Capacitor.isNativePlatform()) {
|
|
||||||
Push._adapter = new CapacitorNotifications()
|
|
||||||
} else {
|
|
||||||
Push._adapter = new WebNotifications()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return Push._adapter
|
|
||||||
}
|
|
||||||
|
|
||||||
static request() {
|
|
||||||
return Push._getAdapter().request()
|
|
||||||
}
|
|
||||||
|
|
||||||
static disable() {
|
|
||||||
return Push._getAdapter().disable()
|
|
||||||
}
|
|
||||||
|
|
||||||
static enable() {
|
|
||||||
return Push._getAdapter().enable()
|
|
||||||
}
|
|
||||||
|
|
||||||
static sync() {
|
|
||||||
return notificationSettings.subscribe(({push}) => {
|
|
||||||
if (push) {
|
|
||||||
Push.enable()
|
|
||||||
} else {
|
|
||||||
Push.disable()
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -0,0 +1,92 @@
|
|||||||
|
import {throttle} from "throttle-debounce"
|
||||||
|
import {App} from "@capacitor/app"
|
||||||
|
import {registerPlugin} from "@capacitor/core"
|
||||||
|
import {session} from "@welshman/app"
|
||||||
|
import type {Session} from "@welshman/app"
|
||||||
|
import {maybe, now} from "@welshman/lib"
|
||||||
|
import type {Filter} from "@welshman/util"
|
||||||
|
import {pushState} from "@app/core/state"
|
||||||
|
import type {IPushAdapter} from "@app/util/push/adapters/common"
|
||||||
|
import {requestPermissions, syncRelaySubscriptions} from "@app/util/push/adapters/common"
|
||||||
|
|
||||||
|
type AndroidFallbackSubscription = {
|
||||||
|
relay: string
|
||||||
|
key: string
|
||||||
|
filters: Array<Filter>
|
||||||
|
ignore: Array<Filter>
|
||||||
|
}
|
||||||
|
|
||||||
|
type AndroidPushFallbackState = {
|
||||||
|
session?: Session
|
||||||
|
activeSince?: number
|
||||||
|
subscriptions?: Array<AndroidFallbackSubscription>
|
||||||
|
}
|
||||||
|
|
||||||
|
type AndroidPushFallbackPlugin = {
|
||||||
|
syncState: (args: {state: AndroidPushFallbackState}) => Promise<void>
|
||||||
|
}
|
||||||
|
|
||||||
|
const AndroidPushFallback = registerPlugin<AndroidPushFallbackPlugin>("AndroidPushFallback")
|
||||||
|
|
||||||
|
export class AndroidFallbackNotifications implements IPushAdapter {
|
||||||
|
_controller = maybe<AbortController>()
|
||||||
|
_subscriptions = new Map<string, AndroidFallbackSubscription>()
|
||||||
|
_activeSince = now()
|
||||||
|
|
||||||
|
async request() {
|
||||||
|
return requestPermissions()
|
||||||
|
}
|
||||||
|
|
||||||
|
async enable() {
|
||||||
|
if (!this._controller) {
|
||||||
|
this._controller = new AbortController()
|
||||||
|
|
||||||
|
const doSync = throttle(1000, () => {
|
||||||
|
AndroidPushFallback.syncState({
|
||||||
|
state: {
|
||||||
|
session: session.get(),
|
||||||
|
activeSince: this._activeSince,
|
||||||
|
subscriptions: Array.from(this._subscriptions.values()),
|
||||||
|
},
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
let appStateListener: Awaited<ReturnType<typeof App.addListener>> | undefined
|
||||||
|
|
||||||
|
App.addListener("appStateChange", ({isActive}) => {
|
||||||
|
if (!isActive) {
|
||||||
|
this._activeSince = now()
|
||||||
|
doSync()
|
||||||
|
}
|
||||||
|
}).then(handle => {
|
||||||
|
appStateListener = handle
|
||||||
|
})
|
||||||
|
|
||||||
|
this._controller.signal.addEventListener("abort", () => {
|
||||||
|
appStateListener?.remove()
|
||||||
|
})
|
||||||
|
|
||||||
|
syncRelaySubscriptions(this._controller.signal, async (relay, key, filters, ignore) => {
|
||||||
|
if (filters.length > 0) {
|
||||||
|
this._subscriptions.set(`${relay}:${key}`, {relay, key, filters, ignore})
|
||||||
|
} else {
|
||||||
|
this._subscriptions.delete(`${relay}:${key}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
doSync()
|
||||||
|
})
|
||||||
|
|
||||||
|
pushState.set({useFallback: true})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async disable() {
|
||||||
|
this._controller?.abort()
|
||||||
|
this._controller = undefined
|
||||||
|
this._subscriptions.clear()
|
||||||
|
|
||||||
|
await AndroidPushFallback.syncState({state: {}})
|
||||||
|
|
||||||
|
pushState.set({})
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,198 @@
|
|||||||
|
import {get} from "svelte/store"
|
||||||
|
import {Capacitor} from "@capacitor/core"
|
||||||
|
import {PushNotifications} from "@capacitor/push-notifications"
|
||||||
|
import {
|
||||||
|
pubkey,
|
||||||
|
publishThunk,
|
||||||
|
loadRelay,
|
||||||
|
waitForThunkError,
|
||||||
|
userMessagingRelayList,
|
||||||
|
} from "@welshman/app"
|
||||||
|
import {assoc, hash, maybe} from "@welshman/lib"
|
||||||
|
import type {Filter} from "@welshman/util"
|
||||||
|
import {DELETE, getRelaysFromList, makeEvent, Address} from "@welshman/util"
|
||||||
|
import {buildUrl} from "@lib/util"
|
||||||
|
import {PUSH_BRIDGE, PUSH_SERVER, pushState, userSpaceUrls, device} from "@app/core/state"
|
||||||
|
import type {IPushAdapter} from "@app/util/push/adapters/common"
|
||||||
|
import {
|
||||||
|
onPushNotificationAction,
|
||||||
|
syncRelaySubscriptions,
|
||||||
|
requestPermissions,
|
||||||
|
requestToken,
|
||||||
|
} from "@app/util/push/adapters/common"
|
||||||
|
|
||||||
|
export class CapacitorNotifications implements IPushAdapter {
|
||||||
|
_controller = maybe<AbortController>()
|
||||||
|
|
||||||
|
async request() {
|
||||||
|
const status = await requestPermissions()
|
||||||
|
|
||||||
|
if (status !== "granted") {
|
||||||
|
return status
|
||||||
|
}
|
||||||
|
|
||||||
|
const {token, error = "denied"} = await requestToken()
|
||||||
|
|
||||||
|
pushState.update(assoc("token", token))
|
||||||
|
|
||||||
|
return token ? "granted" : error
|
||||||
|
}
|
||||||
|
|
||||||
|
async _syncServer(signal: AbortSignal) {
|
||||||
|
const {token, subscription} = pushState.get()
|
||||||
|
|
||||||
|
if (!token) {
|
||||||
|
throw new Error("Attempted to sync push server without a token")
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!subscription) {
|
||||||
|
try {
|
||||||
|
const channel = Capacitor.getPlatform() === "ios" ? "apns" : "fcm"
|
||||||
|
const url = buildUrl(PUSH_SERVER, "subscription", channel)
|
||||||
|
const res = await fetch(url, {
|
||||||
|
signal,
|
||||||
|
method: "POST",
|
||||||
|
body: JSON.stringify({token}),
|
||||||
|
headers: {
|
||||||
|
Accept: "application/json",
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
if (!res.ok) {
|
||||||
|
console.warn(`Failed to register with push server (status ${res.status})`)
|
||||||
|
} else {
|
||||||
|
const json = await res.json()
|
||||||
|
|
||||||
|
if (json?.callback && json?.key) {
|
||||||
|
pushState.update(assoc("subscription", json))
|
||||||
|
} else {
|
||||||
|
console.warn("Failed to register with push server (bad response)")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.warn("Failed to register with push server:", e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_getSubscriptionIdentifier = (relay: string, key: string) =>
|
||||||
|
String(hash(relay + key + device.get()))
|
||||||
|
|
||||||
|
_getPushUrl = async (url: string) => {
|
||||||
|
for (const candidate of [url, PUSH_BRIDGE]) {
|
||||||
|
const relay = await loadRelay(candidate)
|
||||||
|
|
||||||
|
if (relay?.supported_nips?.map(String)?.includes("9a")) {
|
||||||
|
return candidate
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_syncRelay = async (relay: string, key: string, filters: Filter[], ignore: Filter[] = []) => {
|
||||||
|
const {subscription} = pushState.get()
|
||||||
|
|
||||||
|
if (!subscription) {
|
||||||
|
console.warn(`Failed to subscribe ${relay} to notifications: no subscription`)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const url = await this._getPushUrl(relay)
|
||||||
|
|
||||||
|
if (!url) {
|
||||||
|
console.warn(`Failed to subscribe ${relay} to notifications: unsupported`)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const identifier = this._getSubscriptionIdentifier(relay, key)
|
||||||
|
|
||||||
|
const thunk = publishThunk({
|
||||||
|
relays: [url],
|
||||||
|
event: makeEvent(30390, {
|
||||||
|
tags: [
|
||||||
|
["d", identifier],
|
||||||
|
["relay", relay],
|
||||||
|
["callback", subscription.callback],
|
||||||
|
...ignore.map(filter => ["ignore", JSON.stringify(filter)]),
|
||||||
|
...filters.map(filter => ["filter", JSON.stringify(filter)]),
|
||||||
|
],
|
||||||
|
}),
|
||||||
|
})
|
||||||
|
|
||||||
|
const error = await waitForThunkError(thunk)
|
||||||
|
|
||||||
|
if (error) {
|
||||||
|
console.warn(`Failed to subscribe ${relay} to ${key} notifications:`, error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_unsyncRelay = async (relay: string, key: string) => {
|
||||||
|
const url = await this._getPushUrl(relay)
|
||||||
|
|
||||||
|
if (!url) {
|
||||||
|
console.warn(`Failed to unsubscribe ${relay} from notifications: unsupported`)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const relays = [url]
|
||||||
|
const identifier = this._getSubscriptionIdentifier(relay, key)
|
||||||
|
const address = new Address(30390, pubkey.get()!, identifier).toString()
|
||||||
|
const event = makeEvent(DELETE, {tags: [["a", address]]})
|
||||||
|
const error = await waitForThunkError(publishThunk({relays, event}))
|
||||||
|
|
||||||
|
if (error) {
|
||||||
|
console.warn(`Failed to unsubscribe ${relay} from notifications:`, error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async enable() {
|
||||||
|
if (!this._controller) {
|
||||||
|
this._controller = new AbortController()
|
||||||
|
|
||||||
|
PushNotifications.addListener("pushNotificationActionPerformed", onPushNotificationAction)
|
||||||
|
|
||||||
|
this._controller.signal.addEventListener("abort", () => {
|
||||||
|
PushNotifications.removeAllListeners()
|
||||||
|
})
|
||||||
|
|
||||||
|
try {
|
||||||
|
await this._syncServer(this._controller.signal)
|
||||||
|
|
||||||
|
syncRelaySubscriptions(this._controller.signal, (url, key, filters, ignore) => {
|
||||||
|
if (filters.length > 0) {
|
||||||
|
this._syncRelay(url, key, filters, ignore)
|
||||||
|
} else {
|
||||||
|
this._unsyncRelay(url, key)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
} catch (e) {
|
||||||
|
console.error(e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async disable() {
|
||||||
|
this._controller?.abort()
|
||||||
|
this._controller = undefined
|
||||||
|
|
||||||
|
const {subscription} = pushState.get()
|
||||||
|
|
||||||
|
if (subscription) {
|
||||||
|
const res = await fetch(buildUrl(PUSH_SERVER, "subscription", subscription.key), {
|
||||||
|
method: "delete",
|
||||||
|
})
|
||||||
|
|
||||||
|
if (!res.ok) {
|
||||||
|
console.warn("Failed to delete push subscription")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pushState.set({})
|
||||||
|
|
||||||
|
await Promise.all(get(userSpaceUrls).map(url => this._unsyncRelay(url, "spaces")))
|
||||||
|
|
||||||
|
await Promise.all(
|
||||||
|
getRelaysFromList(get(userMessagingRelayList)).map(url => this._unsyncRelay(url, "messages")),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,208 @@
|
|||||||
|
import {goto} from "$app/navigation"
|
||||||
|
import type {Subscriber, Unsubscriber} from "svelte/store"
|
||||||
|
import {
|
||||||
|
PushNotifications,
|
||||||
|
type ActionPerformed,
|
||||||
|
type RegistrationError,
|
||||||
|
type Token,
|
||||||
|
} from "@capacitor/push-notifications"
|
||||||
|
import type {PluginListenerHandle} from "@capacitor/core"
|
||||||
|
import {pubkey, repository, tracker, userMessagingRelayList} from "@welshman/app"
|
||||||
|
import {merged} from "@welshman/store"
|
||||||
|
import {assoc, call, now, on, poll, spec, throttle} from "@welshman/lib"
|
||||||
|
import {load, LOCAL_RELAY_URL} from "@welshman/net"
|
||||||
|
import type {RepositoryUpdate} from "@welshman/net"
|
||||||
|
import {
|
||||||
|
getIdFilters,
|
||||||
|
getRelaysFromList,
|
||||||
|
getTagValue,
|
||||||
|
matchFilters,
|
||||||
|
type Filter,
|
||||||
|
type TrustedEvent,
|
||||||
|
} from "@welshman/util"
|
||||||
|
import {
|
||||||
|
DM_KINDS,
|
||||||
|
CONTENT_KINDS,
|
||||||
|
MESSAGE_KINDS,
|
||||||
|
notificationSettings,
|
||||||
|
pushState,
|
||||||
|
shouldNotify,
|
||||||
|
userSpaceUrls,
|
||||||
|
userSettingsValues,
|
||||||
|
makeCommentFilter,
|
||||||
|
} from "@app/core/state"
|
||||||
|
import {makeSpacePath, getEventPath} from "@app/util/routes"
|
||||||
|
|
||||||
|
export interface IPushAdapter {
|
||||||
|
request: (prompt?: boolean) => Promise<string>
|
||||||
|
disable: () => Promise<void>
|
||||||
|
enable: () => Promise<void>
|
||||||
|
}
|
||||||
|
|
||||||
|
export type PushPermissionResult = {
|
||||||
|
token?: string
|
||||||
|
error?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export const onNotification = call(() => {
|
||||||
|
const allFilters = [{kinds: [...MESSAGE_KINDS, ...DM_KINDS]}, makeCommentFilter(MESSAGE_KINDS)]
|
||||||
|
const filters = allFilters.map(assoc("since", now()))
|
||||||
|
const subscribers: Subscriber<TrustedEvent>[] = []
|
||||||
|
|
||||||
|
let unsubscribe: Unsubscriber | undefined
|
||||||
|
|
||||||
|
return (f: (event: TrustedEvent) => void) => {
|
||||||
|
subscribers.push(f)
|
||||||
|
|
||||||
|
if (!unsubscribe) {
|
||||||
|
unsubscribe = on(repository, "update", ({added}: RepositoryUpdate) => {
|
||||||
|
const $pubkey = pubkey.get()
|
||||||
|
|
||||||
|
for (const event of added) {
|
||||||
|
if (event.pubkey == $pubkey) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
const h = getTagValue("h", event.tags)
|
||||||
|
|
||||||
|
if (Array.from(tracker.getRelays(event.id)).every(url => !shouldNotify(url, h))) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if (matchFilters(filters, event)) {
|
||||||
|
for (const f of subscribers) {
|
||||||
|
f(event)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
subscribers.splice(subscribers.indexOf(f), 1)
|
||||||
|
|
||||||
|
if (subscribers.length === 0) {
|
||||||
|
unsubscribe?.()
|
||||||
|
unsubscribe = undefined
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
export const onPushNotificationAction = async (action: ActionPerformed) => {
|
||||||
|
const {relay, id} = action.notification.data
|
||||||
|
|
||||||
|
const [event] = await load({
|
||||||
|
relays: [relay, LOCAL_RELAY_URL],
|
||||||
|
filters: getIdFilters([id]),
|
||||||
|
})
|
||||||
|
|
||||||
|
if (event) {
|
||||||
|
goto(await getEventPath(event, [relay]))
|
||||||
|
} else {
|
||||||
|
goto(makeSpacePath(relay))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const requestPermissions = async (): Promise<string> => {
|
||||||
|
let status = await PushNotifications.checkPermissions()
|
||||||
|
|
||||||
|
if (["prompt", "prompt-with-rationale"].includes(status.receive)) {
|
||||||
|
status = await PushNotifications.requestPermissions()
|
||||||
|
}
|
||||||
|
|
||||||
|
return status.receive
|
||||||
|
}
|
||||||
|
|
||||||
|
export const requestToken = async (): Promise<PushPermissionResult> => {
|
||||||
|
let {token} = pushState.get()
|
||||||
|
let error = "failed to retrieve token"
|
||||||
|
|
||||||
|
if (!token) {
|
||||||
|
const listeners = [
|
||||||
|
PushNotifications.addListener("registration", ({value}: Token) => {
|
||||||
|
token = value
|
||||||
|
}),
|
||||||
|
PushNotifications.addListener("registrationError", (err: RegistrationError) => {
|
||||||
|
error = err.error
|
||||||
|
}),
|
||||||
|
]
|
||||||
|
|
||||||
|
await Promise.all([
|
||||||
|
PushNotifications.register(),
|
||||||
|
poll({
|
||||||
|
condition: () => Boolean(token),
|
||||||
|
signal: AbortSignal.timeout(5000),
|
||||||
|
}),
|
||||||
|
])
|
||||||
|
|
||||||
|
listeners.forEach(p => p.then((listener: PluginListenerHandle) => listener.remove()))
|
||||||
|
}
|
||||||
|
|
||||||
|
return token ? {token} : {error}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const syncRelaySubscriptions = (
|
||||||
|
signal: AbortSignal,
|
||||||
|
sync: (url: string, key: string, filters: Filter[], ignore: Filter[]) => void,
|
||||||
|
) => {
|
||||||
|
const $pubkey = pubkey.get()
|
||||||
|
|
||||||
|
if (!$pubkey) {
|
||||||
|
throw new Error("Attempted to sync push subscriptions without an active pubkey")
|
||||||
|
}
|
||||||
|
|
||||||
|
const unsubscribeSpaces = merged([
|
||||||
|
userSpaceUrls,
|
||||||
|
notificationSettings,
|
||||||
|
userSettingsValues,
|
||||||
|
]).subscribe(
|
||||||
|
throttle(3000, ([$userSpaceUrls, {spaces, mentions}, {alerts}]) => {
|
||||||
|
const baseFilters = [{kinds: MESSAGE_KINDS}, makeCommentFilter(CONTENT_KINDS)]
|
||||||
|
|
||||||
|
for (const url of $userSpaceUrls) {
|
||||||
|
const {notify = true, exceptions = []} = alerts.find(spec({url})) || {}
|
||||||
|
const filters: Filter[] = []
|
||||||
|
const ignore: Filter[] = []
|
||||||
|
|
||||||
|
if (spaces) {
|
||||||
|
if (notify) {
|
||||||
|
if (exceptions.length > 0) {
|
||||||
|
ignore.push({"#h": exceptions})
|
||||||
|
}
|
||||||
|
filters.push(...baseFilters)
|
||||||
|
} else {
|
||||||
|
if (exceptions.length > 0) {
|
||||||
|
filters.push(...baseFilters.map(f => ({...f, "#h": exceptions})))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (mentions) {
|
||||||
|
filters.push(...baseFilters.map(f => ({...f, "#p": [$pubkey]})))
|
||||||
|
}
|
||||||
|
|
||||||
|
sync(url, "spaces", filters, ignore)
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
|
||||||
|
const unsubscribeMessages = merged([userMessagingRelayList, notificationSettings]).subscribe(
|
||||||
|
throttle(3000, ([$userMessagingRelayList, {messages}]) => {
|
||||||
|
for (const url of getRelaysFromList($userMessagingRelayList)) {
|
||||||
|
const filters: Filter[] = []
|
||||||
|
|
||||||
|
if (messages) {
|
||||||
|
filters.push({kinds: DM_KINDS, "#p": [$pubkey]})
|
||||||
|
}
|
||||||
|
|
||||||
|
sync(url, "messages", filters, [])
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
|
||||||
|
signal.addEventListener("abort", () => {
|
||||||
|
unsubscribeSpaces()
|
||||||
|
unsubscribeMessages()
|
||||||
|
})
|
||||||
|
}
|
||||||
@@ -0,0 +1,73 @@
|
|||||||
|
import {pubkey} from "@welshman/app"
|
||||||
|
import {maybe} from "@welshman/lib"
|
||||||
|
import type {Unsubscriber} from "svelte/store"
|
||||||
|
import {getPubkeyTagValues, matchFilter, type TrustedEvent} from "@welshman/util"
|
||||||
|
import {DM_KINDS, notificationSettings} from "@app/core/state"
|
||||||
|
import type {IPushAdapter} from "@app/util/push/adapters/common"
|
||||||
|
import {onNotification} from "@app/util/push/adapters/common"
|
||||||
|
import {goToEvent} from "@app/util/routes"
|
||||||
|
|
||||||
|
export class WebNotifications implements IPushAdapter {
|
||||||
|
_unsubscriber = maybe<Unsubscriber>()
|
||||||
|
|
||||||
|
async request(prompt = true) {
|
||||||
|
if (prompt && Notification?.permission === "default") {
|
||||||
|
await Notification.requestPermission()
|
||||||
|
}
|
||||||
|
|
||||||
|
return Notification?.permission || "denied"
|
||||||
|
}
|
||||||
|
|
||||||
|
_notify(event: TrustedEvent, title: string, body: string) {
|
||||||
|
console.log("notify:", event)
|
||||||
|
|
||||||
|
const notification = new Notification(title, {
|
||||||
|
body,
|
||||||
|
tag: event.id,
|
||||||
|
icon: "/icon.png",
|
||||||
|
badge: "/icon.png",
|
||||||
|
})
|
||||||
|
|
||||||
|
notification.onclick = () => {
|
||||||
|
window.focus()
|
||||||
|
goToEvent(event)
|
||||||
|
notification.close()
|
||||||
|
}
|
||||||
|
|
||||||
|
const onVisibilityChange = () => {
|
||||||
|
if (document.visibilityState === "visible") {
|
||||||
|
notification.close()
|
||||||
|
document.removeEventListener("visibilitychange", onVisibilityChange)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
document.addEventListener("visibilitychange", onVisibilityChange)
|
||||||
|
}
|
||||||
|
|
||||||
|
async enable() {
|
||||||
|
if (!this._unsubscriber) {
|
||||||
|
this._unsubscriber = onNotification(event => {
|
||||||
|
const {push, messages, mentions, spaces} = notificationSettings.get()
|
||||||
|
|
||||||
|
if (push && document.hidden && Notification?.permission === "granted") {
|
||||||
|
if (messages && matchFilter({kinds: DM_KINDS}, event)) {
|
||||||
|
this._notify(event, "New direct message", "Someone sent you a direct message.")
|
||||||
|
} else if (
|
||||||
|
mentions &&
|
||||||
|
event.pubkey !== pubkey.get() &&
|
||||||
|
getPubkeyTagValues(event.tags).includes(pubkey.get()!)
|
||||||
|
) {
|
||||||
|
this._notify(event, "Someone mentioned you", "Someone tagged you in a message.")
|
||||||
|
} else if (spaces) {
|
||||||
|
this._notify(event, "New activity", "Someone posted a new message.")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async disable() {
|
||||||
|
this._unsubscriber?.()
|
||||||
|
this._unsubscriber = undefined
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,62 @@
|
|||||||
|
import {Capacitor} from "@capacitor/core"
|
||||||
|
import {notificationSettings, pushState} from "@app/core/state"
|
||||||
|
import {WebNotifications} from "@app/util/push/adapters/web"
|
||||||
|
import {CapacitorNotifications} from "@app/util/push/adapters/capacitor"
|
||||||
|
import {AndroidFallbackNotifications} from "@app/util/push/adapters/android"
|
||||||
|
import type {IPushAdapter} from "@app/util/push/adapters/common"
|
||||||
|
|
||||||
|
export {onNotification} from "@app/util/push/adapters/common"
|
||||||
|
|
||||||
|
export class Push {
|
||||||
|
static _adapter: IPushAdapter | undefined
|
||||||
|
|
||||||
|
static _getAdapter() {
|
||||||
|
if (!Push._adapter) {
|
||||||
|
const {useFallback} = pushState.get()
|
||||||
|
|
||||||
|
if (Capacitor.getPlatform() === "android" && useFallback) {
|
||||||
|
Push._adapter = new AndroidFallbackNotifications()
|
||||||
|
} else if (Capacitor.isNativePlatform()) {
|
||||||
|
Push._adapter = new CapacitorNotifications()
|
||||||
|
} else {
|
||||||
|
Push._adapter = new WebNotifications()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return Push._adapter
|
||||||
|
}
|
||||||
|
|
||||||
|
static async request() {
|
||||||
|
const adapter = Push._getAdapter()
|
||||||
|
|
||||||
|
let permission = await adapter.request()
|
||||||
|
if (permission !== "granted" && adapter instanceof CapacitorNotifications) {
|
||||||
|
Push._adapter = new AndroidFallbackNotifications()
|
||||||
|
permission = await Push._adapter.request()
|
||||||
|
|
||||||
|
if (permission === "granted") {
|
||||||
|
pushState.set({useFallback: true})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return permission
|
||||||
|
}
|
||||||
|
|
||||||
|
static disable() {
|
||||||
|
return Push._getAdapter().disable()
|
||||||
|
}
|
||||||
|
|
||||||
|
static enable() {
|
||||||
|
return Push._getAdapter().enable()
|
||||||
|
}
|
||||||
|
|
||||||
|
static sync() {
|
||||||
|
return notificationSettings.subscribe(({push}) => {
|
||||||
|
if (push) {
|
||||||
|
Push.enable()
|
||||||
|
} else {
|
||||||
|
Push.disable()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
+30
-44
@@ -1,4 +1,4 @@
|
|||||||
import type {Page} from "@sveltejs/kit"
|
import theme from "tailwindcss/defaultTheme"
|
||||||
import {get} from "svelte/store"
|
import {get} from "svelte/store"
|
||||||
import * as nip19 from "nostr-tools/nip19"
|
import * as nip19 from "nostr-tools/nip19"
|
||||||
import {goto} from "$app/navigation"
|
import {goto} from "$app/navigation"
|
||||||
@@ -6,7 +6,7 @@ import {page} from "$app/stores"
|
|||||||
import {nthEq} from "@welshman/lib"
|
import {nthEq} from "@welshman/lib"
|
||||||
import type {TrustedEvent} from "@welshman/util"
|
import type {TrustedEvent} from "@welshman/util"
|
||||||
import {getAddress} from "@welshman/util"
|
import {getAddress} from "@welshman/util"
|
||||||
import {tracker, loadRelay} from "@welshman/app"
|
import {tracker, userMessagingRelayList} from "@welshman/app"
|
||||||
import {identity} from "@welshman/lib"
|
import {identity} from "@welshman/lib"
|
||||||
import {
|
import {
|
||||||
getTagValue,
|
getTagValue,
|
||||||
@@ -16,18 +16,32 @@ import {
|
|||||||
ZAP_GOAL,
|
ZAP_GOAL,
|
||||||
EVENT_TIME,
|
EVENT_TIME,
|
||||||
getPubkeyTagValues,
|
getPubkeyTagValues,
|
||||||
|
getRelaysFromList,
|
||||||
} from "@welshman/util"
|
} from "@welshman/util"
|
||||||
import {
|
import {makeChatId, entityLink, encodeRelay, DM_KINDS, ROOM} from "@app/core/state"
|
||||||
makeChatId,
|
import {pushModal} from "@app/util/modal"
|
||||||
entityLink,
|
|
||||||
decodeRelay,
|
|
||||||
encodeRelay,
|
|
||||||
userSpaceUrls,
|
|
||||||
hasNip29,
|
|
||||||
DM_KINDS,
|
|
||||||
ROOM,
|
|
||||||
} from "@app/core/state"
|
|
||||||
import {lastPageBySpaceUrl, lastChatUrl} from "@app/util/history"
|
import {lastPageBySpaceUrl, lastChatUrl} from "@app/util/history"
|
||||||
|
import ChatEnable from "@app/components/ChatEnable.svelte"
|
||||||
|
|
||||||
|
// Chat
|
||||||
|
|
||||||
|
export const makeChatPath = (pubkeys: string[]) => `/chat/${makeChatId(pubkeys)}`
|
||||||
|
|
||||||
|
export const makeRoomPath = (url: string, h: string) => `/spaces/${encodeRelay(url)}/${h}`
|
||||||
|
|
||||||
|
export const makeSpaceChatPath = (url: string) => makeRoomPath(url, "chat")
|
||||||
|
|
||||||
|
export const goToChat = (pubkeys: string[] = []) => {
|
||||||
|
if (getRelaysFromList(get(userMessagingRelayList)).length === 0) {
|
||||||
|
pushModal(ChatEnable, {next: () => goToChat(pubkeys)})
|
||||||
|
} else if (pubkeys.length === 0) {
|
||||||
|
goto(lastChatUrl ?? "/chat")
|
||||||
|
} else {
|
||||||
|
goto(makeChatPath(pubkeys))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Spaces
|
||||||
|
|
||||||
export const makeSpacePath = (url: string, ...extra: (string | undefined)[]) => {
|
export const makeSpacePath = (url: string, ...extra: (string | undefined)[]) => {
|
||||||
let path = `/spaces/${encodeRelay(url)}`
|
let path = `/spaces/${encodeRelay(url)}`
|
||||||
@@ -47,24 +61,16 @@ export const makeSpacePath = (url: string, ...extra: (string | undefined)[]) =>
|
|||||||
export const goToSpace = async (url: string) => {
|
export const goToSpace = async (url: string) => {
|
||||||
const prevPath = lastPageBySpaceUrl.get(encodeRelay(url))
|
const prevPath = lastPageBySpaceUrl.get(encodeRelay(url))
|
||||||
|
|
||||||
if (prevPath) {
|
if (prevPath && prevPath !== makeSpacePath(url)) {
|
||||||
goto(prevPath)
|
goto(prevPath)
|
||||||
} else if (hasNip29(await loadRelay(url))) {
|
} else if (window.matchMedia(`(min-width: ${theme.screens.md})`).matches) {
|
||||||
goto(makeSpacePath(url, "recent"))
|
goto(makeSpacePath(url, "recent"))
|
||||||
} else {
|
} else {
|
||||||
goto(makeSpacePath(url, "chat"))
|
goto(makeSpacePath(url))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export const goToLastChat = () => {
|
// Content types, events
|
||||||
goto(lastChatUrl ?? "/chat")
|
|
||||||
}
|
|
||||||
|
|
||||||
export const makeChatPath = (pubkeys: string[]) => `/chat/${makeChatId(pubkeys)}`
|
|
||||||
|
|
||||||
export const makeRoomPath = (url: string, h: string) => `/spaces/${encodeRelay(url)}/${h}`
|
|
||||||
|
|
||||||
export const makeSpaceChatPath = (url: string) => makeRoomPath(url, "chat")
|
|
||||||
|
|
||||||
export const makeMessagePath = (url: string, event: TrustedEvent) => {
|
export const makeMessagePath = (url: string, event: TrustedEvent) => {
|
||||||
const h = getTagValue(ROOM, event.tags)
|
const h = getTagValue(ROOM, event.tags)
|
||||||
@@ -84,26 +90,6 @@ export const makeClassifiedPath = (url: string, address?: string) =>
|
|||||||
export const makeCalendarPath = (url: string, address?: string) =>
|
export const makeCalendarPath = (url: string, address?: string) =>
|
||||||
makeSpacePath(url, "calendar", address)
|
makeSpacePath(url, "calendar", address)
|
||||||
|
|
||||||
export const getPrimaryNavItem = ($page: Page) => $page.route?.id?.split("/")[1]
|
|
||||||
|
|
||||||
export const getPrimaryNavItemIndex = ($page: Page) => {
|
|
||||||
const urls = get(userSpaceUrls)
|
|
||||||
|
|
||||||
switch (getPrimaryNavItem($page)) {
|
|
||||||
case "discover":
|
|
||||||
return urls.length + 2
|
|
||||||
case "spaces": {
|
|
||||||
const routeUrl = decodeRelay($page.params.relay || "")
|
|
||||||
|
|
||||||
return urls.findIndex(url => url === routeUrl) + 1
|
|
||||||
}
|
|
||||||
case "settings":
|
|
||||||
return urls.length + 3
|
|
||||||
default:
|
|
||||||
return 0
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export const scrollToEvent = (id: string) => {
|
export const scrollToEvent = (id: string) => {
|
||||||
const element = document.querySelector(`[data-event="${id}"]`) as any
|
const element = document.querySelector(`[data-event="${id}"]`) as any
|
||||||
|
|
||||||
|
|||||||
+254
-193
@@ -45,9 +45,8 @@ import {
|
|||||||
wrapManager,
|
wrapManager,
|
||||||
onRelay,
|
onRelay,
|
||||||
} from "@welshman/app"
|
} from "@welshman/app"
|
||||||
import {isMobile} from "@lib/html"
|
import type {Unsubscriber} from "svelte/store"
|
||||||
import type {IDBTable} from "@lib/indexeddb"
|
import {db} from "@app/core/storage"
|
||||||
import {MESSAGE_KINDS, DM_KINDS} from "@app/core/state"
|
|
||||||
|
|
||||||
const kinds = {
|
const kinds = {
|
||||||
meta: [PROFILE, FOLLOWS, MUTES, RELAYS, BLOSSOM_SERVERS, MESSAGING_RELAYS, APP_DATA, ROOMS],
|
meta: [PROFILE, FOLLOWS, MUTES, RELAYS, BLOSSOM_SERVERS, MESSAGING_RELAYS, APP_DATA, ROOMS],
|
||||||
@@ -62,204 +61,266 @@ const kinds = {
|
|||||||
ROOM_REMOVE_MEMBER,
|
ROOM_REMOVE_MEMBER,
|
||||||
ROOM_CREATE_PERMISSION,
|
ROOM_CREATE_PERMISSION,
|
||||||
],
|
],
|
||||||
content: [...MESSAGE_KINDS, ...DM_KINDS],
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const rankEvent = (event: TrustedEvent) => {
|
const shouldPersistEvent = (event: TrustedEvent) =>
|
||||||
if (kinds.meta.includes(event.kind)) return 9
|
kinds.meta.includes(event.kind) ||
|
||||||
if (kinds.alert.includes(event.kind)) return 8
|
kinds.alert.includes(event.kind) ||
|
||||||
if (kinds.space.includes(event.kind)) return 7
|
kinds.space.includes(event.kind) ||
|
||||||
if (kinds.room.includes(event.kind)) return 6
|
kinds.room.includes(event.kind)
|
||||||
if (!isMobile && kinds.content.includes(event.kind)) return 5
|
|
||||||
return 0
|
|
||||||
}
|
|
||||||
|
|
||||||
const eventsAdapter = {
|
|
||||||
name: "events",
|
|
||||||
keyPath: "id",
|
|
||||||
init: async (table: IDBTable<TrustedEvent>) => {
|
|
||||||
const initialEvents = await table.getAll()
|
|
||||||
|
|
||||||
// Mark events verified to avoid re-verification of signatures
|
|
||||||
for (const event of initialEvents) {
|
|
||||||
event[verifiedSymbol] = true
|
|
||||||
}
|
|
||||||
|
|
||||||
repository.load(initialEvents)
|
|
||||||
|
|
||||||
return on(
|
|
||||||
repository,
|
|
||||||
"update",
|
|
||||||
batch(3000, async (updates: RepositoryUpdate[]) => {
|
|
||||||
const add: TrustedEvent[] = []
|
|
||||||
const remove = new Set<string>()
|
|
||||||
|
|
||||||
for (const update of updates) {
|
|
||||||
for (const event of update.added) {
|
|
||||||
if (rankEvent(event) > 0) {
|
|
||||||
add.push(event)
|
|
||||||
remove.delete(event.id)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
for (const id of update.removed) {
|
|
||||||
remove.add(id)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (add.length > 0) {
|
|
||||||
await table.bulkPut(add)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (remove.size > 0) {
|
|
||||||
await table.bulkDelete(remove)
|
|
||||||
}
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
type TrackerItem = {id: string; relays: string[]}
|
type TrackerItem = {id: string; relays: string[]}
|
||||||
|
|
||||||
const trackerAdapter = {
|
|
||||||
name: "tracker",
|
|
||||||
keyPath: "id",
|
|
||||||
init: async (table: IDBTable<TrackerItem>) => {
|
|
||||||
const relaysById = new Map<string, Set<string>>()
|
|
||||||
|
|
||||||
for (const {id, relays} of await table.getAll()) {
|
|
||||||
relaysById.set(id, new Set(relays))
|
|
||||||
}
|
|
||||||
|
|
||||||
tracker.load(relaysById)
|
|
||||||
|
|
||||||
const _onAdd = async (ids: Iterable<string>) => {
|
|
||||||
const items: TrackerItem[] = []
|
|
||||||
|
|
||||||
for (const id of ids) {
|
|
||||||
const event = repository.getEvent(id)
|
|
||||||
|
|
||||||
if (!event || rankEvent(event) === 0) continue
|
|
||||||
|
|
||||||
const relays = Array.from(tracker.getRelays(id))
|
|
||||||
|
|
||||||
if (relays.length === 0) continue
|
|
||||||
|
|
||||||
items.push({id, relays})
|
|
||||||
}
|
|
||||||
|
|
||||||
await table.bulkPut(items)
|
|
||||||
}
|
|
||||||
|
|
||||||
const _onRemove = async (ids: Iterable<string>) => {
|
|
||||||
await table.bulkDelete(Array.from(ids))
|
|
||||||
}
|
|
||||||
|
|
||||||
const onAdd = batch(3000, _onAdd)
|
|
||||||
|
|
||||||
const onRemove = batch(3000, _onRemove)
|
|
||||||
|
|
||||||
const onLoad = () => _onAdd(tracker.relaysById.keys())
|
|
||||||
|
|
||||||
const onClear = () => _onRemove(tracker.relaysById.keys())
|
|
||||||
|
|
||||||
tracker.on("add", onAdd)
|
|
||||||
tracker.on("remove", onRemove)
|
|
||||||
tracker.on("load", onLoad)
|
|
||||||
tracker.on("clear", onClear)
|
|
||||||
|
|
||||||
return () => {
|
|
||||||
tracker.off("add", onAdd)
|
|
||||||
tracker.off("remove", onRemove)
|
|
||||||
tracker.off("load", onLoad)
|
|
||||||
tracker.off("clear", onClear)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
const relaysAdapter = {
|
|
||||||
name: "relays",
|
|
||||||
keyPath: "url",
|
|
||||||
init: async (table: IDBTable<RelayProfile>) => {
|
|
||||||
relaysByUrl.set(indexBy(r => r.url, await table.getAll()))
|
|
||||||
|
|
||||||
return onRelay(batch(1000, table.bulkPut))
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
const relayStatsAdapter = {
|
|
||||||
name: "relayStats",
|
|
||||||
keyPath: "url",
|
|
||||||
init: async (table: IDBTable<RelayStats>) => {
|
|
||||||
relayStatsByUrl.set(indexBy(r => r.url, await table.getAll()))
|
|
||||||
|
|
||||||
return onRelayStats(batch(1000, table.bulkPut))
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
const handlesAdapter = {
|
|
||||||
name: "handles",
|
|
||||||
keyPath: "nip05",
|
|
||||||
init: async (table: IDBTable<Handle>) => {
|
|
||||||
handlesByNip05.set(indexBy(r => r.nip05, await table.getAll()))
|
|
||||||
|
|
||||||
return onHandle(batch(1000, table.bulkPut))
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
const zappersAdapter = {
|
|
||||||
name: "zappers",
|
|
||||||
keyPath: "lnurl",
|
|
||||||
init: async (table: IDBTable<Zapper>) => {
|
|
||||||
zappersByLnurl.set(indexBy(z => z.lnurl, await table.getAll()))
|
|
||||||
|
|
||||||
return onZapper(batch(3000, table.bulkPut))
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
type PlaintextItem = {key: string; value: string}
|
type PlaintextItem = {key: string; value: string}
|
||||||
|
|
||||||
const plaintextAdapter = {
|
const loadCriticalEvents = async () => {
|
||||||
name: "plaintext",
|
const table = db.table<TrustedEvent>("events")
|
||||||
keyPath: "key",
|
const initialEvents = await table.getAll()
|
||||||
init: async (table: IDBTable<PlaintextItem>) => {
|
const keep: TrustedEvent[] = []
|
||||||
const initialRecords = await table.getAll()
|
const drop: string[] = []
|
||||||
|
|
||||||
plaintext.set(fromPairs(initialRecords.map(({key, value}) => [key, value])))
|
for (const event of initialEvents) {
|
||||||
|
if (shouldPersistEvent(event)) {
|
||||||
return throttled(3000, plaintext).subscribe($plaintext => {
|
event[verifiedSymbol] = true
|
||||||
table.bulkPut(Object.entries($plaintext).map(([key, value]) => ({key, value})))
|
keep.push(event)
|
||||||
})
|
} else {
|
||||||
},
|
drop.push(event.id)
|
||||||
}
|
|
||||||
|
|
||||||
const wrapManagerAdapter = {
|
|
||||||
name: "wrapManager",
|
|
||||||
keyPath: "id",
|
|
||||||
init: async (table: IDBTable<WrapItem>) => {
|
|
||||||
wrapManager.load(await table.getAll())
|
|
||||||
|
|
||||||
const addOne = batch(3000, table.bulkPut)
|
|
||||||
|
|
||||||
const removeOne = throttle(3000, table.bulkDelete)
|
|
||||||
|
|
||||||
wrapManager.on("add", addOne)
|
|
||||||
wrapManager.on("remove", removeOne)
|
|
||||||
|
|
||||||
return () => {
|
|
||||||
wrapManager.off("add", addOne)
|
|
||||||
wrapManager.off("remove", removeOne)
|
|
||||||
}
|
}
|
||||||
},
|
}
|
||||||
|
|
||||||
|
repository.load(keep)
|
||||||
|
|
||||||
|
if (drop.length > 0) {
|
||||||
|
void table.bulkDelete(drop)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export const adapters = [
|
const syncEvents = () => {
|
||||||
eventsAdapter,
|
const table = db.table<TrustedEvent>("events")
|
||||||
trackerAdapter,
|
|
||||||
relaysAdapter,
|
return on(
|
||||||
relayStatsAdapter,
|
repository,
|
||||||
handlesAdapter,
|
"update",
|
||||||
zappersAdapter,
|
batch(3000, async (updates: RepositoryUpdate[]) => {
|
||||||
plaintextAdapter,
|
const add: TrustedEvent[] = []
|
||||||
wrapManagerAdapter,
|
const remove = new Set<string>()
|
||||||
]
|
|
||||||
|
for (const update of updates) {
|
||||||
|
for (const event of update.added) {
|
||||||
|
if (shouldPersistEvent(event)) {
|
||||||
|
add.push(event)
|
||||||
|
remove.delete(event.id)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const id of update.removed) {
|
||||||
|
remove.add(id)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (add.length > 0) {
|
||||||
|
await table.bulkPut(add)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (remove.size > 0) {
|
||||||
|
await table.bulkDelete(remove)
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const loadCriticalTracker = async () => {
|
||||||
|
const table = db.table<TrackerItem>("tracker")
|
||||||
|
const relaysById = new Map<string, Set<string>>()
|
||||||
|
const stale: string[] = []
|
||||||
|
|
||||||
|
for (const {id, relays} of await table.getAll()) {
|
||||||
|
if (!repository.getEvent(id)) {
|
||||||
|
stale.push(id)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
relaysById.set(id, new Set(relays))
|
||||||
|
}
|
||||||
|
|
||||||
|
tracker.load(relaysById)
|
||||||
|
|
||||||
|
if (stale.length > 0) {
|
||||||
|
void table.bulkDelete(stale)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const syncTracker = () => {
|
||||||
|
const table = db.table<TrackerItem>("tracker")
|
||||||
|
|
||||||
|
const _onAdd = async (ids: Iterable<string>) => {
|
||||||
|
const items: TrackerItem[] = []
|
||||||
|
|
||||||
|
for (const id of ids) {
|
||||||
|
const event = repository.getEvent(id)
|
||||||
|
|
||||||
|
if (!event || !shouldPersistEvent(event)) continue
|
||||||
|
|
||||||
|
const relays = Array.from(tracker.getRelays(id))
|
||||||
|
|
||||||
|
if (relays.length === 0) continue
|
||||||
|
|
||||||
|
items.push({id, relays})
|
||||||
|
}
|
||||||
|
|
||||||
|
await table.bulkPut(items)
|
||||||
|
}
|
||||||
|
|
||||||
|
const _onRemove = async (ids: Iterable<string>) => {
|
||||||
|
await table.bulkDelete(Array.from(ids))
|
||||||
|
}
|
||||||
|
|
||||||
|
const onAdd = batch(3000, _onAdd)
|
||||||
|
const onRemove = batch(3000, _onRemove)
|
||||||
|
const onLoad = () => _onAdd(tracker.relaysById.keys())
|
||||||
|
const onClear = () => _onRemove(tracker.relaysById.keys())
|
||||||
|
|
||||||
|
tracker.on("add", onAdd)
|
||||||
|
tracker.on("remove", onRemove)
|
||||||
|
tracker.on("load", onLoad)
|
||||||
|
tracker.on("clear", onClear)
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
tracker.off("add", onAdd)
|
||||||
|
tracker.off("remove", onRemove)
|
||||||
|
tracker.off("load", onLoad)
|
||||||
|
tracker.off("clear", onClear)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const loadCriticalRelays = async () => {
|
||||||
|
const table = db.table<RelayProfile>("relays")
|
||||||
|
|
||||||
|
relaysByUrl.set(indexBy(r => r.url, await table.getAll()))
|
||||||
|
}
|
||||||
|
|
||||||
|
const syncRelays = () => onRelay(batch(1000, db.table<RelayProfile>("relays").bulkPut))
|
||||||
|
|
||||||
|
const initRelayStats = async () => {
|
||||||
|
const table = db.table<RelayStats>("relayStats")
|
||||||
|
|
||||||
|
relayStatsByUrl.set(indexBy(r => r.url, await table.getAll()))
|
||||||
|
|
||||||
|
return onRelayStats(batch(1000, table.bulkPut))
|
||||||
|
}
|
||||||
|
|
||||||
|
const initHandles = async () => {
|
||||||
|
const table = db.table<Handle>("handles")
|
||||||
|
|
||||||
|
handlesByNip05.set(indexBy(r => r.nip05, await table.getAll()))
|
||||||
|
|
||||||
|
return onHandle(batch(1000, table.bulkPut))
|
||||||
|
}
|
||||||
|
|
||||||
|
const initZappers = async () => {
|
||||||
|
const table = db.table<Zapper>("zappers")
|
||||||
|
|
||||||
|
zappersByLnurl.set(indexBy(z => z.lnurl, await table.getAll()))
|
||||||
|
|
||||||
|
return onZapper(batch(3000, table.bulkPut))
|
||||||
|
}
|
||||||
|
|
||||||
|
const initPlaintext = async () => {
|
||||||
|
const table = db.table<PlaintextItem>("plaintext")
|
||||||
|
const initialRecords = await table.getAll()
|
||||||
|
|
||||||
|
plaintext.set(fromPairs(initialRecords.map(({key, value}) => [key, value])))
|
||||||
|
|
||||||
|
return throttled(3000, plaintext).subscribe($plaintext => {
|
||||||
|
table.bulkPut(Object.entries($plaintext).map(([key, value]) => ({key, value})))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const initWrapManager = async () => {
|
||||||
|
const table = db.table<WrapItem>("wrapManager")
|
||||||
|
|
||||||
|
wrapManager.load(await table.getAll())
|
||||||
|
|
||||||
|
const addOne = batch(3000, table.bulkPut)
|
||||||
|
const removeOne = throttle(3000, table.bulkDelete)
|
||||||
|
|
||||||
|
wrapManager.on("add", addOne)
|
||||||
|
wrapManager.on("remove", removeOne)
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
wrapManager.off("add", addOne)
|
||||||
|
wrapManager.off("remove", removeOne)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type StorageSync = {
|
||||||
|
unsubscribe: Unsubscriber
|
||||||
|
ready: Promise<void>
|
||||||
|
}
|
||||||
|
|
||||||
|
export const sync = (): StorageSync => {
|
||||||
|
const unsubscribers: Unsubscriber[] = []
|
||||||
|
const deferredTimers: ReturnType<typeof setTimeout>[] = []
|
||||||
|
let stopped = false
|
||||||
|
|
||||||
|
const addUnsubscriber = (unsubscriber: Unsubscriber) => {
|
||||||
|
if (stopped) {
|
||||||
|
unsubscriber()
|
||||||
|
} else {
|
||||||
|
unsubscribers.push(unsubscriber)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const scheduleDeferred = (task: () => Promise<void>) => {
|
||||||
|
const timeout = setTimeout(() => {
|
||||||
|
if (stopped) return
|
||||||
|
|
||||||
|
void task()
|
||||||
|
}, 0)
|
||||||
|
|
||||||
|
deferredTimers.push(timeout)
|
||||||
|
}
|
||||||
|
|
||||||
|
const ready = (async () => {
|
||||||
|
await db.connect()
|
||||||
|
|
||||||
|
await Promise.all([loadCriticalEvents(), loadCriticalRelays()])
|
||||||
|
await loadCriticalTracker()
|
||||||
|
|
||||||
|
addUnsubscriber(syncEvents())
|
||||||
|
addUnsubscriber(syncTracker())
|
||||||
|
addUnsubscriber(syncRelays())
|
||||||
|
|
||||||
|
scheduleDeferred(async () => {
|
||||||
|
addUnsubscriber(await initRelayStats())
|
||||||
|
})
|
||||||
|
|
||||||
|
scheduleDeferred(async () => {
|
||||||
|
addUnsubscriber(await initHandles())
|
||||||
|
})
|
||||||
|
|
||||||
|
scheduleDeferred(async () => {
|
||||||
|
addUnsubscriber(await initZappers())
|
||||||
|
})
|
||||||
|
|
||||||
|
scheduleDeferred(async () => {
|
||||||
|
addUnsubscriber(await initPlaintext())
|
||||||
|
})
|
||||||
|
|
||||||
|
scheduleDeferred(async () => {
|
||||||
|
addUnsubscriber(await initWrapManager())
|
||||||
|
})
|
||||||
|
})()
|
||||||
|
|
||||||
|
const unsubscribe = () => {
|
||||||
|
stopped = true
|
||||||
|
|
||||||
|
for (const timeout of deferredTimers) {
|
||||||
|
clearTimeout(timeout)
|
||||||
|
}
|
||||||
|
|
||||||
|
unsubscribers.forEach(unsubscriber => unsubscriber())
|
||||||
|
}
|
||||||
|
|
||||||
|
return {unsubscribe, ready}
|
||||||
|
}
|
||||||
|
|||||||
@@ -8,8 +8,7 @@ const FALLBACK_APP_NAME = "Flotilla"
|
|||||||
const staticTitles = new Map<string, string>([
|
const staticTitles = new Map<string, string>([
|
||||||
["/", "Redirecting"],
|
["/", "Redirecting"],
|
||||||
["/home", "Home"],
|
["/home", "Home"],
|
||||||
["/discover", "Join a Space"],
|
["/spaces", "Spaces"],
|
||||||
["/spaces", "Your Spaces"],
|
|
||||||
["/spaces/create", "Create a Space"],
|
["/spaces/create", "Create a Space"],
|
||||||
["/spaces/[relay]", "Space"],
|
["/spaces/[relay]", "Space"],
|
||||||
["/spaces/[relay]/chat", "Space Chat"],
|
["/spaces/[relay]/chat", "Space Chat"],
|
||||||
|
|||||||
@@ -0,0 +1,321 @@
|
|||||||
|
/**
|
||||||
|
* Voice rooms via LiveKit. Note: Voice does not work on localhost in Firefox
|
||||||
|
* (ICE candidate gathering fails). Use Chrome or test from deployed HTTPS.
|
||||||
|
*/
|
||||||
|
import {
|
||||||
|
DisconnectReason,
|
||||||
|
Room as LiveKitRoom,
|
||||||
|
RoomEvent,
|
||||||
|
Track,
|
||||||
|
type AudioCaptureOptions,
|
||||||
|
type LocalParticipant,
|
||||||
|
} from "livekit-client"
|
||||||
|
import {derived, get, writable} from "svelte/store"
|
||||||
|
import {map, removeUndefined, uniqBy} from "@welshman/lib"
|
||||||
|
import type {TrustedEvent} from "@welshman/util"
|
||||||
|
import {makeHttpAuth, makeHttpAuthHeader, getTags} from "@welshman/util"
|
||||||
|
import {signer} from "@welshman/app"
|
||||||
|
import {getLivekitEndpoint} from "$lib/livekit"
|
||||||
|
import {AbortError, whenAborted, whenTimeout} from "$lib/util"
|
||||||
|
import {deriveLatestEventForUrl, deriveRoom, makeRoomId, type Room} from "@app/core/state"
|
||||||
|
import {pushToast} from "@app/util/toast"
|
||||||
|
|
||||||
|
export const LIVEKIT_PARTICIPANTS = 39004
|
||||||
|
|
||||||
|
export {checkRelayHasLivekit} from "$lib/livekit"
|
||||||
|
|
||||||
|
export type VoiceSession = {
|
||||||
|
url: string
|
||||||
|
h: string
|
||||||
|
room: LiveKitRoom
|
||||||
|
muted: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
export type Pubkey = string
|
||||||
|
|
||||||
|
export type VoiceParticipant = {pubkey?: Pubkey; identity: string}
|
||||||
|
|
||||||
|
export enum VoiceState {
|
||||||
|
Joining = "joining",
|
||||||
|
Connected = "connected",
|
||||||
|
Disconnected = "disconnected",
|
||||||
|
}
|
||||||
|
|
||||||
|
export const currentVoiceSession = writable<VoiceSession | undefined>(undefined)
|
||||||
|
|
||||||
|
export const voiceState = writable<VoiceState>(VoiceState.Disconnected)
|
||||||
|
|
||||||
|
export const currentVoiceRoom = writable<Room | undefined>(undefined)
|
||||||
|
|
||||||
|
export const participantPubkeyMap = writable<Map<string, Pubkey>>(new Map())
|
||||||
|
|
||||||
|
const addParticipant = (identity: string) => {
|
||||||
|
participantPubkeyMap.update(m => {
|
||||||
|
const next = new Map(m)
|
||||||
|
next.set(identity, pubkeyFromLiveKitIdentity(identity) ?? "")
|
||||||
|
return next
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const deleteParticipant = (identity: string) => {
|
||||||
|
participantPubkeyMap.update(m => {
|
||||||
|
const next = new Map(m)
|
||||||
|
next.delete(identity)
|
||||||
|
return next
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export const pubkeyFromLiveKitIdentity = (identity: string): string | undefined =>
|
||||||
|
/^[a-f0-9]{64}$/.test(identity.slice(0, 64)) ? identity.slice(0, 64) : undefined
|
||||||
|
|
||||||
|
export const participantFromLiveKitIdentity = (identity: string): VoiceParticipant => {
|
||||||
|
const pk = pubkeyFromLiveKitIdentity(identity)
|
||||||
|
return pk ? {pubkey: pk, identity} : {identity}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const participantKey = (p: VoiceParticipant) => p.pubkey ?? p.identity
|
||||||
|
|
||||||
|
export const speakingParticipants = writable<VoiceParticipant[]>([])
|
||||||
|
|
||||||
|
export const isParticipantSpeaking = derived(
|
||||||
|
speakingParticipants,
|
||||||
|
$participants => (p: VoiceParticipant) =>
|
||||||
|
$participants.some(sp => participantKey(sp) === participantKey(p)),
|
||||||
|
)
|
||||||
|
|
||||||
|
const fetchLivekitToken = async (
|
||||||
|
url: string,
|
||||||
|
groupId: string,
|
||||||
|
signal?: AbortSignal,
|
||||||
|
): Promise<{server_url: string; participant_token: string}> => {
|
||||||
|
const endpoint = getLivekitEndpoint(url, groupId)
|
||||||
|
|
||||||
|
const $signer = signer.get()
|
||||||
|
if (!$signer) throw new Error("No signer available")
|
||||||
|
|
||||||
|
if (signal?.aborted) throw new DOMException("Aborted", "AbortError")
|
||||||
|
|
||||||
|
const template = await makeHttpAuth(endpoint, "GET")
|
||||||
|
const signedEvent = await $signer.sign(template)
|
||||||
|
const authHeader = makeHttpAuthHeader(signedEvent)
|
||||||
|
|
||||||
|
const response = await fetch(endpoint, {
|
||||||
|
headers: {Authorization: authHeader},
|
||||||
|
signal,
|
||||||
|
})
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
const text = await response.text()
|
||||||
|
throw new Error(`Token request failed (${response.status}): ${text}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
return response.json()
|
||||||
|
}
|
||||||
|
|
||||||
|
export const deriveVoiceParticipants = (url: string, h: string) =>
|
||||||
|
// We use the livekit identity list while in a call, and fall back to the list in kind 39004.
|
||||||
|
derived(
|
||||||
|
[
|
||||||
|
participantPubkeyMap,
|
||||||
|
currentVoiceRoom,
|
||||||
|
deriveLatestEventForUrl(url, [{kinds: [LIVEKIT_PARTICIPANTS], "#d": [h]}]),
|
||||||
|
],
|
||||||
|
([$participantPubkeyMap, $currentVoiceRoom, $publishedParticipantList]) => {
|
||||||
|
const inCall = $participantPubkeyMap.size > 0 && $currentVoiceRoom?.id === makeRoomId(url, h)
|
||||||
|
|
||||||
|
if (inCall) {
|
||||||
|
const participants = [...$participantPubkeyMap.keys()].map(participantFromLiveKitIdentity)
|
||||||
|
return uniqBy((p: VoiceParticipant) => participantKey(p), participants)
|
||||||
|
} else {
|
||||||
|
const latestEvent = $publishedParticipantList as TrustedEvent | undefined
|
||||||
|
if (!latestEvent) return []
|
||||||
|
const participants = removeUndefined(
|
||||||
|
map(
|
||||||
|
(tag: string[]) => (tag[1] ? {pubkey: tag[1], identity: tag[1]} : undefined),
|
||||||
|
getTags("participant", latestEvent.tags),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
return uniqBy((p: VoiceParticipant) => participantKey(p), participants)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
const setUpMicrophone = async (
|
||||||
|
startMuted: boolean,
|
||||||
|
preferredMicId: string | undefined,
|
||||||
|
participant: LocalParticipant,
|
||||||
|
): Promise<boolean> => {
|
||||||
|
if (startMuted) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
let muted = true
|
||||||
|
let capture: AudioCaptureOptions | undefined = undefined
|
||||||
|
if (preferredMicId) {
|
||||||
|
capture = {deviceId: preferredMicId}
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
await participant.setMicrophoneEnabled(true, capture)
|
||||||
|
muted = false
|
||||||
|
} catch (e) {
|
||||||
|
pushToast({theme: "error", message: "Could not access microphone"})
|
||||||
|
}
|
||||||
|
return muted
|
||||||
|
}
|
||||||
|
|
||||||
|
const onRoomDisconnected = (reason?: DisconnectReason) => {
|
||||||
|
currentVoiceSession.set(undefined)
|
||||||
|
if (reason !== undefined && reason !== DisconnectReason.CLIENT_INITIATED) {
|
||||||
|
voiceState.set(VoiceState.Disconnected)
|
||||||
|
const message =
|
||||||
|
reason === DisconnectReason.JOIN_FAILURE
|
||||||
|
? "Could not connect to voice room. Please try again."
|
||||||
|
: "Voice connection lost."
|
||||||
|
pushToast({theme: "error", message})
|
||||||
|
}
|
||||||
|
speakingParticipants.set([])
|
||||||
|
participantPubkeyMap.set(new Map())
|
||||||
|
}
|
||||||
|
|
||||||
|
const onTrackSubscribed = (track: Track) => {
|
||||||
|
if (track.kind === Track.Kind.Audio) {
|
||||||
|
const element = track.attach()
|
||||||
|
element.style.display = "none"
|
||||||
|
document.body.appendChild(element)
|
||||||
|
element.play().catch(() => {})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const onTrackUnsubscribed = (track: Track) => {
|
||||||
|
track.detach().forEach(el => el.remove())
|
||||||
|
}
|
||||||
|
|
||||||
|
const onActiveSpeakersChanged = (participants: {identity: string}[]) => {
|
||||||
|
speakingParticipants.set(participants.map(p => participantFromLiveKitIdentity(p.identity)))
|
||||||
|
}
|
||||||
|
|
||||||
|
const playJoinSound = () => {
|
||||||
|
const audio = new Audio("/join-voice-room.mp3")
|
||||||
|
audio.play().catch(() => {})
|
||||||
|
}
|
||||||
|
|
||||||
|
const onParticipantConnected = (participant: {identity: string}) => {
|
||||||
|
addParticipant(participant.identity)
|
||||||
|
playJoinSound()
|
||||||
|
}
|
||||||
|
|
||||||
|
const onParticipantDisconnected = (participant: {identity: string}) => {
|
||||||
|
deleteParticipant(participant.identity)
|
||||||
|
}
|
||||||
|
|
||||||
|
let joinAbortController: AbortController | undefined
|
||||||
|
|
||||||
|
export const cancelJoinVoiceRoom = () => {
|
||||||
|
joinAbortController?.abort()
|
||||||
|
}
|
||||||
|
|
||||||
|
export const joinVoiceRoom = async (
|
||||||
|
url: string,
|
||||||
|
h: string,
|
||||||
|
startMuted = true,
|
||||||
|
preferredMicId?: string,
|
||||||
|
): Promise<void> => {
|
||||||
|
cancelJoinVoiceRoom()
|
||||||
|
|
||||||
|
const session = get(currentVoiceSession)
|
||||||
|
if (session) await leaveVoiceRoom()
|
||||||
|
|
||||||
|
currentVoiceRoom.set(get(deriveRoom(url, h)))
|
||||||
|
voiceState.set(VoiceState.Joining)
|
||||||
|
|
||||||
|
const controller = new AbortController()
|
||||||
|
joinAbortController = controller
|
||||||
|
const signal = controller.signal
|
||||||
|
const isActive = () => joinAbortController === controller
|
||||||
|
|
||||||
|
try {
|
||||||
|
const {server_url, participant_token} = await fetchLivekitToken(url, h, signal)
|
||||||
|
|
||||||
|
if (signal.aborted) throw new AbortError()
|
||||||
|
|
||||||
|
const liveKitRoom = new LiveKitRoom({adaptiveStream: true, dynacast: true})
|
||||||
|
|
||||||
|
liveKitRoom.on(RoomEvent.Disconnected, onRoomDisconnected)
|
||||||
|
liveKitRoom.on(RoomEvent.ParticipantConnected, onParticipantConnected)
|
||||||
|
liveKitRoom.on(RoomEvent.ParticipantDisconnected, onParticipantDisconnected)
|
||||||
|
liveKitRoom.on(RoomEvent.TrackSubscribed, onTrackSubscribed)
|
||||||
|
liveKitRoom.on(RoomEvent.TrackUnsubscribed, onTrackUnsubscribed)
|
||||||
|
liveKitRoom.on(RoomEvent.ActiveSpeakersChanged, onActiveSpeakersChanged)
|
||||||
|
|
||||||
|
try {
|
||||||
|
await Promise.race([
|
||||||
|
liveKitRoom.connect(server_url, participant_token, {maxRetries: 0}),
|
||||||
|
whenTimeout(5_000, {
|
||||||
|
message: "Connection timed out. Please check your network and try again.",
|
||||||
|
}),
|
||||||
|
whenAborted(signal),
|
||||||
|
])
|
||||||
|
} catch (e) {
|
||||||
|
liveKitRoom.disconnect()
|
||||||
|
throw e
|
||||||
|
}
|
||||||
|
|
||||||
|
participantPubkeyMap.set(new Map())
|
||||||
|
addParticipant(liveKitRoom.localParticipant.identity)
|
||||||
|
for (const p of liveKitRoom.remoteParticipants.values()) {
|
||||||
|
addParticipant(p.identity)
|
||||||
|
}
|
||||||
|
|
||||||
|
const muted = await setUpMicrophone(startMuted, preferredMicId, liveKitRoom.localParticipant)
|
||||||
|
|
||||||
|
currentVoiceSession.set({url, h, room: liveKitRoom, muted})
|
||||||
|
voiceState.set(VoiceState.Connected)
|
||||||
|
playJoinSound()
|
||||||
|
} catch (e) {
|
||||||
|
if (isActive()) voiceState.set(VoiceState.Disconnected)
|
||||||
|
if (e instanceof AbortError) return
|
||||||
|
throw e
|
||||||
|
} finally {
|
||||||
|
if (isActive()) joinAbortController = undefined
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const leaveVoiceRoom = async () => {
|
||||||
|
const session = get(currentVoiceSession)
|
||||||
|
if (!session) return
|
||||||
|
|
||||||
|
const audio = new Audio("/leave-voice-room.mp3")
|
||||||
|
audio.play().catch(() => {})
|
||||||
|
|
||||||
|
voiceState.set(VoiceState.Disconnected)
|
||||||
|
currentVoiceSession.set(undefined)
|
||||||
|
session.room.disconnect()
|
||||||
|
speakingParticipants.set([])
|
||||||
|
participantPubkeyMap.set(new Map())
|
||||||
|
}
|
||||||
|
|
||||||
|
export const rejoinVoiceRoom = async (): Promise<void> => {
|
||||||
|
const target = get(currentVoiceRoom)
|
||||||
|
if (!target) return
|
||||||
|
return joinVoiceRoom(target.url, target.h)
|
||||||
|
}
|
||||||
|
|
||||||
|
export const toggleMute = async () => {
|
||||||
|
const session = get(currentVoiceSession)
|
||||||
|
if (!session) return
|
||||||
|
|
||||||
|
const muted = !session.muted
|
||||||
|
if (muted) {
|
||||||
|
// Disable and re-enable microphone to trigger permission prompt
|
||||||
|
session.room.localParticipant.setMicrophoneEnabled(false)
|
||||||
|
currentVoiceSession.set({...session, muted})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
await session.room.localParticipant.setMicrophoneEnabled(true)
|
||||||
|
currentVoiceSession.set({...session, muted})
|
||||||
|
} catch (e) {
|
||||||
|
pushToast({theme: "error", message: "Could not access microphone"})
|
||||||
|
}
|
||||||
|
}
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user